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.