SOLID is an acronym for first five object-oriented design (OOD) principles by Robert C. Martin (aka Uncle Bob).
These principles make developers life easy by making foundations for maintenance, clean code and extending of code base as project grows. These design principles encourage us to create more maintainable, understandable, and flexible software.
S — Single Responsibility Principle
A class should have one and only one reason to change, meaning a class/method should have only one job.
- Classes and methods should have high cohesion
class DrawShapes:
def draw_circle(self):
[...]
def draw_rectangle(self):
[...]
# This function violates the principle
# Give it it's own class
def clear_shape(self):
[...]
Solution:
class DrawShapes:
def draw_circle(self):
[...]
def draw_rectangle(self):
[...]
class Display:
def clear_shape(self):
[...]
O — Open/Closed Principle
Objects or entities should be open for extension but closed for modification.
The above code also violates the second principle, what if we wanted to draw circle using different algorithm? or in different color?
i.e., the previous class DrawShapes
need to be modified to make changed happen, but that's what second principle states; class should be closed for modification.
The below code solves the issue:
class Circle:
# This class is closed for modification
def draw_circle(self):
"""Draw default circle"""
[...]
# But open for extension
class CircleNewAlgorithm(Circle):
# The different function to draw circle
def draw_circle_using_new_algo(self):
[...]
L — Liskov Substitution Principle
Letq(x)
be a property provable about objects ofx
of typeT
. Thenq(y)
should be provable for objectsy
of typeS
whereS
is a subtype ofT
Parent classes should be replaceable by their subclasses but without altering the behavior.
In previous code, the class CircleNewAlgorithm
class's function draw_circle_using_new_algo
breaks the third principle.
If a parent class can do something, a child class must also be able to do it.
The newer class is supposed to draw circle using new algorithm but using inheritance gets the older function as well, making it ambiguous to which function should be called:
circle = CircleNewAlgorithm()
# Which of the two function should be called?
circle.draw_circle() # older algorithm
circle.draw_circle_using_new_algo() # newer algorithm
The solution is simply to override the previous implementation:
class CircleNewAlgorithm(Circle):
# Override older function to draw circle
draw_circle(self):
"""Newer algorithm goes here"""
[...]
I — Interface Segregation Principle
A client/classes should never be forced to implement an interface that it doesn't use, clients shouldn't be forced to depend on methods they do not use.
Use several specific interfaces instead of one general one.
Lets say our original Circle
class implement WindowInterface
like this:
from abc import abstractmethod
# Abstract base class used as interface for example
# but other language support interfaces
class WindowInterface:
@abstractmethod
def draw_circle(self):
pass
@abstractmethod
def clear_screen(self):
"""function to clear screen"""
pass
The abstract method clear_screen
violates fourth principle as class implementing this might require to implement clear_screen
method as well, which obviously is not required for a child class of Circle
to handle. This will also violate first principle.
But what if the method is somehow reasonable to belong to interface and not to classes implementing them, then
In that case you can provide default implementation for that function (like default methods in Java) so the class implementing the interface need not to provide the implementation if they don't use it.
Remember a class should not be forced to implement an interface that it doesn't use.
For our example, solution might look like this:
from abc import abstractmethod
# Our interface for class
class CircleInterface:
@abstractmehod
def draw_circle(self):
pass
class Circle(CircleInterface):
def draw_circle(self):
"""implement circle drawing function"""
[...]
# Our interface for display
class DisplayInterface:
@abstractmethod
def clear_screen():
pass
class Display(DisplayInterface)
def clear_screen(self):
"""implement screen clearning function"""
[...]
D — Dependency Inversion Principle
Entities/classes must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
The principle allows for decoupling.
Let's say out Circle
class depends on some external library to render the shape it draws.
class Circle:
import GraphicsLibrary
def __init__(self):
# external concreate dependency is problematic
self.driver = self.GraphicsLibrary.driver
def draw_circle(self)
self.driver.draw_pixel()
circle = Circle()
The problem arises when we want to use some other library for rendering or create our own library. The concretion makes it harder to make that change to current implementation.
What we can do is something like this:
from abc import abstractmethod
# create an interface to render
class Renderer():
@abstractmethod
def driver(self):
pass
# Create our class for external render library
class GraphicsLibraryRender(Render):
import GraphicsLibrary
def driver(self):
return self.GraphicsLibrary.driver
# and our Circle class can now use the interface
class Circle:
def __init__(self, Renderer):
self.driver = self.Renderer.driver()
def draw_circle(self)
self.driver.draw_pixel()
graphics_ren = GraphicsLibraryRender()
circle = Circle(graphics_ren)
What this will now allow us to use different classes that implements Renderer
interface and that could be external library or in future your own library or dummy library for dry run without modifying the entire codebase.
Keep Learning...