Open-Closed Principle: Extending Functionality Without Changing Core Logic
Umang Ahuja
Lead Engineer at Arcesium | My Newsletter - Joy Of Engineering | YouTuber @getsetpython
Do you remember the first rule of programming...
The world of software engineering evolves at a breakneck pace, and so do the requirements of your software.
Clients frequently request new features, and as a developer, you're always striving to improve your product by enhancing existing functionality. But think about how exhausting it can be to modify code that’s already working perfectly—retesting everything and battling the anxiety of potentially breaking something that was fine before your change.
Do these thoughts cross your mind when you’re implementing new changes? If so, it could be a sign of a code structure that’s not flexible enough.
This is where the Open-Closed Principle comes to the rescue.
The Open-Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
First introduced by Bertrand Meyer and later popularized as part of the SOLID principles, OCP suggests that your code should be designed to allow new functionality to be added without altering existing code.
At first glance, this might seem a bit abstract, especially if you’re encountering the concept for the first time.
But don’t worry—follow along, and by the end of this post, you’ll have a clearer understanding of how this principle can help make your code more adaptable and robust.
Benefits of Applying the Open-Closed Principle
Think back to those times when you needed to introduce new functionality into an existing workflow. You likely had to modify a switch block, a series of if-else statements, or even rewrite multiple functions. Every new addition became tightly coupled to the core of your application, increasing the risk of unintended side effects when making changes.
By adopting the Open-Closed Principle, you can shield your core logic from these risks. OCP helps ensure that your code is:
The Open-Closed Principle encourages you to decouple entities while maintaining high cohesion between related components. It promotes smaller, manageable classes that allow new extensions to be introduced without modifying the core code, keeping the system stable and secure.
Implementing OCP in Your Codebase
The Single Responsibility Principle (SRP) and the Open-Closed Principle (OCP) often go hand-in-hand. SRP helps us ensure that each module or class has a single responsibility, paving the way for the modular structure needed to effectively implement OCP.
The first step in applying OCP is identifying areas where it’s needed. The simplest way to do this is by looking for:
Conditional logic such as switch or if-else statements, which may require future extensions to handle new functionality.
Once you’ve identified these areas, the key to implementing OCP is to ensure that new entities or functionalities implement an interface. This approach is known as programming to interfaces, a best practice where different parts of the codebase interact via interfaces rather than concrete classes.
By programming to interfaces, you keep your code loosely coupled. The caller functions don't need to know about the specific classes—they only rely on the base interface. This allows you to introduce new functionality by simply creating new implementations of the interface without modifying existing code.
In essence, OCP enables the client to interact with your system without knowing the specifics of new classes, allowing developers to extend functionality without disrupting the existing structure.
Real World Example of Open Closed Principle
Understanding a concept becomes much clearer when we see how it benefits us in a real-world scenario.
Let’s start with an example that doesn’t follow the Open-Closed Principle (OCP), recognize its limitations, and then refactor it to adhere to OCP.
领英推荐
Initial Code (Violates OCP)
public class NotificationService {
public void notify(User receiver, String message, String notificationType) {
if ("SMS".equals(notificationType)) {
sendSMS(receiver, message);
} else if ("Email".equals(notificationType)) {
sendEmail(receiver, message);
} else {
System.out.println("Notification Type " + notificationType + " is not supported");
}
}
private void sendEmail(User receiver, String message) {
System.out.println("Email sent to " + receiver.getEmail());
}
private void sendSMS(User receiver, String message) {
System.out.println("SMS sent to " + receiver.getPhoneNumber());
}
}
In this NotificationService, we have a method that handles different types of notifications (SMS, Email) based on the notificationType. However, the problem arises when we want to add a new notification type, say Push Notifications.
To add a new type, we would have to modify the notify() method, adding yet another if condition. This breaks OCP, as the class is now open to modification whenever we introduce new functionality.
At first glance, you might think, "What's the big deal? It’s just adding another if statement." However, this seemingly small change can become problematic in a larger codebase. The notify() method may grow complex, and future modifications could introduce bugs or side effects in existing functionality.
Let’s refactor the code to follow OCP.
We will be applying the Strategy Design Pattern to help achieve OCP.
Refactored Code (Follows OCP)
The first step is to define a common interface that each notification type will implement. This interface will have a send method to handle the sending logic for each notification.
public interface NotificationStrategy {
void send(User receiver, String message);
}
Each type of notification (SMS, Email) will now have its own class that implements the NotificationStrategy interface and defines the send functionality.
public class SMSNotification implements NotificationStrategy {
@Override
public void send(User receiver, String message) {
System.out.println("SMS sent to " + receiver.getPhoneNumber());
}
}
public class EmailNotification implements NotificationStrategy {
@Override
public void send(User receiver, String message) {
System.out.println("Email sent to " + receiver.getEmail());
}
}
Now, the NotificationService is no longer responsible for handling different notification types. Instead, it relies on the NotificationStrategy interface. This keeps the core logic unchanged, even when new notification types are added.
public class NotificationService {
private NotificationStrategy notificationStrategy;
public NotificationService(NotificationStrategy notificationStrategy) {
this.notificationStrategy = notificationStrategy;
}
public void notify(User receiver, String message) {
notificationStrategy.send(receiver, message);
}
}
To add a new notification type, like Push Notifications, all you need to do is implement the NotificationStrategy interface without modifying the NotificationService.
public class PushNotification implements NotificationStrategy {
@Override
public void send(User receiver, String message) {
System.out.println("Push Notification sent to " + receiver.getName());
}
}
With this refactored solution, we achieve the essence of OCP: the NotificationService is now open for extension (adding new notification types) but closed for modification (no changes to the core logic are needed).
Final Thoughts!
The Open-Closed Principle (OCP) is arguably the most important of the five SOLID principles. While it's fairly easy to implement, it can be one of the hardest to consistently follow. After all, there are situations where applying OCP may either be impractical or lead to over-engineering.
However, understanding OCP allows you to make informed decisions about when and where it should be applied.
That said, whenever you foresee a class or module having the potential to grow or change in the future, consider implementing OCP. Ensuring that your code is decoupled and ready for extension will make it more adaptable, maintainable, and resilient as requirements evolve.
If you enjoy this newsletter, leave a like below ??, share your thoughts in the comments section ??, and don’t forget to follow for more such content! ????
Keep showering your love on this one, and I’ll see you next time with something new. Till then, have fun and keep the Joy of Engineering alive within you! ???
#SoftwareEngineering #Learning #Design #Java #Innovation #JoyOfEngineering