Single Responsibility Principle (SRP)

July 28, 2024

A class, method or module should have one responsibility or job to do, and any change to that responsibility should only require changes to that class/method.

"A class should have only one reason to change"* - Uncle Bob

This also means that we're creating a high-cohesion responsibility ensuring that the function does one thing only, and can be reused easily.

Classes should have a small number of instance variables. Each of the methods of a class should manipulate one or more of those variables. In general the more variables a method manipulates the more cohesive that method is to its class. A class in which each variable is used by each method is maximally cohesive. In general it is neither advisable nor possible to create such maximally cohesive classes; on the other hand, we would like cohesion to be high. When cohesion is high, it means that the methods and variables of the class are co-dependent and hang together as a logical whole.

Let's refactor the Checkout class and apply Single Responsibility Model:

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

    def add_to_basket(self, id, quantity, price):
        self.products.append(id)
        self.quantity.append(quantity)
        self.price.append(price)

    def total_basket(self):
        total = 0
        for i in range(len(self.price)):
            total += self.quantity[i] * self.price[i]
        return total

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

checkout = Checkout()
checkout.payment("paypal", "Damian")

Looking at the code above, we have a Checkout class with add_to_basket method to create our product list, total_price method to calculate the total price of each added product, and payment method that is responsible for processing payment types.

We're looking at refactoring the PaymentHandler as it shouldn't really belong to the Checkout class. The trick here is to remove the if-else condition and create new class to handle payments giving it a Single Responsibility.

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

    def add_to_basket(self, name, quantity, price):
        self.products.append(name)
        self.quantity.append(quantity)
        self.price.append(price)

    def total_price(self):
        total = 0
        for i in range(len(self.price)):
            total += self.quantity[i] * self.price[i]
        return total

class PaymentHandler:
    def pay(self, checkout: Checkout):
        print("Processing 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 = PaymentHandler()
processor.pay(checkout)

Now we have de-coupled payment functionality from the Checkout class.

Note: how we're passing a Checkout variable class into the PaymentHandler to be able to update the self.status.