Skip to content

Open Closed Principle

Design software entities to be open for extension but closed for modification.

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.

Diagram

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

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.

Diagram
bad_notifications.py
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!
Diagram
notifications.py
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}")

If you need to add a new notification channel (like Teams or SMS), you can create a new subclass without touching the existing code:

notifications.py
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}")
# Usage
teams_notification = TeamsNotification("Engineering")
teams_notification.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

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:

third_party_lib.py
# This is from an external library - you cannot modify it
class ThirdPartyEmailService:
def send_email(self, to: str, subject: str, body: str):
print(f"Third-party service sending email to {to}")
# Actual implementation...

Solution: Create an adapter/wrapper class that implements your interface:

notifications.py
# Your own interface
class Notification:
def send(self, message: str):
pass
# Adapter that wraps the third-party class
class 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
)
# Usage
email_service = ThirdPartyEmailService()
notification = ThirdPartyEmailAdapter(email_service, "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

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:

notifications.py
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)
# Usage
email_notification = EmailNotification("dev@company.com")
audited_notification = AuditLogger(email_notification)
audited_notification.send("Build completed!")

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.