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): [...]
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
q(x)be a property provable about objects of
q(y)should be provable for objects
Sis a subtype of
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.