Open/Closed Principle (OCP)

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” This means you can add new functionality without changing existing code.

Purpose

  • Adapts to new requirements with minimal risk: new features can be added by extending existing code (e.g. via inheritance or composition) rather than altering working code.
  • Prevents regressions, since existing classes remain untouched when extending behavior.
  • Encourages flexible designs using polymorphism, interfaces, or abstract classes, so that core logic remains stable over time.

Minimal Example

The first function below violates OCP: adding a new shape (e.g., Triangle) would require modifying the function to handle it. The improved design uses polymorphism – an abstract Shape base class with a defined area() method. New shapes can be extended by creating a subclass and overriding area(), with no changes needed to existing code.

# Without OCP: adding new shapes requires modifying this function (not extensible)
def get_area(shape):
    if isinstance(shape, Circle):
        return 3.14 * shape.radius ** 2
    elif isinstance(shape, Rectangle):
        return shape.width * shape.height
    # If a new shape type is added, we would have to modify this function.

# With OCP: define a common interface for shapes
class Shape:
    def area(self):
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return 3.14 * (self.radius ** 2)

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def area(self):
        return self.width * self.height

# Now, adding a new shape (e.g., Triangle) means creating a new subclass of Shape
# without changing any existing code or functions that use Shape.area().

More Realistic Example

Imagine a notification system that sends alerts via multiple channels (email, SMS, etc.). Using OCP, we define a base NotificationSender class and extend it for each channel. The client code can treat all senders uniformly through the base interface. Adding a new channel (say, a push notification) is as simple as creating a new subclass, with no modifications to the existing sending logic.

class NotificationSender:
    def send(self, message):
        raise NotImplementedError

class EmailSender(NotificationSender):
    def __init__(self, email_address):
        self.email = email_address
    def send(self, message):
        print(f"Email to {self.email}: {message}")

class SMSSender(NotificationSender):
    def __init__(self, phone_number):
        self.phone = phone_number
    def send(self, message):
        print(f"SMS to {self.phone}: {message}")

# Client code works with the abstract NotificationSender type:
notifiers = [EmailSender("team@example.com"), SMSSender("555-1234")]
for sender in notifiers:
    sender.send("Server is down!")

# Output:
# Email to team@example.com: Server is down!
# SMS to 555-1234: Server is down!

# If we need to add a PushNotifier later, we can create a PushNotifier class
# extending NotificationSender without changing the loop or other existing code.