Open-Closed Principle

The Open/Closed Principle (OCP)

“Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.” [APPP]

When the requirements of an application changes, if the application confirms to OCP, we can extend the existing modules with new behaviours to satisfy the changes (Open for extension). Extending the behaviour of the existing modules does not result in changes to the source code of the existing modules (Closed for modification). Other modules that depends on the extended modules are not affected by the extension. Therefore we don’t need to recompile and retest them after the change. The scope of the change is localised and much easier to implement.

The key of OCP is to place useful abstractions (abstract classes/interfaces) in the code for future extensions. However it is not always obvious that what abstractions are necessary. It can lead to over complicated software if we add abstractions blindly. I found Robert C Martin’s “Fool me once” attitude very useful. I start my code with minimal number of abstractions. When a change of requirements takes place, I modify the code to add an abstraction and protect myself from future changes of the similar kind.

I recently implemented a simple module that sends messages and made a series of changes to it afterward. I feel it is a good example of OCP to share.

At the beginning, I created a MessageSender that is responsible to convert an object message to a byte array and send it through a transport.

package com.thinkinginobjects;

public class MessageSender {

	private Transport transport;

	public synchronized void send(Message message) throws IOException{
		byte[] bytes = message.toBytes();
		transport.sendBytes(bytes);
	}
}

After the code was deployed to production, we found out that we sent messages too fast that the transport cannot handle. However the transport was optimised for handling large messages, I modified the MessageSender to send messages in batches of size of ten.

package com.thinkinginobjects;

public class MessageSenderWithBatch {

	private static final int BATCH_SIZE = 10;

	private Transport transport;

	private List buffer = new ArrayList();

	private ByteArrayOutputStream byteStream = new ByteArrayOutputStream();

	public MessageSenderWithBatch(Transport transport) {
		this.transport = transport;
	}

	public synchronized void send(Message message) throws IOException {
		buffer.add(message);
		if (buffer.size() == BATCH_SIZE) {
			sendBuffer();
		}
	}

	private void sendBuffer() throws IOException {
		for (Message each : buffer) {
			byte[] bytes = each.toBytes();
			byteStream.write(bytes);
		}
		byteStream.flush();
		transport.sendBytes(byteStream.toByteArray());
		byteStream.reset();
	}

}

The solution was simple but I hesitated to commit to it. There were two reasons:

  1. MessageSender class need to be modified if we change how messages are batched in the future. It violated the Open-Closed Principle.
  2. MessageSender had secondary responsibility to batch messages in addition to the responsibility of convert/delegate messages. It violated the Single Responsibility Principle.

Therefore I created a BatchingStrategy abstraction, who was solely responsible for deciding how message are batched together. It can be extended by different implementations if the batch strategy changes in the future. In another word, the module was open for extensions of different batch strategy. The MessageSender kept its single responsibility that converting/delegating messages, which means it does not get modified if similar changes happen in the future. The module was closed for modification.

package com.thinkinginobjects;

public class MessageSenderWithStrategy {

	private Transport transport;

	private BatchStrategy strategy;

	private ByteArrayOutputStream byteStream = new ByteArrayOutputStream();

	public synchronized void send(Message message) throws IOException {
		strategy.newMessage(message);
		List buffered = strategy.getMessagesToSend();
		sendBuffer(buffered);
		strategy.sent();
	}

	private void sendBuffer(List buffer) throws IOException {
		for (Message each : buffer) {
			byte[] bytes = each.toBytes();
			byteStream.write(bytes);
		}
		byteStream.flush();
		transport.sendBytes(byteStream.toByteArray());
		byteStream.reset();
	}
}
package com.thinkinginobjects;

public class FixSizeBatchStrategy implements BatchStrategy {

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

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

	@Override
	public List getMessagesToSend() {
		if (buffer.size() == BATCH_SIZE) {
			return buffer;
		} else {
			return Collections.emptyList();
		}
	}

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

The patch was successful, but two weeks later we figured out that we can batch the messages together in time slices and overwrite outdated messages with newer version in the same time slice. The solution was specific to our business domain of publishing market data.

More importantly, the OCP showed its benefits when we implemented the change. We only needed to extend the existing BatchStrategy interface with an different implementation. We didn’t change a single line of code but the spring configuration file.

package com.thinkinginobjects;

public class FixIntervalBatchStrategy implements BatchStrategy {

	private static final long INTERVAL = 5000;

	private List buffer = new ArrayList();

	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 getMessagesToSend() {
		if (readyToSend) {
			List toBeSent = buffer;
			buffer = new ArrayList();
			return toBeSent;
		} else {
			return Collections.emptyList();
		}
	}

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

* For the sake of simplicity, I left the message coalsecing logic out of the example.

Conclusion:
The Open-Closed Principle serves as an useful guidance for writing good quality module that is easy to change and maintain. We need to be careful not to create too many abstractions prematurely. It is worth to defer the creation of abstractions to the time when the change of requirement happens. However, when the changes strike, don’t hesitate to create an abstraction and make the module to confirm OCP. There is a great chance that a similar change of the same kind is at your door step.

References: [APPP] – Agile Software Development, Principles, Patterns, and Practices – Robert C Martin

Advertisements

2 Responses to “Open-Closed Principle”


  1. 1 fbuontempo March 21, 2013 at 4:23 pm

    Thanks for letting us publish this in ACCU’s Overload: accu.org/var/uploads/journals/Overload113.pdf

    I wondered if you’d seen Jon Skeet’s blog:
    http://msmvps.com/blogs/jon_skeet/archive/2013/03/15/the-open-closed-principle-in-review.aspx

  2. 2 Pure Logic February 27, 2014 at 8:36 am

    Excellent! That was great 🙂


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s





%d bloggers like this: