SOLID principles¶
SOLID is an acronym for the first five principles of object-oriented design (OOD) by Robert C. Martin (also known as Uncle Bob).
These principles set out practices for developing software with a view to maintainability 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
A class’s methods should be focused on a single purpose.
- O – Open-Closed Principle
Objects should be open for extension but closed for modification.
- L – Liskov Substitution Principle
Subclasses should be substitutable for their superclasses.
- I – Interface Segregation Principle
Objects should not depend on methods they do not use.
- D – The Dependency Inversion Principle
Abstractions should not depend on details.
Single Responsibility Principle¶
The Single Responsibility Principle states that each class should fulfil only one task:
A class should have one, and only one, reason to change.
– SRP: The Single Responsibility Principle by Robert C. Martin
Let’s take, for example, an application that takes a collection of shapes – circles and squares – and calculates the sum of the perimeters of all the shapes in the collection.
First, create the Form classes with the necessary parameters. For
squares, this is the side length, and for circles, the diameter:
class Form:
def __init__(self, x=0, y=0):
self.x = x
self.y = 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 class called SquaresAndCircles containing the
logic for calculating the perimeters of all 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 handles the logic required to calculate the
perimeters of all squares and circles. This fulfils the Single Responsibility
Principle.
Open-Closed Principle¶
The Open-Closed Principle states:
Modules should be both open (for extension) and closed (for modification).
Accordingly, the code would require very little modification to add new functionality, for example, via a subclass.
Let’s look at the SquaresAndCircles class and focus on the
circumferences() method. Imagine a scenario in which the sum of additional
shapes, such as triangles, pentagons, hexagons, etc., needs
to be calculated. You would have to constantly edit this class and add further
if blocks. That would violate the Open-Closed Principle. One way to improve
this method is to remove the logic for calculating the circumference of each
shape from the SquaresAndCircles class and attach it to the classes for
the specific shapes. 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 sum method circumferences() 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 to new requirements, you should first reorganise (refactor) the existing code so that it is open to the new functionality. Only then should you add new code.
Refactoring refers to the process of modifying a software system in such a way that the external behaviour of the code remains unchanged, whilst its internal structure is improved.
– Refactoring by Martin Fowler
Note
Safe refactoring relies on tests. If you are genuinely restructuring the code without changing its behaviour, the existing tests should continue to pass at every stage. The tests act as a safety net that justifies your confidence in the new structure of the code. If they fail,
you have inadvertently broken the code,
or the existing tests are faulty.
Liskov Substitution Principle¶
The Liskov Substitution Principle states that a programme which uses objects of the base class must also function correctly with objects of the subclass. There are two rules of thumb that are helpful for adhering to the Liskov Substitution Principle:
Avoid overriding concrete methods wherever possible.
If you do so nonetheless, check whether you can call the overridden method from within the overriding method.
Let’s extend the Form class so that classes derived from it 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 along 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
The Liskov Substitution Principle also applies to Duck typing: any 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 known as polymorphism.
Interface Segregation Principle¶
The Interface Segregation Principle applies the Single Responsibility Principle to interfaces in order to isolate specific behaviour. If a change is required to part of your code, extracting an object that plays a particular role makes it possible to support the new behaviour without having to modify the existing code. This is preferable to hard-coded specialisations.
In the previous example, for instance, we checked whether our Form object
actually provided a circumference() method. This is necessary in case
shapes such as Point or Line are added later, which do not
have a circumference.
Note
In this context, the Law of Demeter is also of interest; it 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 coupling between objects: an object can only communicate with its neighbours, but not with the neighbours of its neighbours; objects can only send messages to those directly involved.
The Dependency Inversion Principle¶
The Dependency Inversion Principle can be defined as follows:
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
– The Dependency Inversion Principle by Robert C. Martin
circumferences() should not be defined within the Forme class,
as there are shapes without a circumference.