SOLID principles¶
SOLID an acronym for the first five principles of object-oriented design (OOD) by Robert C. Martin (also known as Uncle Bob).
These principles establish practices for developing software with considerations for maintenance and extensibility as the project grows. Adopting these principles can also help to avoid code smells, refactor code and develop agile or adaptive software.
SOLID stands for:
- S – Single responsibility principle
The methods of a class should be orientated towards a single purpose.
- O – Open-closed principle
Objects should be open for extensions but closed for changes.
- L – Liskov substitution principle
Subclasses should be substitutable by their superclasses.
- I – Interface segregation principle
Objects should not depend on methods that they do not use.
- D – Dependency inversion principle
Abstractions should not depend on details.
Single responsibility principle¶
The single responsibility principle states that each class should only fulfil one task:
A class should have one and only one reason to change, meaning that a class should have only one job.
– SRP: The Single Responsibility Principle by Robert C. Martin
For example, consider an application that takes a collection of shapes – circles and squares – and calculates the sum of the circumferences of all the forms in the collection.
First create the Form classes with the necessary parameters. For
squares this is the edge length and for circles the diameter:
class Form:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def move(self, delta_x, delta_y):
self.x = self.x + delta_x
self.y = self.y + delta_y
class Square(Form):
def __init__(self, length=1, x=0, y=0):
super().__init__(x, y)
self.length = length
class Circle(Form):
def __init__(self, diameter=1, x=0, y=0):
super().__init__(x, y)
self.diameter = diameter
Now you can create a SquaresAndCircles class with the logic to
calculate all the circumferences of squares and circles:
import gc
class SquaresAndCircles:
pi = 3.14159
@classmethod
def circumferences(cls):
csum = 0
for obj in gc.get_objects():
if isinstance(obj, Square):
csum = csum + 4 * obj.length
if isinstance(obj, Circle):
csum = csum + obj.diameter * cls.pi
return csum
The SquaresAndCircles class takes over the logic required to calculate
all the circumferences of squares and circles. This fulfils the principle of
individual responsibility.
Open-closed principle¶
The OCP states:
Objects or entities should be open for extension but closed for modification.
This means that a class should be extendable without changing the class itself.
Let’s take a look at the SquaresAndCircles class and focus on the
circumferences() method. Imagine a scenario where you want to calculate
the sum of additional forms such as triangles, pentagons, hexagons, etc. You would have to constantly edit this class and add more if
blocks. This would violate the open-closed principle. One way to improve this
method is to remove the logic for calculating the circumference of each form
from the SquaresAndCircles class and attach it to the special form
classes. Here, the circumference calculations are defined in the Square
and Circle classes:
class Square(Form):
def __init__(self, length=1, x=0, y=0):
super().__init__(x, y)
self.length = length
def circumference(self):
return 4 * self.length
class Circle(Form):
pi = 3.14159
def __init__(self, diameter=1, x=0, y=0):
super().__init__(x, y)
self.diameter = diameter
def circumference(self):
return self.diameter * Circle.pi
The circumferences() sum method in the CircumferenceFormInstances
class can then be rewritten as follows:
class CircumferenceFormInstances:
def circumferences():
csum = 0
for obj in gc.get_objects():
if isinstance(obj, Form) and hasattr(obj, "circumference"):
csum = csum + obj.circumference()
return csum
This fulfils the open-closed principle.
Tip
If your code is not yet open for new requirements, you should first refactor the existing code so that it is open for the new function. Only then should you add new code.
Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.
– Refactoring by Martin Fowler
Note
Safe refactoring relies on tests. If you really refactor the code without changing the behaviour, the existing tests should continue to succeed at every step. The tests are a safety net that justifies confidence in the new arrangement of the code. If they fail,
you have inadvertently broken the code,
or the existing tests are flawed.
Liskov substitution principle¶
Liskov substitution principle states that a programme that uses objects of the base class must also function correctly with objects of the subclass.
Let’s extend our Form class so that derived forms can be moved in the
x and y directions:
class Form:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def move(self, delta_x, delta_y):
self.x = self.x + delta_x
self.y = self.y + delta_y
You can then move both squares and circles on the x and y axes:
>>> import forms
>>> s1 = forms.Square()
>>> c1 = forms.Circle()
>>> s1.x, s1.y, c1.x, c1.y
(0, 0, 0, 0)
>>> s1.move(4, 5)
>>> c1.move(2, 3)
>>> s1.x, s1.y, c1.x, c1.y
(4, 5, 2, 3)
Note
Liskov’s substitution principle also applies to Duck typing: every object that claims to be a duck must fully implement the duck’s API. Duck types should be interchangeable. Applying logic across different data types of objects is called polymorphism.
Interface segregation principle¶
The interface segregation principle applies the Single responsibility principle principle to interfaces in order to isolate a specific behaviour. If a change to a part of your code is required, extracting an object that plays a role opens up the possibility of supporting the new behaviour without having to change the existing code. This is preferable to coded concretisations.
In the previous example, we checked whether our Form object actually
provides a circumference() method. This is necessary if forms such as
Point or Line are added later that do not have a
circumference.
Note
In this context, Demeter’s law is also interesting, which states that objects should only communicate with objects in their immediate vicinity. This effectively restricts the list of other objects to which an object can send a message and reduces the coupling between objects: an object can only talk to its neighbours, but not to the neighbours of its neighbours; objects can only send messages to those directly involved.
Dependency inversion principle¶
The Dependency inversion principle can be defined as
Abstractions should not depend upon details. Details should depend upon abstractions.
– Robert C. Martin: The Dependency Inversion Principle
circumferences() should not already be defined in the Form class,
as there are also forms without circumferences.