Armstrong Nigere
The Data Plumber Blogs

The Data Plumber Blogs

The Solid Principals

Armstrong Nigere's photo
Armstrong Nigere
·Apr 9, 2022·

7 min read

Table of contents

  • Introduction
  • Single-Responsibility Principle
  • Open-closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
  • Conclusion
  • Follow Me On
  • Sources

Introduction

In Software engineering , SOLID is a mnemonic,acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. The principles are a subset of many principles promoted by American software engineer and instructor Robert C. Martin, first introduced in his 2000 paper Design Principles and Design Patterns.

Note: While these principles can apply to various programming languages, the sample code contained in this article will use Python.

SOLID stands for:

S - Single-responsiblity Principle
O - Open-closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle

lets analysis each principle more in detail with examples

Single-Responsibility Principle

The Single Responsibility Principle states that a class should do one thing and therefore it should have only a single reason to change.

In simple word each class and module in a program should focus on a single task .
One rule of thumb that you can use to know when your break the SRP is when a description of your class/module has the word AND in it than you might need to refactor your class/module .

Let’s take into example this python Order class by giving a description: The Order class allows you to add items to the order, calculate the total of the order AND manage payments.

class Order:
    items = []
    quantities = []
    prices = []
    status = "open"

    def add_item(self, name, quantity, price):
        self.items.append(name)
        self.quantities.append(quantity)
        self.prices.append(price)

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

    def paynment(self, payment_type, security_code):
        if payment_type == "debit":
            print("Processing debit payment type")
            print(f"Verifiying security code : {security_code}")
            self.status = "paid"

        elif payment_type == "credtit":
            print("Processing credit  payment type")
            print(f"Verifiying security code : {security_code}")
            self.status = "paid"
        elif  payment_type == "bitcoin":
            print("Processing bitcoin   payment type")
            print(f"Verifiying security code : {security_code}")
            self.status = "paid"

        else:
            raise Exception(f"Unknown payment type : {security_code}")

The following class has the responsibility of managing payments this breaks the SRP rule. So we need to refactor the class , we shall do this by creating a new class called PaymentProcessor which will manage our payments .

after some refactoring our Order class has a single Responsibility and so does our new PaymentProcessor class.

Order

class Order:
    items = []
    quantities = []
    prices = []
    status = "open"

    def add_item(self, name, quantity, price):
        self.items.append(name)
        self.quantities.append(quantity)
        self.prices.append(price)

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

PaymentProcessor

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

Open-closed Principle

The Open-Closed Principle requires that classes should be open for extension and closed to modification.

Modification means changing the code of an existing class, and extension means adding new functionality.

So what this principle wants to say is: We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs. So we should avoid touching the tested and reliable code if possible.

We can add new functionality without touching the class by using interfaces and abstract classes.

To understand the open /closed principal we are going to consider our payment class , lets say we wanted to add a new payment method bitcoin, we would add a new if clause and thus require to test the code again to see if we haven’t broken any functionality.

class Payment:
    def payment(self, payment_type, security_code):
        if payment_type == "debit":
            print("Processing debit payment type")
            print(f"Verifiying security code : {security_code}")
            self.status = "paid"

        elif payment_type == "credtit":
            print("Processing credit  payment type")
            print(f"Verifiying security code : {security_code}")
            self.status = "paid"
        elif payment_type == "bitcoin":
            print("Processing bitcoin   payment type")
            print(f"Verifiying security code : {security_code}")
            self.status = "paid"

        else:
            raise Exception(f"Unknown payment type : {security_code}")

let's refactor the code above by trying to apply the Open-Closed Principle to our payments class: we start by creating an abstract/interface class that way we can extend the functionality of the payment class to have different payment types.

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

we have extended the functionality of our payment class to manager debit payment that way we have created code that is easy to read and maintain and we have applied the open-closed principal.

@dataclass
class DebitPaymentProcessor(PaymentProcessor):
    security_code: str
    authorizer: Authorizer

    def pay(self, order):
        if not self.authorizer.is_authorized():
            raise Exception("Not authorized")
        print("Processing debit payment type")
        print(f"Verifiying security code : {self.security_code}")
        order.status = "paid"

