Understanding SOLID principles for beginners is not quite easy. My experience with learning about SOLID principles was finding an elementary tutorial that a beginner could understand wasn't easy. In this post, I use simple python codes to explain (with code) what SOLID principles are.

What are SOLID principles?

SOLID principles are a set of 5 Object-Oriented Programming design principles that were advocated for by Uncle Bob (Robert Fowler) by the year 2000. SOLID is an acronym that stands for:

  • Single Responsibility Principle (S)
  • Open Closed Principle (O)
  • Liskov Substitution Principle (L)
  • Interface Segregation Principle (I)
  • Dependency Inversion Principle (D)

Single Responsibility Principle (SRP).

A class should have one and only one reason to change, meaning that a class should have only one job.
Class User:
        
    def __init__(self, details: dict):
	    self.details = details
        
    def get_details(self):
   	    pass
        
    def save(self):
        # saves to database
        pass

In the above code snippet, the user class not only gets the user but also saves him in a database. The class handles both setting user data requirements and database connection and saving. We ought to have a User class that handles the user data and a database class that handles the DB connection and transactions.

Class User:    

    def __init__(self, details: dict):
        self.details = details
        
    def get_details(self):
        pass
        
Class Db:
    def save(self):
        pass

As refactored above, we have two (2) classes, each with one (1) responsibility. If a class has only one responsibility, they tend to be more reusable , simpler, and propagate fewer changes to the system.

Open Closed Principle(OCP)

Software entities(classes, modules, functions) should be open for extension, not modification.
Class SalaryRaise:    
	
    def __init__(self, employee_class, salary):
    	self.employee_class = employee_class
        self.salary = salary
        
    def give_raise(self):
    	if self.employee_class = 'A':
            return self.salary * 0.02        #2 percent salary raise

If we wanted to add another employee of employee class B with a four (4) percent increase in the Salary Raise program, we would have to modify the give_raise method. This violates the open closed principle. Hence the above code can be modified to be.

Class SalaryRaise:
	
    def __init__(self, employee_class, salary):       
    	self.employee_class = employee_class    
    
    def get_salary(self):        
    	return self.salary       
    
    def employee_A(self):        
    	return self.get_salary * 0.02    
        
    # To add another employee class we just   
    # extend the class by adding a function    
    def employee_B(self):
    	return self.get_salary * 0.04
        

Liskov Substitution Principle(LSP)

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

Simply put, this states that objects of the derived class (or super class) can be used in place of objects of their super-class (parent class) without breaking. This means that the objects should behave in the same way.

For instance, they should have the same value return type, should have the same number of arguments, raise the same exceptions etc.

class Shape:    

    def area(self, l, w):
        return l * w    
        
class Square(Shape):    
    def area(self, s):
        return s * s

In the above code snippet, the class Square violates the Liskov Principle as its area method cannot be substituted by its Parent class (Shape) method. This is because of the number of arguments being taken in.

Another example is shown below

class Calculator:

    def calculate(self, a, b):
        return a * b

class Divide(Calculator):    
        
    def calculate(self, a, b):        
    	return a / b

Notice that in the above two (2) classes, even though the calculate methods take equal no of arguments, the method in the Divide class could possibly throw a divide by zero error which isn't a possibility in its parent class (Calculator class) hence a violation of SOLID principles.

Interface Segregation Principle(ISP)

A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.

Given an interface IMotion that defines various motion types

class IMotion:
    
    def drive(self):
        pass
        
    def cycle(self):
        pass
        
    def fly(self):
        pass
        
    

To implement this interface in multiple classes, say Bicycle, Car and Aeroplane, we would have

class Bicycle(IMotion):
    
    def drive(self):
        pass
        
    def cycle(self):
        pass
        
    def fly(self):
        pass
        
class Car(IMotion):
    
    def drive(self):
        pass
        
    def cycle(self):
        pass
        
    def fly(self):
        pass
        
class Aeroplane(IMotion):
    
    def drive(self):
        pass
        
    def cycle(self):
        pass
        
    def fly(self):
        pass
        
    
    

As you can see, the classes Car, Bicycle and Aeroplane are forced to have methods that they don't need. This violates the ISP. In case, for instance, we wanted to add a sail motion to the interface, all the classes implementing this interface would need to add it too otherwise, an exception would be thrown. The above code can be refactored to be:

class IMotion:
    
    def motion(self):
        pass
        
class Aeroplane(IMotion):
    
    def motion(self):
        pass
        
class Car(IMotion):
    
    def motion(self):
        pass
        
class Bicycle(IMotion):
    
    def motion(self):
        pass

In case we now need to add a sail motion, all we need to add is a new class called ship that implements the IMotion interface rather than altering all other classes.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. They should depend on abstractions and abstractions should not depend on details, rather details should depend on abstractions.

This means that any changes on lower level modules shouldn't affect higher level modules providing complex logic. This can be achieved by adding abstractions. This means, therefore that basically, you end up with two dependencies: higher level modules depending on the abstraction layer and lower level modules depending on the abstraction layer.

Assume we want to create various Http Service types. Like XHTML and Mock request.

class Xhtml:

    def request(self, url, method):
        pass
        
class Http:
    
    def __init__(self, Xhtml: Xhtml):
        self.xhtml = Xhtml
        
    def post(self, url):
        self.xhtml.request(url, 'POST')
        
    def get(self, url):
        self.xhtml.request(url, 'GET')
            
        

This will always work just for Xhtml requests, but it violates DIP. Http being the higher level module is dependent on Xhtml, which is the lower level. Hence, if we need to have to do Mock requests, it would necessitate a lot of changes in the higher level Http. Let's refactor this by introducing an abstraction layer

class Connector:
    #abstraction layer
    
    def request(self, url, method):
        pass
        
class Xhtml(Connector):

    def request(self, url, method):
        pass
        
#new class added
class Mock(Connector):
    
    def request(self, url, method):
        pass
        
# high level module
class Http:

    def __init__(self, conn: Connector):
        self.conn = conn
        
    def post(self, url):
        self.conn.request(url, 'POST')
        
    def get(self, url):
        self.conn.request(url, 'GET')

By adding the Connector interface, we can now ensure that both high and low level modules only depend on the abstraction introduced. If you are keen enough, you'll notice that DIP is some sort of a combination of Open/Closed and Liskov

That is it. You can read and re-read to understand the principles better.

Good Luck and Happy Coding :-)

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.