SOLID Principles explained with examples

November 27, 2021 MrAnyx 6 min de lecture

SOLID principles

SOLID principles have almost become standards in programming, and more particularly in object oriented programming (OOP). These principles were first mentioned in 2000 in a book by Robert C. Martin entitled Design Principles and Design Patterns.

The term SOLID is actually an acronym for 5 fundamental principles :

  • S - Single responsability principle,
  • O - Open-closed principle,
  • L - Liskov substitution principle,
  • I - Interface segregation principle,
  • D - Dependency inversion principle.

These principles were devised in order to overcome a recurring problem during the development phase, but more particularly during the maintenance phase of a software or an application.

Indeed, for a long time, there were no real rules or standards allowing to produce flexible, reusable and maintainable code. This is why the SOLID principles were imagined.

Finally, what do each of these principles correspond to and how can we implement them in a program ?

All of the examples below are available in a Github repository that I created for the occasion : https://github.com/MrAnyx/solid-principles

Simple responsibility principle

This first principle is probably the best known of all. As its name may indicate, this one says that each function, each class must be as specialized as possible. These elements should have only one responsibility.

In many cases, it is easier to develop and maintain a class or function that has only one purpose. In other words, we must at all costs avoid "Swiss Army knife" classes and functions. Better to develop several specialized classes rather than just one that does everything.

In addition, it is this principle that will be mainly applied during aspect-oriented programming, because all the transversal concerns (cross-cutting concerns) will be dissociated from the principle logic of the method.

Thus, by applying this principle, it will be much easier, as a developer to :

  • Maintain classes and functions, because they will have only one responsibility,
  • Reuse classes and functions,
  • Test the classes and functions, because there will be fewer cases to consider.

Bad

class Singer {
   public function sing() {
      echo "🎶🎵";
   }

   public function writeSongs() {
      echo "I'm writing songs of my album";
   }

   public function publishSong() {
      echo "I'm publishing the song on Spotify";
   }
}

Good

class Singer {
   public function sing() {
      echo "🎶🎵";
   }

   public function writeSongs() {
      echo "I'm writing songs of my album";
   }
}

class Producer {
   public function publishSong() {
      echo "I'm publishing the song on Spotify"
   }
}

Open-closed principle

For a class to be the easiest to reuse across multiple projects, it may be important to apply this principle. This indicates that a class must, as much as possible, be open to evolution and closed to modification.

In other words, this principle allows to extend the behavior of a class without making any modifications to it.

Conversely, if we had to make changes to already existing classes with each new feature, it would be almost impossible to maintain functional code.

Thus, the main advantages of this principle are :

  • Facilitate the evolution of a class,
  • Make the code more flexible in general.

Bad

class Triangle {
   private float $baseLength;
   private float $height;
   // Constructor, Getters & Setters ...
}

class Circle {
   private float $radius;
   // Constructor, Getters & Setters ...
}

class AreaCalculation {
   /**
    * @param Triangle[]|Circle[] $shapes
    */
   public function getAreaSum(array $shapes): float {
      $sum = 0;
      foreach($shapes as $shape) {
         if($shape instanceof Triangle) {
            $sum += ($shape->getBaseLength() * $shape->getHeight()) / 2;
         } else if($shape instanceof Circle) {
            $sum += pi() * ($shape->getRadius() ** 2);
         }
      }
      return $sum;
   }
}

Good

abstract class Shape {
   public abstract function getArea();
}

class Triangle extends Shape {
   private float $baseLength;
   private float $height;
   // Constructor, Getters & Setters ...

   public function getArea(): float {
      return ($this->getBaseLength() * $this->getHeight()) / 2;
   }
}

class Circle extends Shape {
   private float $radius;
   // Constructor, Getters & Setters ...

   public function getArea(): float {
      return pi() * ($this->getRadius() ** 2);
   }
}

class AreaCalculation {
   /**
    * @param Shape[] $shapes
    */
   public function getAreaSum(array $shapes): float {
      $sum = 0;
      foreach($shapes as $shape) {
         $sum += $shape->getArea();
      }
      return $sum;
   }
}

Liskov substitution principle

