Principe SOLID simplifiés avec des exemples en PHP

Cet article est la traduction de l’article Anglais SOLID Principles Simplified with Examples in PHP.

Principes SOLID simplifiés avec des exemples en PHP
Photo by Zach Reiner on Unsplash

SOLID est un acronyme des cinq premiers principes de conception orientée objet (OOD).
Il constitue une série de directives que les développeurs peuvent utiliser pour créer des logiciels de manière simple à maintenir et à étendre.
Comprendre ces concepts fera de vous un meilleur développeur et vous permettra d’éviter les problèmes de code.

SOLID signifie:

  • S: Principe de responsabilité unique
  • O: principe ouvert-fermé
  • L: principe de substitution de Liskov
  • I: principe de ségrégation d’interface
  • D: Principe d’inversion de dépendance

Approfondissons ces principes!

Principe de responsabilité unique

Une classe doit avoir une et une seule raison de changer, ce qui signifie qu’une classe ne doit avoir qu’un seul emploi.

Cela signifie que si notre classe assume plus d’une responsabilité, nous aurons un couplage élevé. La cause est que notre code sera fragile à tout changement.

Supposons que nous ayons une classe d’utilisateurs comme celle-ci :

<?php
class User {
  
    private $email;
    
    // Getter and setter...
    
    public function store() {
        // Store attributes into a database...
    }
}

Dans ce cas, la méthode store est en dehors de la portée.
Cette responsabilité doit appartenir à une classe qui gère la base de données.
La solution ici est de créer deux classes chacune avec les responsabilités appropriées.

<?php
class User {
  
    private $email;
    
    // Getter and setter...
}
<?php
class UserDB {
  
    public function store(User $user) {
        // Store the user into a database...
    }
}

Principe ouvert-fermé

Les objets ou entités doivent être ouverts à l’extension mais fermés à la modification.

Selon ce principe, une entité logicielle doit être facilement extensible avec de nouvelles fonctionnalités. Cela sans avoir à modifier son code existant.

Supposons que nous devions calculer l’aire totale de certains objets. Nous pouvons avoir besoin d’une classe AreaCalculator ne faisant que la somme de chaque aire de forme.
Le problème ici est que chaque forme a une méthode différente pour calculer sa propre surface.

<?php

class Rectangle {
  
    private $width;
    private $height;
    
    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
}

class Square {
  
    private $length;
    
    public function __construct($length) {
        $this->length = $length;
    }
}


class AreaCalculator {
  
    protected $shapes;
    
    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }
    
    public function sum() {
        foreach($this->shapes as $shape) {
            if($shape instanceof Square) {
                $area[] = pow($shape->length, 2);
            } else if($shape instanceof Rectangle) {
                $area[] = $shape->width * $shape->height;
            }
        }
    
        return array_sum($area);
    }
}

Si nous ajoutons une autre forme, comme un cercle, nous devons changer le calculateur de surface afin de calculer la nouvelle surface, ce qui n’est pas durable.
La solution ici est de créer une interface Shape simple. Elle utilise la méthode area, et sera implémentée par toutes les autres formes.
De cette manière, nous utiliserons une seule méthode pour calculer la somme. Si nous devons ajouter une nouvelle forme, elle implémentera simplement l’interface Shape.

<?php

interface Shape {
    public function area();
}

class Rectangle implements Shape {
  
    private $width;
    private $height;
    
    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
    
    public function area() {
        return $this->width * $this->height;
    }
}

class Square implements Shape {
  
    private $length;
    
    public function __construct($length) {
        $this->length = $length;
    }
    
    public function area() {
        return pow($this->length, 2);
    }
}


class AreaCalculator {
  
    protected $shapes;
    
    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }
    
    public function sum() {
        foreach($this->shapes as $shape) {
            $area[] = $shape->area();
        }
    
        return array_sum($area);
    }
}

Principe de substitution de Liskov

Soit q (x) une propriété prouvable sur les objets de x de type T. Alors, q (y) devrait être prouvable pour les objets y de type S, où S est un sous-type de T.

Le principe dit que les objets doivent être remplaçables par des instances de leurs sous-types sans altérer le fonctionnement correct de notre système.

Imaginez gérer deux types de machines à café.
Selon le plan de l’utilisateur, nous utiliserons une machine à café de base ou de qualité supérieure. La seule différence est que la machine de qualité supérieure produit un meilleur café vanille que la machine de base.
Le comportement du programme principal doit être identique pour les deux machines.

<?php

interface CoffeeMachineInterface {
    public function brewCoffee($selection);
}


class BasicCoffeeMachine implements CoffeeMachineInterface {
  
    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }
    
    protected function brewEspresso() {
        // Brew an espresso...
    }
}


