SOLID Principles in Python¶
The SOLID principles are:
- SRP - Single-Responsibility Principle
- OCP - The Open-Closed Principle
- LSP - Liskov Substitution Principle
- ISP - Interface Segregation Principle
- DIP - Dependency inversion Principle
SOLID principles are the building blocks of the Class design
Single-responsibility principle(SRP)¶
Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
Let's see a simple example
class User:
def create_user(self, username, password, email):
print("registered user in db")
def send_email(self, subject, to_email):
print("email sent")
def log_error(self, error):
print("log:", error)
def register_user(self, username, password, email):
try:
self.create_user(username, password, email)
self.send_email("user registered", email)
except Exception:
self.log_error("error in user register")
if __name__ == '__main__':
user = User()
user.register_user("anji", "secret", "anji@example.com")
In above class, we can see the register_user
, login_user
, send_email
and log_error
total of 4 responsibilities. The class should only have the single responsibility i.e user realted. But, it also have sending email and logging an error functionalities. so, it should be separated and moved to classes like Email
and ErrorLog
. Let's re-write the code with single responsibility.
class UserDB:
def create_user(self, username, password, email):
print("registered user in db")
class Email:
def send_email(self, subject, to_email):
print("email sent")
class ErrorLog:
def log_error(self, error):
print("log:", error)
class User:
def register_user(self, username, password, email):
try:
user_db = UserDB()
user_db.create_user(username, password, email)
email_server = Email()
email_server.send_email("user registered", email)
except Exception:
logger = ErrorLog()
logger.log_error("error in user register")
if __name__ == '__main__':
user = User()
user.register_user("anji", "secret", "anji@example.com")
After, implementing the Single Responsibility Principle we can see that the code more readable and maintainable now. Let's say if want to add a new feature to update_user
which updates the user info to the database. We can simply add the method to UserDB
class. Because, it's related to database. The client class User
will have a new method called update_user_info
to use it.
Open–closed principle(OCP)¶
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Let's check out the below example.
class Calculator:
def add(self, a, b):
return a + b
def sub(self, a, b):
return a - b
def mul(self, a, b):
return a * b
def div(self, a, b):
return a / b
In the above code, we have a class Calculator
which performs the basic math operations like addition
, subtraction
, multiplication
and division
. If we want to add scientific math operations like sin
, cos
, tan
then we do not add these methods in the same class. As per open-closed principle, we do not want to modify the existsing class but we can inherit the class and add the scientific math operations to the inherited class i.e ScientificCalculator
import math
class ScientificCalculator(Calculator):
def sin(self, value):
return math.sin(value)
def cos(self, value):
return math.cos(value)
def tan(self, value):
return math.tan(value)
If we want to implement the business calculator then we can use the simpilar open-closed principle and solve it using class inheritance. Let's see the code.
class BusinessCalculator(Calculator):
def future_value(self, pv, r, n):
return pv * (1+r)**n
def monthly_payment(self, p, r, n):
return p * (r * (1 + r)**n) / (((1 + r)**n) - 1)
For ScientificCalculator
and BusinessCalculator
we did not modify the existing code but we have used it's functionality and implemented the new feature requirements. so, the code satisfies the open-closed principle.
In the future, If we get any requirement related to business then we can only modify the BusinessCalculator
. so, it won't have an effect on the other two classes.
Liskov substitution principle¶
Derived classes must be usable through the base class interface, without the need for the user to know the difference. —Barbara Liskov
If S is a subtype of T, then objects of type T may be replaced with objects of the S —Barbara Liskov
In simple words, Let's say we have a abstract class with 4 methods and 3 different classes are inheriting the abstract class. Then we should be able to access the methods abstract class without knowing the actual class that implemented the method.
Let's use the classic example of shapes.
import math
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
@abstractmethod
def calculate_perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height
def calculate_perimeter(self):
return 2 * (self.width + self.height)
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def calculate_perimeter(self):
return 4 * self.width
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self):
return math.pi * (self.radius ** 2)
def calculate_perimeter(self):
return 2 * math.pi * self.radius
def print_shape_info(shape: Shape):
data = {
"name": shape.__class__.__name__,
"area": shape.calculate_area(),
"perimeter": shape.calculate_perimeter(),
}
print(data)
if __name__ == '__main__':
shapes = [
Rectangle(10, 15),
Square(8),
Circle(12),
]
for shape in shapes:
print_shape_info(shape)
Output:
{'name': 'Rectangle', 'area': 150, 'perimeter': 50}
{'name': 'Square', 'area': 64, 'perimeter': 32}
{'name': 'Circle', 'area': 452.3893421169302, 'perimeter': 75.39822368615503}
The function print_shape_info
does not know the infomation about the implemeted class. It just know the abstract class. As per the Liskov substitution principle we can able to access the methods of Shape
in all of it's chil classes.
Interface Segregation Principle(ISP)¶
Interface segregation principle (ISP) states that no code should be forced to depend on methods it does not use.ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
Let's consider the code below.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def eat(self):
pass
@abstractmethod
def walk(self):
pass
@abstractmethod
def swim(self):
pass
@abstractmethod
def fly(self):
pass
class Cat(Animal):
def eat(self):
return True
def walk(self):
return True
def swim(self):
raise NotImplemented
def fly(self):
raise NotImplemented
class Duck(Animal):
def eat(self):
return True
def walk(self):
return True
def swim(self):
return True
def fly(self):
raise NotImplemented
class Pigeon(Animal):
def eat(self):
return True
def walk(self):
return True
def swim(self):
raise NotImplemented
def fly(self):
return True
In the above code the class Animal
is inherited by classes Cat
, Duck
, Pigeon
. But, these classes do not require all methods from the Animal
class. but, It's forcing these classes to implement the methods which are not required. It's violating the Interface Segregation Principle(ISP).
To implement fix this issue we seperate out the non-common methods to new classses SwimAbility
, FlyAbility
like below code.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def eat(self):
pass
@abstractmethod
def walk(self):
pass
class SwimAbility(ABC):
@abstractmethod
def swim(self):
pass
class FlyAbility(ABC):
@abstractmethod
def fly(self):
pass
class Cat(Animal):
def eat(self):
return True
def walk(self):
return True
class Duck(Animal, SwimAbility):
def eat(self):
return True
def walk(self):
return True
def swim(self):
return True
class Pigeon(Animal, FlyAbility):
def eat(self):
return True
def walk(self):
return True
def fly(self):
return True
The above code satisfies the principle Interface Segregation Principle(ISP). We can see that there is no forced implementation of the inherited class methods.
Dependency inversion Principle¶
The Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
Let's consider the scenario of electric switch board and electric devices. i.e Switches, Fan, Light Bulb, etc.
class LightBulb:
def turn_on(self):
print("LightBulb: on")
def turn_off(self):
print("LightBulb: off")
class PowerSwitch:
def __init__(self, light_bulb: LightBulb):
self.light_bulb = light_bulb
self.on = False
def press(self):
if self.on:
self.light_bulb.turn_off()
else:
self.light_bulb.turn_on()
self.on = True
if __name__ == '__main__':
light_bulb = LightBulb()
switch = PowerSwitch(light_bulb)
switch.press()
switch.press()
The above code works perfectly, but it violates the Dependency Inversion Principle(DIP). The class PowerSwitch
is tightly coupled with class LightBulb
. The power switch object takes in a light bulb and then directly calls the turn off and turn on method on that instance.
Let's implement the pattern Dependency Inversion Principle(DIP) for above case.
from abc import ABC, abstractmethod
class Switchable(ABC):
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
class LightBulb(Switchable):
def turn_on(self):
print("LightBulb: on")
def turn_off(self):
print("LightBulb: off")
class PowerSwitch:
def __init__(self, device: Switchable):
self.device = device
self.on = False
def press(self):
if self.on:
self.device.turn_off()
else:
self.device.turn_on()
self.on = True
if __name__ == '__main__':
light_bulb = LightBulb()
switch = PowerSwitch(light_bulb)
switch.press()
switch.press()
Now, the class PowerSwitch
does not depend on the class LightBulb
. It depends on the abstract class Switchable
. with the above code we succesfully removed the dependency between PowerSwitch
and LightBulb
.
If we want to implement the functionality for another device we can do easily by inheriting the class Switchable
. Let's implement it for device Fan
class Fan(Switchable):
def turn_on(self):
print("Fan: on")
def turn_off(self):
print("Fan: off")
We can use the Fan
object with the class PowerSwitch
just like LightBulb
.
By Dependency Inversion Principle(DIP), we have decoupled two classes through an interface, which in our case is Switchable. It is now clear to see the usefulness that dependency inversion provides by reducing coupling between classes.
References¶
- http://www.cs.sjsu.edu/~pearce/modules/lectures/ood/principles/ocp.htm
- https://python.astrotech.io/design-patterns/oop/solid.html
- https://en.wikipedia.org/wiki/Interface_segregation_principle
- https://medium.com/@zackbunch/python-dependency-inversion-8096c2d5e46c
- https://github.com/AnjaneyuluBatta505/learnbatta/blob/master/python/SOLID/DIP.py