The principle of Liskov substitution is particularly easy to understand. This indicates that all subclasses of a parent class should be able to perform the same actions. If this is the case, it will then be possible to interchange a parent class with one of the child classes without any problems.

Generally, using inheritance, this principle makes it possible to standardize the behavior of the classes that we create for a project.

For example, if we create a Mustang and Ford class which inherit from the same Car class, the Mustang and Ford classes must have the same behavior as the Car class so that , if necessary, it is possible to interchange, for example, the Mustang class with the Car class without any problems.

By applying this type of principle, it will then be possible to :

  • Reduce the probability of bugs,
  • Make our code more logical.

Bad

abstract class Animal {
   public abstract function makeNoise();
}

class Dog extends Animal {
   public function makeNoise() {
      echo "Waf !";
   }
}

class Fish extends Animal {
   public function makeNoise() {
     throw new Exception("A fish can't make a noise under water");
   }
}

Good

abstract class LandAnimal {
   public abstract function makeNoise();
}

abstract class AquaticAnimal {
   public abstract function swim();
}

class Dog extends LandAnimal {
   public function makeNoise() {
      echo "Waf !";
   }
}

class Fish extends AquaticAnimal {
   public function swim() {
     echo "I'm swimming with my fins";
   }
}

Interface segregation principle

The principle of interface segregation is a principle that says that a class should only implement the methods that are really necessary for it. By extension, a class should only implement the interfaces it needs.

Indeed, if a certain class should not implement a certain method, then it is not necessary to implement it.

For example, suppose we have a FlyingMachine interface containing the igniteMotors() and fly() methods. So this interface could be implemented on an Airplane class, but not for a Glider class. In fact, in both cases, the machine in question can fly, but in the case of the glider, it has no motors. It would then be necessary to create several interfaces in order to implement only the functions necessary for the class.

Thus, better to implement several specific interfaces rather than an overly generic willow.

Applying this principle therefore makes it possible to :

  • Have concise, no-frills classes that have a specific objective,
  • Reduce the appearance of bugs in the event of a method not implemented.

Bad

interface IFlyingMachine {
   public function startEngines();
   public function fly();
}

class Airplane implements IFlyingMachine {
   public function startEngines() {
      echo "I start all the engines";
   }

   public function fly() {
      echo "The airplane is flying above the sea";
   }
}

class Glider implements IFlyingMachine {
   public function startEngines() {
      throw new Exception("Sorry, I don't have engines");
   }

   public function fly() {
      echo "The glideris flying above the sea";
   }
}

Good

interface IFlyingMachine {
   public function fly();
}

interface IFlyingMachineWithEngine {
   public function startEngines();
}

class Airplane implements IFlyingMachine, IFlyingMachineWithEngine {
   public function startEngines() {
      echo "I start all the engines";
   }

   public function fly() {
      echo "The airplane is flying above the sea";
   }
}

class Glider implements IFlyingMachine {
   public function fly() {
      echo "The glider is flying above the sea";
   }
}

Dependency inversion principle

The principle of inversion of dependencies is a principle which says that we must prefer the use of interfaces or more global classes rather than too precise child classes and with a too low level of abstraction.

Finally, this principle is closely linked to the Liskov substitution that we have detailed previously.

This principle therefore makes it possible to :

  • Make the code more flexible,
  • Facilitate the evolution of the project.

Suppose we have the following code :

interface PersonInterface {
    public function walk();
    public function speak();
}

class Human implements PersonInterface {
    private string $name;
    private int $age;

    public function walk() {
        echo "I walk";
    }

    public function speak() {
        echo "I speak";
    }
}

Bad

/**
 * @param Human $thing
 */
function foo(Human $thing) {
   $thing->walk();
}

Good

/**
 * @param PersonInterface $thing
 */
function foo(PersonInterface $thing) {
   $thing->walk();
}

Conclusion

Ultimately, all of these principles are closely linked. By applying one of them, this induces the application of another principle. To summarize, these principles are used to create and produce code that is easier to maintain, evolve or even debug. This also makes it possible to facilitate team projects as well as the testing of different classes and functions.

Cover by Fakurian Design


Cette œuvre est mise à disposition selon les termes de la licence Licence Creative Commons