class PremiumCoffeeMachine extends BasicCoffeeMachine {
  
    public function brewCoffee($selection) {
        switch ($selection) {
            case 'ESPRESSO':
                return $this->brewEspresso();
            case 'VANILLA':
                return $this->brewVanillaCoffee();
            default:
                throw new CoffeeException('Selection not supported');
        }
    }
    
    protected function brewVanillaCoffee() {
        // Brew a vanilla coffee...
    }
}


function getCoffeeMachine(User $user) {
    switch ($user->getPlan()) {
        case 'PREMIUM':
            return new PremiumCoffeeMachine();
        case 'BASIC':
        default:
            return new BasicCoffeeMachine();
    }
}


function prepareCoffee(User $user, $selection) {
    $coffeeMachine = getCoffeeMachine($user);
    return $coffeeMachine->brewCoffee($selection);
}

Principe de séparation des interfaces

Un client ne doit jamais être obligé d’implémenter une interface qu’il n’utilise pas ou les clients ne doivent pas être obligés de dépendre de méthodes qu’ils n’utilisent pas.

Ce principe définit qu’une interface qui ne sera pas utilisée ne doit jamais être implémenté.
Dans ce cas, cela signifie que dans nos implémentations, nous aurons des méthodes inutiles. La solution consiste à développer des interfaces spécifiques plutôt que des interfaces à usage général.

Imaginez que nous inventions la future voiture capable de voler et de conduire…

<?php

interface VehicleInterface {
    public function drive();
    public function fly();
}

class FutureCar implements VehicleInterface {
    
    public function drive() {
        echo 'Driving a future car!';
    }
  
    public function fly() {
        echo 'Flying a future car!';
    }
}

class Car implements VehicleInterface {
    
    public function drive() {
        echo 'Driving a car!';
    }
  
    public function fly() {
        throw new Exception('Not implemented method');
    }
}

class Airplane implements VehicleInterface {
  
    public function drive() {
        throw new Exception('Not implemented method');
    }
    
    public function fly() {
        echo 'Flying an airplane!';
    }
}

Comme vous pouvez le constater, le principal problème est que la voiture et l’avion ont des méthodes inutilisables.
La solution consiste à scinder l’interface VehicleInterface en deux interfaces plus spécifiques. Ces deux interfaces ne seront utilisées que lorsque c’est nécessaire, comme suit :

<?php

interface CarInterface {
    public function drive();
}

interface AirplaneInterface {
    public function fly();
}

class FutureCar implements CarInterface, AirplaneInterface {
    
    public function drive() {
        echo 'Driving a future car!';
    }
  
    public function fly() {
        echo 'Flying a future car!';
    }
}

class Car implements CarInterface {
    
    public function drive() {
        echo 'Driving a car!';
    }
}

class Airplane implements AirplaneInterface {
    
    public function fly() {
        echo 'Flying an airplane!';
    }
}

Principe d’inversion de dépendance

Les entités doivent dépendre d’abstractions et non de concrétions. Il indique que le module de haut niveau ne doit pas dépendre du module de bas niveau, mais qu’ils doivent dépendre d’abstractions.

Ce principe signifie qu’une classe particulière ne devrait pas dépendre directement d’une autre classe mais d’une abstraction de cette classe. Ce principe permet un découplage et une plus grande réutilisabilité du code.

Prenons le premier exemple de la classe UserDB. Cette classe pourrait dépendre d’une connexion à une base de données :

<?php

class UserDB {
  
    private $dbConnection;
    
    public function __construct(MySQLConnection $dbConnection) {
        $this->$dbConnection = $dbConnection;
    }
  
    public function store(User $user) {
        // Store the user into a database...
    }
}

Dans ce cas, la classe UserDB dépend directement de la base de données MySQL. Cela signifie que si nous modifions le moteur de base de données en cours d’utilisation, nous devons réécrire cette classe et enfreindre le principe Open-Close.

La solution consiste à développer une abstraction de la connexion à la base de données :

<?php

interface DBConnectionInterface {
    public function connect();
}

class MySQLConnection implements DBConnectionInterface {
  
    public function connect() {
        // Return the MySQL connection...
    }
}

class UserDB {
  
    private $dbConnection;
    
    public function __construct(DBConnectionInterface $dbConnection) {
        $this->$dbConnection = $dbConnection;
    }
  
    public function store(User $user) {
        // Store the user into a database...
    }
}

Conclusion

Le principe SOLID représentent l’état de l’art de la qualité du code et permettent ensuite d’écrire un logiciel facilement extensible, réutilisable et factorisé.

J’espère que cet article vous aidera à mieux comprendre la qualité du code et peut-être à améliorer votre capacité de codage!

Bonne codage! 😄