The Open–Closed Principle (OCP)

The Open–Closed Principle (OCP)

By Nan Wang

Overload, 21(113):14-15, February 2013


Changing requirements and environments can require cascading changes through software. Nan Wang demonstrates how the Open-Closed principle can minimise changes.

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

When the requirements of an application change, 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 depend 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 which 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 [ APPP ]. I start my code with a 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 a 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 for converting 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 for the transport to handle. However, the transport was optimised for handling large messages, so I modified the MessageSender to send messages in batches of size of ten (Listing 1).

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();
  }
}
			
Listing 1

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

  1. The MessageSender class needs to be modified if we change how messages are batched in the future. It violated the Open-Closed Principle.
  2. The MessageSender had a secondary responsibility to batch messages in addition to the responsibility of converting/delegating 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 a word, the module was open for extensions of different batch strategy. The MessageSender kept its single responsibility of converting/delegating messages, which means it does not get modified if similar changes happen in the future. The module was closed for modification (see Listing 2).

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();
    }
}
			
Listing 2

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 versions 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 just the spring configuration file. (Listing 3)

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();
  }
}
			
Listing 3

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

Conclusion

The Open-Closed Principle serves as an useful guidance for writing a 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 deferring 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






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.