开闭原则指出:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭
我现在正在设计一个域,并在我的域实体中包含相当多的行为。我正在使用域事件并将依赖项注入方法中,因此请确保我的实体不与外部影响耦合。但是,我突然想到,如果客户以后想要更多功能,我将不得不违反 OCP 并破解打开这些域实体以添加功能。行为丰富的领域实体如何与开闭原则和谐相处?
开闭原则指出:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭
我现在正在设计一个域,并在我的域实体中包含相当多的行为。我正在使用域事件并将依赖项注入方法中,因此请确保我的实体不与外部影响耦合。但是,我突然想到,如果客户以后想要更多功能,我将不得不违反 OCP 并破解打开这些域实体以添加功能。行为丰富的领域实体如何与开闭原则和谐相处?
在设计类时牢记开闭原则 (OCP) 很有用,但立即使类“关闭以供修改”并不总是可行或可取的。我认为单一职责原则(SRP)在实践中更有用——只要一个类只做一件事,如果对那一件事的要求发生变化,就可以修改它。
此外,随着时间的推移,SRP 会导致 OCP;如果您发现自己经常更改一个类,您最终会对其进行重构,以便将更改部分隔离在一个单独的类中,从而使原始类更加封闭。
答案很简单:工厂方法和接口 + 组合。
Open for extension 意味着您使用新的子类添加新功能。
要启用它,您必须使用工厂来创建域对象。我通常将我的存储库用作工厂。
如果您针对接口而不是具体代码进行编码,您将可以轻松添加新功能:
功能本身可以是您使用组合包含的小类:
public class ManagerSupervisor : User, IManager, ISupervior
(
public ManagerSupervisor()
{
// used to work with the supervisor features.
// without breaking Law Of Demeter
_supervisor = new SuperVisorFeatures(this);
}
)
如果没有一些具体的例子,这是一个很难解释的问题。我建议您阅读 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();
}
}
你不必打开你的课程。您只需要附加额外的条件事件处理程序。
域事件-事件处理程序关系可以是一对多的,并且处理程序可以根据域事件参数的具体类型触发。
简单的答案是确保:
在繁重的组件中实现打开/关闭原则通常要容易得多,但在应用程序流控制和业务逻辑通常需要最大更改的应用程序部分应用相同的原则要困难得多。通过配置运行工作流是理想的。
领域驱动设计很难与良好的编程设计原则相协调,DDD 的关键部分是语言和基本原理,以使低级概念远离业务领域,并使业务模型保持高层次的语言。用户和业务使用,即“通用语言”,以防止对业务模型的软件/技术污染。