Introduction aux principes de la programmation objet SOLID

SOLID est un acronyme pour les cinq premiers principes de conception orientée objet (OOD) et est 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 odeurs de code.

SOLID signifie :

Plongeons-nous plus profondément dans ces principes !

1 - 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 en 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, le magasin de méthodes est hors de portée et 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 des 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...
    }
}

2 - Principe ouvert-fermé

Les objets ou entités doivent être ouverts pour extension mais fermés pour modification. Selon ce principe, une entité logicielle doit être facilement extensible avec de nouvelles fonctionnalités sans avoir à modifier son code existant en cours d'utilisation.

Supposons que nous devions calculer l'aire totale de certains objets et pour ce faire, nous avons besoin d'une classe AreaCalculator qui ne fait qu'une somme de chaque aire de forme. Le problème ici est que chaque forme a une méthode différente pour calculer sa propre zone.


<?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 AreaCalculator afin de calculer la nouvelle zone de forme et ce n'est pas durable. La solution ici est de créer une interface Shape simple qui a la méthode area et sera implémentée par toutes les autres formes. De cette façon, nous n'utiliserons qu'une seule méthode pour calculer la somme et si nous devons ajouter une nouvelle forme, il 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);
    }
}

3 - 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 bon fonctionnement de notre système.

Imaginez gérer deux types de machine à café. Selon le plan d'utilisation, nous utiliserons une machine à café de base ou premium, la seule différence est que la machine premium fait un bon café vanille plus que la machine de base. Le comportement du programme principal doit être le même 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);
}

4 - Principe de ségrégation d'interface

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

Ce principe définit qu'une classe ne doit jamais implémenter une interface qui ne va pas être utilisée. Dans ce cas, cela signifie que dans nos implémentations, nous aurons des méthodes dont nous n'avons pas besoin.

La solution consiste à développer des interfaces spécifiques au lieu d'interfaces polyvalentes.

Imaginez que nous inventions la FutureCar qui peut à la fois voler et conduire…


&l<?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!';
    }
}

Le problème principal, comme vous pouvez le voir, est que la voiture et l'avion ont des méthodes qui n'utilisent pas. La solution consiste à diviser le VehicleInterface en deux interfaces plus spécifiques qui ne sont utilisées que lorsque cela 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!';
    }
}

5 - 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 ils doivent dépendre d'abstractions.

Ce principe signifie qu'une classe particulière ne doit 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 peut dépendre d'une connexion DB :


<?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 utilisé, nous devons réécrire cette classe et violer le principe d'ouverture-fermeture.

La solution est de développer une abstraction de 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...
    }
}

Ces principes représentent l'état de l'art de la qualité du code et les suivre vous permet d'écrire des logiciels qui seront facilement étendus, réutilisables et refactorisés.