One place for hosting & domains

      SOLID : les 5 premiers principes de conception orientée objet


      Introduction

      SOLID est un acronyme des cinq premiers principes de la conception orientée objet (OOD) de Robert C. Martin (également connu sous le nom de d’oncle Bob).

      Remarque : bien que ces principes puissent s’appliquer aux différents langages de programmation, nous utiliserons le langage PHP dans l’exemple de code utilisé dans cet article.

      Ces principes établissent des pratiques qui appartiennent au développement de logiciels tout en prenant en considération les exigences d’entretien et d’extension à mesure que le projet se développe. L’adoption de ces pratiques peut également contribuer à éviter les mauvaises odeurs, réusiner un code et développer des logiciels agiles et adaptatifs.

      SOLID signifie :

      Au cours de cet article, vous serez initié à chacun de ces principes afin de comprendre comment SOLID peut vous aider à devenir un meilleur développeur.

      Principe de responsabilité unique

      Le principe de responsabilité unique (SRP) précise ce qui suit :

      Une classe doit avoir une seule et unique raison de changer, ce qui signifie qu’une classe ne doit appartenir qu’à une seule tâche.

      Par exemple, imaginez une application qui prend un ensemble de formes (cercles et carrés) et calcule la somme de la superficie de toutes les formes de l’ensemble.

      Tout d’abord, vous devez créer les classes de forme et configurer les paramètres requis des constructeurs.

      Pour les carrés, vous devrez connaître la longueur d’un côté :

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

      Pour les cercles, vous devrez connaître le rayon :

      class Circle
      {
          public $radius;
      
          public function construct($radius)
          {
              $this->radius = $radius;
          }
      }
      

      Ensuite, créez la classe AreaCalculator et écrivez la logique pour additionner les superficies de toutes les formes fournies. La superficie d’un carré se calcule avec la longueur au carré. La superficie d’un cercle se calcule en multipliant pi par le rayon au carré.

      class AreaCalculator
      {
          protected $shapes;
      
          public function __construct($shapes = [])
          {
              $this->shapes = $shapes;
          }
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  if (is_a($shape, 'Square')) {
                      $area[] = pow($shape->length, 2);
                  } elseif (is_a($shape, 'Circle')) {
                      $area[] = pi() * pow($shape->radius, 2);
                  }
              }
      
              return array_sum($area);
          }
      
          public function output()
          {
              return implode('', [
                '',
                    'Sum of the areas of provided shapes: ',
                    $this->sum(),
                '',
            ]);
          }
      }
      

      Pour utiliser la classe AreaCalculator, vous devrez instancier la classe, y passer un tableau de formes et afficher la sortie au bas de la page.

      Voici un exemple avec un ensemble de trois formes :

      • un cercle avec un rayon de 2
      • un carré avec une longueur de 5
      • un second carré avec une longueur de 6
      $shapes = [
        new Circle(2),
        new Square(5),
        new Square(6),
      ];
      
      $areas = new AreaCalculator($shapes);
      
      echo $areas->output();
      

      Le problème avec la méthode de sortie est que l’AreaCalculator gère la logique qui génère les données.

      Imaginez un scénario où la sortie doit être convertie dans un autre format, comme JSON.

      L’ingralité de la logique serait traitée par la classe AreaCalculator. Cela viendrait enfreindre le principe de responsabilité unique. La classe AreaCalculator doit uniquement s’occuper de la somme des superficies des formes fournies. Elle ne doit pas chercher à savoir si l’utilisateur souhaite un format JSON ou HTML.

      Pour y remédier, vous pouvez créer une classe SumCalculatorOutputter distincte. Ensuite, utilisez cette nouvelle classe pour gérer la logique dont vous avez besoin pour générer les données sur l’utilisateur :

      class SumCalculatorOutputter
      {
          protected $calculator;
      
          public function __constructor(AreaCalculator $calculator)
          {
              $this->calculator = $calculator;
          }
      
          public function JSON()
          {
              $data = [
                'sum' => $this->calculator->sum(),
            ];
      
              return json_encode($data);
          }
      
          public function HTML()
          {
              return implode('', [
                '',
                    'Sum of the areas of provided shapes: ',
                    $this->calculator->sum(),
                '',
            ]);
          }
      }
      

      La classe SumCalculatorOutputter fonctionnerait comme suit :

      $shapes = [
        new Circle(2),
        new Square(5),
        new Square(6),
      ];
      
      $areas = new AreaCalculator($shapes);
      $output = new SumCalculatorOutputter($areas);
      
      echo $output->JSON();
      echo $output->HTML();
      

      Maintenant, la logique dont vous avez besoin pour générer les données pour l’utilisateur est traitée par la classe SumCalculatorOutputter.

      Cela répond au principe de responsabilité unique.

      Ouvert/fermé

      Le principe ouvert/fermé (S.R.P.) précise ce qui suit :

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

      Cela signifie qu’une classe doit être extensible mais ne pas modifier la classe en elle-même.

      Reprenons la classe AreaCalculator et concentrons sur la méthode sum :

      class AreaCalculator
      {
          protected $shapes;
      
          public function __construct($shapes = [])
          {
              $this->shapes = $shapes;
          }
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  if (is_a($shape, 'Square')) {
                      $area[] = pow($shape->length, 2);
                  } elseif (is_a($shape, 'Circle')) {
                      $area[] = pi() * pow($shape->radius, 2);
                  }
              }
      
              return array_sum($area);
          }
      }
      

      Imaginez qu’un utilisateur souhaite connaître la somme (sum) de formes supplémentaires comme des triangles, des pentagones, des hexagones, etc. Il vous faudrait constamment modifier ce fichier et ajouter des blocs if/else. Cela viendrait enfreindre le principe ouvert/fermé.

      Il existe un moyen de rendre cette méthode sum plus simple qui consiste à supprimer la logique qui permet de calculer la superficie de chaque forme de la méthode de la classe AreaCalculator et la joindre à la classe de chaque forme.

      Voici la méthode area définie dans Square :

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

      Et voici la méthode area définie dans Circle :

      class Circle
      {
          public $radius;
      
          public function construct($radius)
          {
              $this->radius = $radius;
          }
      
          public function area()
          {
              return pi() * pow($shape->radius, 2);
          }
      }
      

      Vous pourrez alors réécrire la méthode sum pour AreaCalculator de la manière suivante :

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

      Maintenant, vous pouvez créer une autre classe de forme et la passer dans le calcul de la somme sans briser le code.

      Cependant, un autre problème se pose. Comment savoir si l’objet passé dans l’AreaCalculator est réellement une forme ou si la forme a une méthode nommée area ?

      Le codage vers une interface fait partie intégrante de SOLID.

      Créez une ShapeInterface qui prend en charge area :

      interface ShapeInterface
      {
          public function area();
      }
      

      Modifiez vos classes de forme pour implémenter la ShapeInterface.

      Voici la mise à jour faite à Square :

      class Square implements ShapeInterface
      {
          // ...
      }
      

      Et voici la mise à jour faite à Circle :

      class Circle implements ShapeInterface
      {
          // ...
      }
      

      Dans la méthode sum pour AreaCalculator, vous pouvez vérifier si les formes fournies sont effectivement des instances de la ShapeInterface. Dans le cas contraire, lancez une exception :

       class AreaCalculator
      {
          // ...
      
          public function sum()
          {
              foreach ($this->shapes as $shape) {
                  if (is_a($shape, 'ShapeInterface')) {
                      $area[] = $shape->area();
                      continue;
                  }
      
                  throw new AreaCalculatorInvalidShapeException();
              }
      
              return array_sum($area);
          }
      }
      

      Cela satisfait au principe ouvert/fermé.

      Substitution de Liskov

      Le principe de substitution de Liskov précise ce qui suit :

      Si q(x) est une propriété démontrable pour tout objet x de type T, alors q(y) est vraie pour tout objet y de type S tel que S est un sous-type de T.

      Cela signifie que chaque sous-classe ou classe dérivée doit être substituable au niveau de leur classe de base ou parent.

      En reprenant l’exemple de la classe AreaCalculator, imaginez une nouvelle classe VolumeCalculator qui étend la classe AreaCalculator :

      class VolumeCalculator extends AreaCalculator
      {
          public function construct($shapes = [])
          {
              parent::construct($shapes);
          }
      
          public function sum()
          {
              // logic to calculate the volumes and then return an array of output
              return [$summedData];
          }
      }
      

      Rappelez-vous que la classe SumCalculatorOutputter ressemble à ce qui suit :

      class SumCalculatorOutputter {
          protected $calculator;
      
          public function __constructor(AreaCalculator $calculator) {
              $this->calculator = $calculator;
          }
      
          public function JSON() {
              $data = array(
                  'sum' => $this->calculator->sum();
              );
      
              return json_encode($data);
          }
      
          public function HTML() {
              return implode('', array(
                  '',
                      'Sum of the areas of provided shapes: ',
                      $this->calculator->sum(),
                  ''
              ));
          }
      }
      

      Si vous avez essayé d’exécuter un exemple comme celui-ci :

      $areas = new AreaCalculator($shapes);
      $volumes = new VolumeCalculator($solidShapes);
      
      $output = new SumCalculatorOutputter($areas);
      $output2 = new SumCalculatorOutputter($volumes);
      

      Une fois que vous appelez la méthode HTML sur l’objet $output2, vous obtiendrez une erreur E_NOTICE vous informant de la conversion d’un tableau en chaînes de caractères.

      Pour corriger ce problème, au lieu de renvoyer un tableau à partir de la méthode de somme de la classe VolumeCalculator, renvoyez $summedData :

      class VolumeCalculator extends AreaCalculator
      {
          public function construct($shapes = [])
          {
              parent::construct($shapes);
          }
      
          public function sum()
          {
              // logic to calculate the volumes and then return a value of output
              return $summedData;
          }
      }
      

      Le $summedData peut être un décimal, un double ou un entier.

      Cela satisfait au principe de substitution de Liskov.

      Ségrégation des interfaces

      Le principe de ségrégation des interfaces indique ce qui suit :

      Un client ne doit jamais être forcé à installer une interface qu’il n’utilise pas et les clients ne doivent pas être forcés à dépendre de méthodes qu’ils n’utilisent pas.

      Toujours en se basant sur l’exemple précédent de ShapeInterface, vous devrez prendre en charge les nouvelles formes tri-dimensionnelles de Cuboid et Spheroid. Ces formes devront également calculer volume.

      Imaginons ce qui se passerait si vous deviez modifier la ShapeInterface pour ajouter un autre contrat :

      interface ShapeInterface
      {
          public function area();
      
          public function volume();
      }
      

      Maintenant, toute forme que vous créez doit implémenter la méthode volume. Cependant, vous savez que les carrés sont des formes plates et qu’ils n’ont pas de volume. Ainsi, cette interface forcera la classe Square à implémenter une méthode dont elle n’a pas besoin.

      Cela viendrait enfreindre le principe de ségrégation des interfaces. Au lieu de cela, vous pouvez créer une autre interface appelée ThreeDimensionalShapeInterface qui a le contrat volume et les formes tri-dimensionnelles peuvent implémenter cette interface :

      interface ShapeInterface
      {
          public function area();
      }
      
      interface ThreeDimensionalShapeInterface
      {
          public function volume();
      }
      
      class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
      {
          public function area()
          {
              // calculate the surface area of the cuboid
          }
      
          public function volume()
          {
              // calculate the volume of the cuboid
          }
      }
      

      Il s’agit d’une bien meilleure approche. Mais faites attention à ne pas tomber dans le piège lors de la saisie de ces interfaces. Au lieu d’utiliser une ShapeInterface ou une ThreeDimensionalShapeInterface, vous pouvez créer une autre interface, peut-être ManageShapeInterface. Vous pourrez ensuite l’implémenter sur les formes à la fois plates et tri-dimensionnelles.

      Ainsi, vous pourrez gérer les formes avec une application unique :

      interface ManageShapeInterface
      {
          public function calculate();
      }
      
      class Square implements ShapeInterface, ManageShapeInterface
      {
          public function area()
          {
              // calculate the area of the square
          }
      
          public function calculate()
          {
              return $this->area();
          }
      }
      
      class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
      {
          public function area()
          {
              // calculate the surface area of the cuboid
          }
      
          public function volume()
          {
              // calculate the volume of the cuboid
          }
      
          public function calculate()
          {
              return $this->area();
          }
      }
      

      Maintenant dans la classe AreaCalculator, vous pouvez remplacer l’appel à la méthode area avec calculate, et également vérifier si l’objet est une instance de ManageShapeInterface et non de la ShapeInterface.

      Cela satisfait au principe de ségrégation des interfaces.

      Inversion des dépendances

      Le principe d’inversion des dépendances précise :

      Les entités doivent dépendre des abstractions, pas des implémentations. Il indique que le module de haut niveau ne doit pas dépendre du module de bas niveau, mais qu’ils doivent dépendre des abstractions.

      Ce principe permet le découplage.

      Voici l’exemple d’un PasswordReminder qui se connecte à une base de données MySQL :

      class MySQLConnection
      {
          public function connect()
          {
              // handle the database connection
              return 'Database connection';
          }
      }
      
      class PasswordReminder
      {
          private $dbConnection;
      
          public function __construct(MySQLConnection $dbConnection)
          {
              $this->dbConnection = $dbConnection;
          }
      }
      

      Tout d’abord, le MySQLConnection est le module de bas niveau tandis que le PasswordReminder est celui de haut niveau, mais selon la définition du D de SOLID, qui indique de Depend on abstraction, not on concretions. Ce fragment de code ci-dessus enfreint ce principe, car la classe PasswordReminder est forcée de dépendre de la classe MySQLConnection.

      Plus tard, si vous venez à devoir modifier le moteur de la base de données, vous aurez également à modifier la classe PasswordReminder, ce qui enfreindrait l’open-close principle.

      La classe PasswordReminder ne doit pas se préoccuper de la base de données utilisée par votre application. Pour résoudre ces problèmes, vous pouvez coder sur une interface étant donné que les modules de haut et de bas niveau doivent dépendre de l’abstraction :

      interface DBConnectionInterface
      {
          public function connect();
      }
      

      L’interface a une méthode de connexion et la classe MySQLConnection implémente cette interface. De plus, au lieu de directement indiquer la classe MySQLConnection dans le constructeur du PasswordReminder, vous devriez plutôt indiquer le type de la DBConnectionInterface. Et, quel que soit le type de base de données utilisée par votre application, la classe PasswordReminder pourra se connecter à la base de données sans aucun problème tout en respectant le principe ouvert-fermé.

      class MySQLConnection implements DBConnectionInterface
      {
          public function connect()
          {
              // handle the database connection
              return 'Database connection';
          }
      }
      
      class PasswordReminder
      {
          private $dbConnection;
      
          public function __construct(DBConnectionInterface $dbConnection)
          {
              $this->dbConnection = $dbConnection;
          }
      }
      

      Ce code établit que les modules génériques et détaillés dépendent de l’abstraction.

      Conclusion

      Au cours de cet article, nous vous avons présenté les cinq principes du code SOLID. Les projets qui adhèrent aux principes SOLID peuvent être partagés avec des collaborateurs, étendus, modifiés, testés et remaniés plus facilement.

      Continuez votre apprentissage en consultant d’autres pratiques de développement de logiciels agiles et adaptatifs.



      Source link