Open-Closed Principle

LMAX Exchange

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

Any opinions, news, research, analyses, prices or other information ("information") contained on this Blog, constitutes marketing communication and it has not been prepared in accordance with legal requirements designed to promote the independence of investment research. Further, the information contained within this Blog does not contain (and should not be construed as containing) investment advice or an investment recommendation, or an offer of, or solicitation for, a transaction in any financial instrument. LMAX Group has not verified the accuracy or basis-in-fact of any claim or statement made by any third parties as comments for every Blog entry.

LMAX Group will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information. No representation or warranty is given as to the accuracy or completeness of the above information. While the produced information was obtained from sources deemed to be reliable, LMAX Group does not provide any guarantees about the reliability of such sources. Consequently any person acting on it does so entirely at his or her own risk. It is not a place to slander, use unacceptable language or to promote LMAX Group or any other FX and CFD provider and any such postings, excessive or unjust comments and attacks will not be allowed and will be removed from the site immediately.