我怎样才能拥有一个遵守开闭原则的行为丰富的域实体?

Posted

技术标签:

【中文标题】我怎样才能拥有一个遵守开闭原则的行为丰富的域实体?【英文标题】:How can I have a behavior-rich domain entity that adheres to Open-Closed Principle? 【发布时间】:2012-09-04 01:44:18 【问题描述】:

Open-Closed Principle 声明:

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭

我现在正在设计一个域,并在我的域实体中包含相当多的行为。我正在使用域事件并将依赖项注入方法中,因此请确保我的实体不与外部影响耦合。但是,我突然想到,如果客户以后想要更多功能,我将不得不违反 OCP 并破解打开这些域实体以添加功能。行为丰富的领域实体如何与开闭原则和谐相处?

【问题讨论】:

我认为OCP最好应用在框架场景中。我认为驱动动机之一是防止修改导致的回归错误,而不是选择可扩展性来添加功能。这在 DDD 中并不真正适用,因为您没有将域类设计为可被第三方扩展。 【参考方案1】:

在设计类时牢记开闭原则 (OCP) 很有用,但立即使类“关闭以进行修改”并不总是可行或可取的。我认为单一职责原则 (SRP) 在实践中更有用——只要一个类只做一件事,如果该一件事的要求发生变化,就可以修改它。

此外,随着时间的推移,SRP 会导致 OCP;如果您发现自己经常更改一个类,您最终会对其进行重构,以便将更改部分隔离在一个单独的类中,从而使原始类更加封闭。

【讨论】:

显然里程可能会有所不同...根据我的经验,我会说,如果您遵守 SRP 并且课程没有硬编码,您永远不需要修改它。如果需求发生变化,那么您通常可以为新需求编写一个新类,如果您做得对,则将两者互换。原因是您可能必须同时执行这两项操作,但在不同的配置/运行时条件下......一种方法很容易做到这一点,而另一种方法则不是 @StuartWakefield:这可能适用于通用类,但不适用于域对象。假设您有一个 Shipment 类并且货件的业务定义发生了变化,您将如何在不更改类的情况下进行? 这确实取决于变化的性质,但我同意你的观点。如果不能通过配置或将Shipment 换成新的Shipment 类来解决,那么您别无选择,只能修改原来的类。 例如。以实体框架或任何 ORM 为例。代码优先开发。您是否建议每次需要诸如附加属性之类的新要求时创建一个新课程?因为这将需要数据库中的新表并迁移现有数据。修改类和修改表不是更简单实用吗?【参考方案2】:

答案很简单:工厂方法和接口 + 组合。

为扩展开放意味着您使用新的子类添加新功能。

要启用,您必须使用工厂来创建域对象。我通常将我的存储库用作工厂。

如果您针对接口而不是具体代码编写代码,则可以轻松添加新功能:

IUser IManager : IUser(添加一些管理器功能) ISupervisor : IUser(添加主管功能)

特性本身可以是小类,您可以使用组合来包含它们:

public class ManagerSupervisor : User, IManager, ISupervior
(
    public ManagerSupervisor()
    
        // used to work with the supervisor features.
        // without breaking Law Of Demeter
        _supervisor = new SuperVisorFeatures(this);

    
)

【讨论】:

【参考方案3】:

如果没有一些具体的例子,这是一个很难解释的问题。我建议您阅读 Robert Martin 的书《敏捷软件开发、原则、模式和实践》。这本书也是开闭原则的由来。

具有丰富行为的域对象与开闭原则不冲突。如果他们没有行为,您将无法创建合理的扩展。应用开闭原则的关键是预测未来的变化并创建新的接口来履行角色并使他们保持单一职责。

我将讲述一个在实际代码中应用开闭原则的故事。希望对您有所帮助。

我有一个在开始时发送消息的 Sender 类:

package com.thinkinginobjects;

public class MessageSender 

private Transport transport;

public void send(Message message) 
    byte[] bytes = message.toBytes();
    transport.sendBytes(bytes);


有一天,我被要求分批发送 10 条消息。一个简单的解决方案是:

包 com.thinkinginobjects;

公共类 MessageSenderWithBatch

private static final int BATCH_SIZE = 10;

private Transport transport;

private List<Message> buffer = new ArrayList<Message>();

public void send(Message message) 
    buffer.add(message);
    if (buffer.size() == BATCH_SIZE) 
        for (Message each : buffer) 
            byte[] bytes = each.toBytes();
            transport.sendBytes(bytes);
        
                    buffer.clear();
    


但是我的经验告诉我,这可能不是故事的结局。我预计人们将需要不同的方式来批处理消息。因此,我创建了一个批处理策略并让我的 Sender 使用它。 请注意,我在这里应用了开闭原则。 如果我将来有新的批处理策略,我的代码可以扩展(通过添加新的 BatchStrategy),但接近修改(通过不修改任何现有代码)。然而,正如 Robert Martin 在他的书中所说,当代码对某些类型的更改开放时,它也接近于其他类型的更改。如果将来有人想在发送后通知组件,我的代码不会针对此类更改开放。

package com.thinkinginobjects;

public class MessageSenderWithStrategy 

private Transport transport;

private BatchStrategy strategy;

public void send(Message message) 
    strategy.newMessage(message);
    List<Message> messages = strategy.getMessagesToSend();

    for (Message each : messages) 
        byte[] bytes = each.toBytes();
        transport.sendBytes(bytes);
    
    strategy.sent();



package com.thinkinginobjects;

public class FixSizeBatchStrategy implements BatchStrategy 

private static final int BATCH_SIZE = 0;
private List<Message> buffer = new ArrayList<Message>();

@Override
public void newMessage(Message message) 
    buffer.add(message);    


@Override
public List<Message> getMessagesToSend() 
    if (buffer.size() == BATCH_SIZE) 
        return buffer;
     else 
        return Collections.emptyList();
    


@Override
public void sent() 
    buffer.clear(); 



为了完成这个故事,几天后我收到了一个要求,要求每 5 秒发送一次批量消息。我的猜测是正确的,我可以通过添加扩展而不是修改我的代码来满足要求:

package com.thinkinginobjects;

public class FixIntervalBatchStrategy implements BatchStrategy 

private static final long INTERVAL = 5000;

private List<Message> buffer = new ArrayList<Message>();

private volatile boolean readyToSend;

public FixIntervalBatchStrategy() 
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    executorService.scheduleAtFixedRate(new Runnable() 

        @Override
        public void run() 
            readyToSend = true;

        
    , 0, INTERVAL, TimeUnit.MILLISECONDS);


@Override
public void newMessage(Message message) 
    buffer.add(message);


@Override
public List<Message> getMessagesToSend() 
    if (readyToSend) 
        return buffer;
     else 
        return Collections.emptyList();
    


@Override
public void sent() 
    readyToSend = false;
    buffer.clear();


免责声明:代码示例属于 www.thinkingInObjects.com

【讨论】:

我认为这不是他真正要问的。您的代码是简单的服务,OCP 在这里显然是可以的。但是对于域实体(模型)来说,OCP 通常是不可取的。【参考方案4】:

你在领域事件模式的正确轨道上。

您不必打开课程。您只需要附加额外的条件事件处理程序。

域事件 - 事件处理程序关系可以是一对多的,并且处理程序可以根据域事件参数的具体类型触发。

【讨论】:

【参考方案5】:

简单的答案是确保:

    它可以尽其所能地完成任务,因此如果您以后发现它有问题,则不需要您对其进行修改。虽然开放/封闭原则不涵盖错误,但仅更改。 它鼓励替换,即它实现了一个合理的接口。该组件应换成另一个组件,而不是修改。 使其可配置,任何可以更改的硬编码参数都应可配置。 将可能更改的代码外部化,使用控制反转导出任何可能需要更改的功能。

在繁重的组件中实现打开/关闭原则通常要容易得多,但在应用程序流控制和业务逻辑通常需要最大更改的应用程序部分应用相同的原则要困难得多。通过配置运行工作流是理想的。

领域驱动设计很难与良好的编程设计原则相协调,DDD 的关键部分是使低级概念远离业务领域并保持业务模型高级别的语言和基本原理。用户和企业使用的语言,也就是“通用语言”,以防止软件/技术污染商业模式。

【讨论】:

以上是关于我怎样才能拥有一个遵守开闭原则的行为丰富的域实体?的主要内容,如果未能解决你的问题,请参考以下文章

设计模式遵循的原则

Java中的工厂模式

代码设计原则--开闭原则

[转]设计模式六大原则[6]:开闭原则

开闭原则 Open Closed Principle

设计模式六大原则:开闭原则