In the case if our boss asks to add a new functionality of bitcoin payments we would simple add a class called BitCoinPaymentProcessor which implements the pay method from PaymentProcessor class like the credit class did.

#  adding a new  payment method that use bitcoin  ( open closed principal
@dataclass
class BitCoinPaymentProcessor(PaymentProcessor):
    email_address: str
    authorizer: Authorizer

    def pay(self, order):
        if not self.authorizer.is_authorized():
            raise Exception("Not authorized")
        print("Processing bitcoin  payment type")
        print(f"Verifiying email code : {self.authorizer}")
        order.status = "paid"

Liskov Substitution Principle

The Liskov Substitution Principle states that subclasses should be substitutable for their base classes.

This means that the ability to replace any instance of a parent class with an instance of one of its child classes without negative side effects.

Interface Segregation Principle

Segregation means keeping things separated, and the Interface Segregation Principle is about separating the interfaces.

Code should not be forced to depend on methods that it doesn’t use. Its best to have several interfaces than one generic interface that does all.

For example lets take it consideration we wanted authorization before applying a payment , it seems much easier to simple add a method is_authorized in the PaymentProcesser but this is incorrect as it violets the interface segregation Principal and the SRP.

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

    @abstractmethod
    def is_authorized(self) -> bool:
        pass

instead what we could do is create a new interface called PaymentProcessor_SMS that manages authorization which is a subclass of PaymentProcessor , so that classes that require two way authentication through sms would use the PaymentProcessor_SMS instead of PaymentProcessor class

class PaymentProcessorSMS(PaymentProcessor):
    @abstractmethod
    def auth_sms(self, code):
        pass  # better to use composition  than inheritance make  a class auth to manage this
@dataclass
class CreditPaymentProcessor(PaymentProcessorSMS):

    def __init__(self, security_code, authorizer: Authorizer):
        self.security_code = security_code
        self.authorizer = authorizer

    def pay(self, order):
        if not self.authorizer.is_authorized():
            raise Exception("Not authorized")
        print("Processing credit  payment type")
        print(f"Verifiying security code : {self.security_code}")
        order.status = "paid"

    def auth_sms(self, code):
        pass

Dependency Inversion Principle

The Dependency Inversion principle states that our classes should depend upon interfaces or abstract classes instead of concrete classes and functions. We want our classes to be open to extension, so we have reorganized our dependencies to depend on interfaces instead of concrete classes.

Example let’s say we had a concrete class called SMSAuth instead of passing the concrete class we would pass the interface or abstract class.

First we would create an abstract/interface class called Authorizer and SMSAuth would be a sub class of it.

class Authorizer(ABC):
    @abstractmethod
    def is_authorized(self) -> bool:
        pass
@dataclass
class SMSAuth(Authorizer):
    authorized: False

    def verify_code(self, code):
        print(f"Verifiying security code : {code}")
        self.authorized = True

    def is_authorized(self) -> bool:
        return self.authorized

That way in the DebitPaymentProcessor class we can pass the abstract class instead of the concret class.

@dataclass
class DebitPaymentProcessor(PaymentProcessor):

    def __init__(self, security_code, authorizer: Authorizer):
        self.security_code = security_code
        self.authorizer = authorizer

Conclusion

In this article, we took a look at the SOLID principles, and tried to understand what each principal means and how it can be applied to an application. This principals can be applied to any programming language that is object oriented. I suggest keeping these principles in mind while designing, writing, and refactoring your code so that your code will be much more clean, extendable, and testable. I thank you for taking the time to read this article and If you enjoyed reading this and you would like to follow the next series of blogs or if you are just curious about Data Engineering, you can follow me on the links provided below for future articles.

For me writing blogs is just a way of documenting my work, this blog was inspired by ArjanCodes video on the topic which I provided a link below above. you can follow me on the links provided below for future articles.

Follow Me On

twitter

The Data Plumber Blog

Sources

ArjanCodes video on the topic

wikipedia SOLID

 
Share this