Open Closed Principle
Open Closed Principle
Section titled “Open Closed Principle”The Open Closed Principle (also known as OCP or OCP principle) is one of the five SOLID principles of object-oriented design. The Open Closed Principle (sometimes written as open-closed principle or open/closed principle) 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 Open Closed Principle is essential for building maintainable, scalable software systems.
Open Closed Principle Example
Section titled “Open Closed Principle Example”Let’s look at a practical Open Closed Principle example to understand how it works in real-world scenarios.
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! }}class NotificationService { sendNotification(channel: string, message: string): void { if (channel === "email") { console.log(`Sending email: ${message}`); } else if (channel === "slack") { console.log(`Posting to Slack: ${message}`); } else if (channel === "teams") { console.log(`Sending Teams message: ${message}`); } // ❌ Must modify this class to add new channels! }}#include <string>#include <iostream>
class NotificationService {public: void sendNotification(const std::string& channel, const std::string& message) { if (channel == "email") { std::cout << "Sending email: " << message << std::endl; } else if (channel == "slack") { std::cout << "Posting to Slack: " << message << std::endl; } else if (channel == "teams") { std::cout << "Sending Teams message: " << message << std::endl; } // ❌ Must modify this class to add new channels! }};using System;
public class NotificationService{ public void SendNotification(string channel, string message) { if (channel == "email") { Console.WriteLine($"Sending email: {message}"); } else if (channel == "slack") { Console.WriteLine($"Posting to Slack: {message}"); } else if (channel == "teams") { Console.WriteLine($"Sending Teams message: {message}"); } // ❌ Must modify this class to add new channels! }}package main
import "fmt"
type NotificationService struct{}
func (s *NotificationService) SendNotification(channel, message string) { if channel == "email" { fmt.Printf("Sending email: %s\n", message) } else if channel == "slack" { fmt.Printf("Posting to Slack: %s\n", message) } else if channel == "teams" { fmt.Printf("Sending Teams message: %s\n", message) } // ❌ Must modify this struct to add new channels!}enum NotificationType { Email, Sms,}struct NotificationService;impl NotificationService { fn send(&self, kind: NotificationType, message: &str) { match kind { NotificationType::Email => println!("Email: {message}"), NotificationType::Sms => println!("SMS: {message}"), } }}// Adding a channel requires editing this match.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); }}// Base class - closed for modificationabstract class Notification { abstract send(message: string): void;}
class EmailNotification extends Notification { constructor(private recipient: string) { super(); }
send(message: string): void { console.log(`Sending email to ${this.recipient}: ${message}`); }}
class SlackNotification extends Notification { constructor(private channel: string) { super(); }
send(message: string): void { console.log(`Posting to Slack #${this.channel}: ${message}`); }}#include <string>#include <iostream>
// Base class - closed for modificationclass Notification {public: virtual ~Notification() = default; virtual void send(const std::string& message) = 0;};
class EmailNotification : public Notification {private: std::string recipient;
public: EmailNotification(const std::string& recipient) : recipient(recipient) {}
void send(const std::string& message) override { std::cout << "Sending email to " << recipient << ": " << message << std::endl; }};
class SlackNotification : public Notification {private: std::string channel;
public: SlackNotification(const std::string& channel) : channel(channel) {}
void send(const std::string& message) override { std::cout << "Posting to Slack #" << channel << ": " << message << std::endl; }};using System;
// Base class - closed for modificationpublic abstract class Notification{ public abstract void Send(string message);}
public class EmailNotification : Notification{ private string recipient;
public EmailNotification(string recipient) { this.recipient = recipient; }
public override void Send(string message) { Console.WriteLine($"Sending email to {recipient}: {message}"); }}
public class SlackNotification : Notification{ private string channel;
public SlackNotification(string channel) { this.channel = channel; }
public override void Send(string message) { Console.WriteLine($"Posting to Slack #{channel}: {message}"); }}package main
import "fmt"
// Base abstraction - closed for modificationtype Notification interface { Send(message string)}
type EmailNotification struct{ recipient string }
func NewEmailNotification(recipient string) *EmailNotification { return &EmailNotification{recipient: recipient}}
func (n *EmailNotification) Send(message string) { fmt.Printf("Sending email to %s: %s\n", n.recipient, message)}
type SlackNotification struct{ channel string }
func NewSlackNotification(channel string) *SlackNotification { return &SlackNotification{channel: channel}}
func (n *SlackNotification) Send(message string) { fmt.Printf("Posting to Slack #%s: %s\n", n.channel, message)}trait Notification { fn send(&self, message: &str);}struct EmailNotification;struct SmsNotification;impl Notification for EmailNotification { fn send(&self, message: &str) { println!("Email: {message}"); }}impl Notification for SmsNotification { fn send(&self, message: &str) { println!("SMS: {message}"); }}struct NotificationService;impl NotificationService { fn send(&self, n: &dyn Notification, message: &str) { n.send(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!"); }}class TeamsNotification extends Notification { constructor(private team: string) { super(); }
send(message: string): void { console.log(`Sending Teams message to ${this.team}: ${message}`); }}
// Usageconst teamsNotification = new TeamsNotification("Engineering");teamsNotification.send("Deployment successful!");#include <string>#include <iostream>
class TeamsNotification : public Notification {private: std::string team;
public: TeamsNotification(const std::string& team) : team(team) {}
void send(const std::string& message) override { std::cout << "Sending Teams message to " << team << ": " << message << std::endl; }};
// Usageint main() { TeamsNotification teamsNotification("Engineering"); teamsNotification.send("Deployment successful!"); return 0;}using System;
public class TeamsNotification : Notification{ private string team;
public TeamsNotification(string team) { this.team = team; }
public override void Send(string message) { Console.WriteLine($"Sending Teams message to {team}: {message}"); }}
// Usageclass Program{ static void Main() { TeamsNotification teamsNotification = new TeamsNotification("Engineering"); teamsNotification.Send("Deployment successful!"); }}package main
import "fmt"
type TeamsNotification struct{ team string }
func NewTeamsNotification(team string) *TeamsNotification { return &TeamsNotification{team: team}}
func (n *TeamsNotification) Send(message string) { fmt.Printf("Sending Teams message to %s: %s\n", n.team, message)}
func main() { teams := NewTeamsNotification("Engineering") teams.Send("Deployment successful!")}trait Notification { fn send(&self, message: &str);}struct TeamsNotification;impl Notification for TeamsNotification { fn send(&self, message: &str) { println!("Teams: {message}"); }}fn notify(notification: &dyn Notification) { notification.send("Deployment finished");}// New functionality is an extension.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... }}// This is from an external library - you cannot modify itclass ThirdPartyEmailService { sendEmail(to: string, subject: string, body: string): void { console.log(`Third-party service sending email to ${to}`); // Actual implementation... }}#include <string>#include <iostream>
// This is from an external library - you cannot modify itclass ThirdPartyEmailService {public: void sendEmail(const std::string& to, const std::string& subject, const std::string& body) { std::cout << "Third-party service sending email to " << to << std::endl; // Actual implementation... }};using System;
// This is from an external library - you cannot modify itpublic class ThirdPartyEmailService{ public void SendEmail(string to, string subject, string body) { Console.WriteLine($"Third-party service sending email to {to}"); // Actual implementation... }}package main
import "fmt"
// This is from an external library - you cannot modify ittype ThirdPartyEmailService struct{}
func (s *ThirdPartyEmailService) SendEmail(to, subject, body string) { fmt.Printf("Third-party service sending email to %s\n", to) // Actual implementation...}struct ThirdPartyEmailService;impl ThirdPartyEmailService { fn send_email(&self, to: &str, subject: &str, body: &str) { println!("Third-party email to {to}: {subject} - {body}"); }}// External API does not match our Notification abstraction.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.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.send("Build completed!"); }}// Your own interfaceabstract class Notification { abstract send(message: string): void;}
// Adapter that wraps the third-party classclass ThirdPartyEmailAdapter extends Notification { constructor( private emailService: ThirdPartyEmailService, // Composition private recipient: string ) { super(); }
send(message: string): void { // Adapt the third-party interface to your interface this.emailService.sendEmail(this.recipient, "Notification", message); }}
// Usageconst emailService = new ThirdPartyEmailService();notification.send("Build completed!");#include <string>#include <iostream>#include <memory>
// Your own interfaceclass Notification {public: virtual ~Notification() = default; virtual void send(const std::string& message) = 0;};
// Adapter that wraps the third-party classclass ThirdPartyEmailAdapter : public Notification {private: std::shared_ptr<ThirdPartyEmailService> emailService; // Composition std::string recipient;
public: ThirdPartyEmailAdapter(std::shared_ptr<ThirdPartyEmailService> emailService, const std::string& recipient) : emailService(emailService), recipient(recipient) {}
void send(const std::string& message) override { // Adapt the third-party interface to your interface emailService->sendEmail(recipient, "Notification", message); }};
// Usageint main() { auto emailService = std::make_shared<ThirdPartyEmailService>(); auto notification = std::make_shared<ThirdPartyEmailAdapter>( notification->send("Build completed!"); return 0;}using System;
// Your own interfacepublic abstract class Notification{ public abstract void Send(string message);}
// Adapter that wraps the third-party classpublic class ThirdPartyEmailAdapter : Notification{ private ThirdPartyEmailService emailService; // Composition private string recipient;
public ThirdPartyEmailAdapter(ThirdPartyEmailService emailService, string recipient) { this.emailService = emailService; this.recipient = recipient; }
public override void Send(string message) { // Adapt the third-party interface to your interface emailService.SendEmail(recipient, "Notification", message); }}
// Usageclass Program{ static void Main() { ThirdPartyEmailService emailService = new ThirdPartyEmailService(); notification.Send("Build completed!"); }}package main
import "fmt"
// Your own interfacetype Notification interface { Send(message string)}
// Adapter that wraps the third-party structtype ThirdPartyEmailAdapter struct { emailService *ThirdPartyEmailService recipient string}
func NewThirdPartyEmailAdapter(svc *ThirdPartyEmailService, recipient string) *ThirdPartyEmailAdapter { return &ThirdPartyEmailAdapter{emailService: svc, recipient: recipient}}
func (a *ThirdPartyEmailAdapter) Send(message string) { // Adapt the third-party interface to your interface a.emailService.SendEmail(a.recipient, "Notification", message)}
func main() { emailService := &ThirdPartyEmailService{} notification.Send("Build completed!")}
type ThirdPartyEmailService struct{}
func (s *ThirdPartyEmailService) SendEmail(to, subject, body string) { fmt.Printf("Third-party service sending email to %s\n", to)}trait Notification { fn send(&self, message: &str);}struct ThirdPartyEmailService;impl ThirdPartyEmailService { fn send_email(&self, to: &str, subject: &str, body: &str) { println!("Third-party email to {to}: {subject} - {body}"); }}struct ThirdPartyEmailAdapter { service: ThirdPartyEmailService, to: String,}impl Notification for ThirdPartyEmailAdapter { fn send(&self, message: &str) { self.service.send_email(&self.to, "Notification", message); }}This way, you extend functionality (add new notification types) without modifying the third-party code, following OCP through composition.
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)
# Usageaudited_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 auditedNotification = new AuditLogger(emailNotification); auditedNotification.send("Build completed!"); }}abstract class Notification { abstract send(message: string): void;}
// Decorator for cross-cutting concernclass AuditLogger extends Notification { constructor(private notification: Notification) { super(); }
send(message: string): void { console.log(`[AUDIT] Sending notification: ${message}`); this.notification.send(message); }}
// Usageconst auditedNotification = new AuditLogger(emailNotification);auditedNotification.send("Build completed!");#include <string>#include <iostream>#include <memory>
class Notification {public: virtual ~Notification() = default; virtual void send(const std::string& message) = 0;};
// Decorator for cross-cutting concernclass AuditLogger : public Notification {private: std::shared_ptr<Notification> notification;
public: AuditLogger(std::shared_ptr<Notification> notification) : notification(notification) {}
void send(const std::string& message) override { std::cout << "[AUDIT] Sending notification: " << message << std::endl; notification->send(message); }};
// Usageint main() { auto auditedNotification = std::make_shared<AuditLogger>(emailNotification); auditedNotification->send("Build completed!"); return 0;}using System;
public abstract class Notification{ public abstract void Send(string message);}
// Decorator for cross-cutting concernpublic class AuditLogger : Notification{ private Notification notification;
public AuditLogger(Notification notification) { this.notification = notification; }
public override void Send(string message) { Console.WriteLine($"[AUDIT] Sending notification: {message}"); notification.Send(message); }}
// Usageclass Program{ static void Main() { Notification auditedNotification = new AuditLogger(emailNotification); auditedNotification.Send("Build completed!"); }}package main
import "fmt"
// Decorator for cross-cutting concerntype AuditLogger struct { notification Notification}
func NewAuditLogger(n Notification) *AuditLogger { return &AuditLogger{notification: n}}
func (a *AuditLogger) Send(message string) { fmt.Printf("[AUDIT] Sending notification: %s\n", message) a.notification.Send(message)}
func main() { auditedNotification := NewAuditLogger(emailNotification) auditedNotification.Send("Build completed!")}trait Notification { fn send(&self, message: &str);}struct AuditLogger<T: Notification> { inner: T,}impl<T: Notification> Notification for AuditLogger<T> { fn send(&self, message: &str) { println!("Audit: sending notification"); self.inner.send(message); }}// Wrap behavior instead of modifying the core notifier.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.