Open Closed Principle
The Open/Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.
In simple terms, whenever we design a class, we need to carefully encapsulate the implementation details so that it has good maintainability. We want it to be open to extension but closed to modification.
Understanding the Principle
Section titled “Understanding the Principle”The behavior of a module can be extended without modifying its source code. This is typically achieved through mechanisms such as:
- Inheritance - Creating subclasses
- Interfaces - Implementing contracts
- Composition - Combining objects
Example: Notification System
Section titled “Example: Notification System”Consider a notification system where you need to send alerts through different channels. Instead of modifying the core notification logic every time you add a new channel, you can extend it through inheritance.
Violating OCP (Bad Approach)
Section titled “Violating OCP (Bad Approach)”class NotificationService: def send_notification(self, channel: str, message: str): if channel == "email": print(f"Sending email: {message}") elif channel == "slack": print(f"Posting to Slack: {message}") elif channel == "teams": print(f"Sending Teams message: {message}") # ❌ Must modify this class to add new channels!public class NotificationService { public void sendNotification(String channel, String message) { if ("email".equals(channel)) { System.out.println("Sending email: " + message); } else if ("slack".equals(channel)) { System.out.println("Posting to Slack: " + message); } else if ("teams".equals(channel)) { System.out.println("Sending Teams message: " + message); } // ❌ Must modify this class to add new channels! }}Following OCP (Good Approach)
Section titled “Following OCP (Good Approach)”class Notification: def send(self, message: str): """Base method - closed for modification""" pass
class EmailNotification(Notification): def __init__(self, recipient: str): self.recipient = recipient
def send(self, message: str): print(f"Sending email to {self.recipient}: {message}")
class SlackNotification(Notification): def __init__(self, channel: str): self.channel = channel
def send(self, message: str): print(f"Posting to Slack #{self.channel}: {message}")// Base class - closed for modificationpublic abstract class Notification { public abstract void send(String message);}
public class EmailNotification extends Notification { private String recipient;
public EmailNotification(String recipient) { this.recipient = recipient; }
@Override public void send(String message) { System.out.println("Sending email to " + recipient + ": " + message); }}
public class SlackNotification extends Notification { private String channel;
public SlackNotification(String channel) { this.channel = channel; }
@Override public void send(String message) { System.out.println("Posting to Slack #" + channel + ": " + message); }}Adding New Functionality
Section titled “Adding New Functionality”If you need to add a new notification channel (like Teams or SMS), you can create a new subclass without touching the existing code:
class TeamsNotification(Notification): def __init__(self, team: str): self.team = team
def send(self, message: str): print(f"Sending Teams message to {self.team}: {message}")
# Usageteams_notification = TeamsNotification("Engineering")teams_notification.send("Deployment successful!")public class TeamsNotification extends Notification { private String team;
public TeamsNotification(String team) { this.team = team; }
@Override public void send(String message) { System.out.println("Sending Teams message to " + team + ": " + message); }}
// Usagepublic class Main { public static void main(String[] args) { TeamsNotification teamsNotification = new TeamsNotification("Engineering"); teamsNotification.send("Deployment successful!"); }}classDiagram
class Notification {
<<abstract>>
+send(message: str)
}
class EmailNotification {
-recipient: str
+send(message: str)
}
class SlackNotification {
-channel: str
+send(message: str)
}
class TeamsNotification {
-team: str
+send(message: str)
}
Notification <|-- EmailNotification
Notification <|-- SlackNotification
Notification <|-- TeamsNotification
Benefits of Following OCP
Section titled “Benefits of Following OCP”Handling Scenarios Where Modification Might Be Necessary
Section titled “Handling Scenarios Where Modification Might Be Necessary”While OCP encourages extension over modification, there are scenarios where modification is unavoidable or where alternative approaches are needed:
1. Third-Party Classes (Using Adapter Pattern)
Section titled “1. Third-Party Classes (Using Adapter Pattern)”When working with external libraries or sealed classes that cannot be extended, use composition and adapters instead.
Problem: You’re using a third-party notification library that you cannot modify:
# This is from an external library - you cannot modify itclass ThirdPartyEmailService: def send_email(self, to: str, subject: str, body: str): print(f"Third-party service sending email to {to}") # Actual implementation...// This is from an external library - you cannot modify itpublic class ThirdPartyEmailService { public void sendEmail(String to, String subject, String body) { System.out.println("Third-party service sending email to " + to); // Actual implementation... }}Solution: Create an adapter/wrapper class that implements your interface:
# Your own interfaceclass Notification: def send(self, message: str): pass
# Adapter that wraps the third-party classclass ThirdPartyEmailAdapter(Notification): def __init__(self, email_service: ThirdPartyEmailService, recipient: str): self.email_service = email_service # Composition self.recipient = recipient
def send(self, message: str): # Adapt the third-party interface to your interface self.email_service.send_email( to=self.recipient, subject="Notification", body=message )
# Usageemail_service = ThirdPartyEmailService()notification = ThirdPartyEmailAdapter(email_service, "dev@company.com")notification.send("Build completed!")// Your own interfacepublic abstract class Notification { public abstract void send(String message);}
// Adapter that wraps the third-party classpublic class ThirdPartyEmailAdapter extends Notification { private ThirdPartyEmailService emailService; // Composition private String recipient;
public ThirdPartyEmailAdapter(ThirdPartyEmailService emailService, String recipient) { this.emailService = emailService; this.recipient = recipient; }
@Override public void send(String message) { // Adapt the third-party interface to your interface emailService.sendEmail(recipient, "Notification", message); }}
// Usagepublic class Main { public static void main(String[] args) { ThirdPartyEmailService emailService = new ThirdPartyEmailService(); Notification notification = new ThirdPartyEmailAdapter(emailService, "dev@company.com"); notification.send("Build completed!"); }}This way, you extend functionality (add new notification types) without modifying the third-party code, following OCP through composition.
classDiagram
class Notification {
<<interface>>
+send(message: str)
}
class ThirdPartyEmailService {
+send_email(to, subject, body)
}
class ThirdPartyEmailAdapter {
-email_service: ThirdPartyEmailService
-recipient: str
+send(message: str)
}
Notification <|.. ThirdPartyEmailAdapter
ThirdPartyEmailAdapter *-- ThirdPartyEmailService : uses
2. Cross-Cutting Concerns
Section titled “2. Cross-Cutting Concerns”When you need to add functionality that doesn’t belong to the same domain (like logging, caching, or security), consider using decorators or aspect-oriented programming instead of modifying the base class.
Example: Adding audit logging without modifying the base class:
class Notification: def send(self, message: str): """Base method - closed for modification""" pass
class AuditLogger: """Decorator for cross-cutting concern""" def __init__(self, notification: Notification): self.notification = notification
def send(self, message: str): print(f"[AUDIT] Sending notification: {message}") self.notification.send(message)
# Usageemail_notification = EmailNotification("dev@company.com")audited_notification = AuditLogger(email_notification)audited_notification.send("Build completed!")public abstract class Notification { public abstract void send(String message);}
// Decorator for cross-cutting concernpublic class AuditLogger extends Notification { private Notification notification;
public AuditLogger(Notification notification) { this.notification = notification; }
@Override public void send(String message) { System.out.println("[AUDIT] Sending notification: " + message); notification.send(message); }}
// Usagepublic class Main { public static void main(String[] args) { Notification emailNotification = new EmailNotification("dev@company.com"); Notification auditedNotification = new AuditLogger(emailNotification); auditedNotification.send("Build completed!"); }}3. Fundamental Behavior Changes
Section titled “3. Fundamental Behavior Changes”If the core behavior needs to change (not just extend), modification might be necessary. However, consider if this indicates a design issue that should be refactored. Sometimes, it’s better to create a new abstraction rather than modifying the existing one.