Open-Closed Principle (OCP)

July 29, 2024

Software entities (such as classes, methods, functions, etc.) should be open for extension but closed for modification.

This means that we should design our software in a way that allows us to add new functionality or behavior without changing the existing code. We can achieve this using composition, inheritance, and interfaces by abstracting the structure of classes (superclass) and subclasses.

Let's take an example with the PaymentHandler class:

class Checkout:
    def __init__(self):
        self.products = []
        self.quantity = []
        self.price = []
        self.status = "pending"

    ...

class PaymentHandler:
    def pay(self, checkout: Checkout, payment_type):
        if payment_type == "card":
            print("Processing card payment...")
            checkout.status = "paid"
        elif payment_type == "paypal":
            print("Processing PayPal payment...")
            checkout.status = "paid"
        else:
            print(f"Payment: {payment_type} is not supported")
            checkout.status = "failed"

checkout = Checkout()
checkout.add_to_basket("Pierogi", 1, 4)
checkout.add_to_basket("Pizza", 2, 6)
checkout.add_to_basket("Pineapple", 1, 3)

processor = PaymentHandler()
processor.pay(checkout, "paypal")

We're going to apply the Open-Closed Principle to the PaymentHandler class, because if we wanted to add another payment method (say Crypto), we would need to modify the code, which violates OCP.

The solution here, is to create subclasses (child classes) and abstract out the superclass (the parent) with the use of the abc module:

from abc import ABC, abstractmethod

class Checkout:
    def __init__(self):
        self.products = []
        self.quantity = []
        self.price = []
        self.status = "pending"
    ...

class PaymentHandler(ABC):
    @abstractmethod
    def pay(self, checkout: Checkout, security_token: str):
        pass

class CardPayment(PaymentHandler):
    def pay(self, checkout: Checkout, security_token: str):
        print("Processing card payment...")
        checkout.status = "paid"

class PayPalPayment(PaymentHandler):
    def pay(self, checkout: Checkout, security_token: str):
        print("Processing PayPal payment...")
        checkout.status = "paid"

checkout = Checkout()
checkout.add_to_basket("Pierogi", 1, 4)
checkout.add_to_basket("Pizza", 2, 6)
checkout.add_to_basket("Pineapple", 1, 3)

processor = PayPalPayment()

processor.pay(checkout, "900913")

Now, we have a PaymentHandler superclass with an abstract pay method, and two subclasses CardPayment and PayPalPayment that inherit the pay method - nice and clean! This allows to add more PaymentHandler types without modifying the existing code.