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 thePaymentHandler
to be able to update theself.status
.