This is a tricky one to explain without some concrete examples. I recommend you to read Robert Martin's book "Agile Software Development, Principles, Patterns, and Practices". This book is also where the Open-Close principle comes from.
Domain objects with rich behaviour does not conflict with Open-close principle. If they have no behaviour, you can't create reasonable extension. The key to apply open-close principle is to anticipate future changes and create new interfaces to fulfil roles and keep them single responsibility.
I am going to tell a story of applying the open-close principle in real code. Hopefully it helps.
I had a Sender class that send messages at the begining:
package com.thinkinginobjects;
public class MessageSender {
private Transport transport;
public void send(Message message) {
byte[] bytes = message.toBytes();
transport.sendBytes(bytes);
}
}
One day, I was asked to send messages in a batch of 10. A simple solution would be:
package com.thinkinginobjects;
public class 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();
}
}
}
However my experience told me this is may not be the end of the story. I anticipated that people will require different way of batching the messages. Hence I created a batch strategy and made my Sender to use it. Note that I was applying Open-close principle here. If I had new kind of batching strategy in the future, my code is open for extension (by adding a new BatchStrategy), but close to modification (by not modifying any existing code). However as Robert Martin stated in his book, when the code is open for some types of changes, it is also close to other type of changes. If someone want to notify a component after sent in the future, my code is not open for this type of change.
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();
}
}
Jus to complete the story, I received an requirement a couple of days later to send batched messages every 5 seconds. My guess was right and I can accommodate the requirement by adding extensions instead of modifying my code:
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();
}
}
- Disclaimer: The code example belongs to www.thinkingInObjects.com