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.