SOLID: Open Closed Principle
Photo by Tim Mossholder from Pexels

SOLID: Open Closed Principle

All programs are composed of functions and data structures and the SOLID principles introduced by Robert C. Martin, help us group our code into modules and advise how to interconnect them. The whole point of application architecture is to manage change. Software requirements change all the time and we as developers need to make sure that these changes don't break existing code.

There are 5 principles that make up the acronym are:

In this post I will attempt to explain the OCP (Open Closed Principle).

What is the OCP?

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. Meaning that we need to check what is common across all objects and refactor that into a common interface then use that abstraction. So the OCP is purely coding up abstractions instead of concretions

A more realistic meaning would be that when we need to extend functionality we need to refactor all functionality into a common

huh

This explanation is quite confusing if you see this for the first time. This statement, at first, seems like a contradiction.

To explain, let's create a function which Does Not Follow OSP:

1import { MESSAGE_TYPE } from "./models/message-type"; 2import { emailLib } from './core/email-lib' 3import { smsLib } from './core/sms-lib' 4 5class Message { 6 messageType: string 7 to: string 8 body: string 9 subject: string 10 11 constructor(messageType: string, body: string, to: string, subject: string) { 12 this.messageType = messageType; 13 this.body = body; 14 this.to = to; 15 this.subject = subject; 16 } 17} 18 19async function sendMessages(messages: Message[]) { 20 let messageLog: string[] = []; 21 messages.forEach(async (message) => { 22 23 if (message.messageType === MESSAGE_TYPE.SMS) { 24 const accepted = await smsLib.send(message.body, message.to) 25 messageLog.push(`${MESSAGE_TYPE.SMS} ${(accepted ? "sent" : "not sent")}`) 26 } 27 28 if (message.messageType === MESSAGE_TYPE.EMAIL) { 29 const accepted = await emailLib.send(message.body, message.subject, message.to) 30 messageLog.push(`${MESSAGE_TYPE.EMAIL} ${(accepted ? "sent" : "not sent")}`) 31 } 32 33 }) 34 35 return messageLog 36} 37 38async function main() { 39 const messages = [ 40 new Message(MESSAGE_TYPE.EMAIL, "email message 1", "jim@abc.com", "My 1st Email",), 41 new Message(MESSAGE_TYPE.EMAIL, "email message 2", "jack@xyz.com", "My 2nd Email"), 42 new Message(MESSAGE_TYPE.SMS, "sms message 1", "0825555555", "") 43 ]; 44 const result = await sendMessages(messages); 45 console.log(result); 46} 47 48main() 49
OUTPUT:
[ 'EMAIL sent', 'EMAIL sent', 'SMS sent' ]

This program takes a list of messages of different types and uses the sendMessages function to loop through them and send them one by one. When all messages are sent, a message log prints

The sendMessages function does not follow OSP because it is not open to extension. It has to know too much about the message object and can only ever handle Email and SMS messages. What if we change the message property from "body" to "content"? What if we want to add support for another message type. This will surely break sendMessages. We'd be forced to modify the function.

When we write code, it's all about anticipating the future by leaving existing code untouched and adding new functionality with new code. This prevents situations in which a change to one module will also require other modules to change.

Let's fix the program. We can achieve OSP by changing the Message class to an interface with one send method. We can then add message classes that implement the Message interface. This polymorphism helps us implement the Open/Closed Principle.

1import { MESSAGE_TYPE } from "./models/message-type"; 2import { emailLib } from './core/email-lib' 3import { smsLib } from './core/sms-lib' 4 5interface Message { 6 send(): Promise<string> 7} 8 9class EmailMessage implements Message { 10 private to: string 11 private body: string 12 private subject: string 13 14 constructor(body: string, to: string, subject: string) { 15 this.body = body; 16 this.to = to; 17 this.subject = subject; 18 } 19 async send(): Promise<string> { 20 const accepted = await emailLib.send(this.body, this.subject, this.to) 21 return `${MESSAGE_TYPE.EMAIL} ${(accepted ? "sent" : "not sent")}` 22 } 23} 24 25class SMSMessage implements Message { 26 private to: string 27 private body: string 28 29 constructor(body: string, to: string) { 30 this.body = body; 31 this.to = to; 32 } 33 34 async send(): Promise<string> { 35 const accepted = await smsLib.send(this.body, this.to) 36 return `${MESSAGE_TYPE.SMS} ${(accepted ? "sent" : "not sent")}` 37 } 38} 39 40async function sendMessages(messages: Message[]) { 41 let messageLog: string[] = []; 42 for (const message of messages) { 43 const result = await message.send() 44 messageLog.push(result) 45 } 46 return messageLog 47} 48 49 50async function main() { 51 const messages = [ 52 new EmailMessage("email message 1", "paul@abc.com", "My 1st Email",), 53 new EmailMessage("email message 2", "james@xyz.com", "My 2nd Email"), 54 new SMSMessage("sms message 1", "0825555555") 55 ]; 56 const result = await sendMessages(messages); 57 console.log(result); 58} 59 60main()
OUTPUT:
[ 'EMAIL sent', 'EMAIL sent', 'SMS sent' ]

The sendMessages now adheres to OCP. It just does not need to know anything about the inner workings of the message object. It merely loops through the messages and invokes the public send method. The function is now unaffected by new message types or property changes.

In Conclusion

We must try to anticipate the future as best we can by writing them in a way that shouldn't change when requirements might. 

The open-closed principle allows for easier extension while reducing time spent adapting existing code