One place for hosting & domains

      SOLID

      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

      SOLID: Die ersten 5 Prinzipien des objektorientierten Designs


      Einführung

      SOLID ist ein Akronym für die ersten fünf Prinzipien des objektorientierten Designs (OOD) von Robert C. Martin (auch bekannt als Onkel Bob).

      Anmerkung: Obwohl diese Prinzipien auf verschiedene Programmiersprachen angewendet werden können, wird der in diesem Artikel enthaltene Beispielcode PHP verwendet.

      Diese Prinzipien legen Praktiken fest, die sich für die Entwicklung von Software mit Überlegungen zur Aufrechterhaltung und Erweiterung eignen, wenn das Projekt wächst. Die Übernahme dieser Praktiken kann auch zur Vermeidung von Code Smells, Refactoring von Code und agiler oder adaptiver Softwareentwicklung beitragen.

      SOLID steht für:

      In diesem Artikel werden Sie jedes Prinzip einzeln kennenlernen, um zu verstehen, wie SOLID Ihnen dabei helfen kann, ein besserer Entwickler zu werden.

      Single-Responsibility-Prinzip

      Das Single-Responsibility-Prinzip (SRP) besagt:

      Eine Klasse sollte einen und nur einen Grund haben, sich zu ändern, d. h. eine Klasse sollte nur eine Aufgabe haben.

      Betrachten Sie beispielsweise eine Anwendung, die eine Sammlung von Formen – Kreise und Quadrate – nimmt und die Summe der Fläche aller Formen in der Sammlung berechnet.

      Erstellen Sie zunächst die Formklassen und lassen Sie die Konstruktoren die erforderlichen Parameter einrichten.

      Für Quadrate müssen Sie die length einer Seite kennen:

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

      Für Kreise müssen Sie den radius kennen:

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

      Erstellen Sie anschließend die Klasse AreaCalculator und schreiben Sie dann die Logik, um die Fläche aller bereitgestellten Formen zu summieren. Der Flächeninhalt eines Quadrats wird durch die Länge zum Quadrat berechnet. Der Flächeninhalt eines Kreises wird durch Pi mal Radius zum Quadrat berechnet.

      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(),
                '',
            ]);
          }
      }
      

      Um die Klasse AreaCalculator zu verwenden, müssen Sie die Klasse instanziieren und ein Array von Formen übergeben und die Ausgabe am Ende der Seite anzeigen.

      Hier ist ein Beispiel mit einer Sammlung von drei Formen:

      • Ein Kreis mit einem Radius von 2
      • Ein Quadrat mit einer Länge von 5
      • Ein zweites Quadrat mit einer Länge von 6
      $shapes = [
        new Circle(2),
        new Square(5),
        new Square(6),
      ];
      
      $areas = new AreaCalculator($shapes);
      
      echo $areas->output();
      

      Das Problem mit der Ausgabemethode ist, dass der AreaCalculator die Logik zur Ausgabe der Daten bearbeitet.

      Bedenken Sie ein Szenario, in dem die Ausgabe in ein anderes Format wie JSON konvertiert werden soll.

      Die gesamte Logik würde von der Klasse AreaCalculator bearbeitet werden. Dies würde gegen das Single-Responsibility-Prinzip verstoßen. Die Klasse AreaCalculator sollte nur mit der Summe der Flächen der bereitgestellten Formen befasst sein. Sie sollte sich nicht damit befassen, ob der Benutzer JSON oder HTML wünscht.

      Um dies zu beheben, können Sie eine separate Klasse SumCalculatorOutputter erstellen und diese neue Klasse verwenden, um die Logik zu bearbeiten, die Sie für die Ausgabe der Daten an den Benutzer benötigen:

      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(),
                '',
            ]);
          }
      }
      

      Die Klasse SumCalculatorOutputter würde wie folgt funktionieren:

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

      Jetzt wird die Logik, die Sie zur Ausgabe der Daten an den Benutzer benötigen, von der Klasse SumCalculatorOutputter bearbeitet.

      Das erfüllt das Single-Responsibility-Prinzip.

      Open-Closed-Prinzip

      Das Open-Closed-Prinzip (S.R.P.) besagt:

      Objekte oder Entitäten sollten offen für Erweiterungen, aber geschlossen für Änderungen sein.

      Das bedeutet, dass eine Klasse erweiterbar sein sollte, ohne die Klasse selbst zu modifizieren.

      Gehen wir noch einmal auf die Klasse AreaCalculator ein und konzentrieren uns auf die Methode 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);
          }
      }
      

      Bedenken Sie ein Szenario, in dem der Benutzer die Summe sum zusätzlicher Formen wie Dreiecke, Fünfecke, Sechsecke usw. wünscht. Sie müssten diese Datei ständig bearbeiten und weitere if/else-Blöcke hinzufügen. Das würde das Open-Closed-Prinzip verletzen.

      Eine Möglichkeit, diese Methode sum zu verbessern, besteht darin, die Logik zur Berechnung der Fläche jeder Form aus der Klassenmethode AreaCalculator zu entfernen und sie an die Klasse jeder Form anzuhängen.

      Hier ist die in Square definierte Methode area:

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

      Und hier ist die in Circle definierte Methode area:

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

      Die Methode sum für AreaCalculator kann dann umgeschrieben werden als:

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

      Jetzt können Sie eine andere Formklasse erstellen und diese bei der Berechnung der Summe übergeben, ohne den Code zu verändern.

      Es ergibt sich jedoch ein weiteres Problem. Woher wissen Sie, dass das an den AreaCalculator übergebene Objekt tatsächlich eine Form ist oder ob die Form eine Methode namens area aufweist?

      Die Codierung auf eine Schnittstelle ist ein integraler Bestandteil von SOLID.

      Erstellen Sie ein ShapeInterface, das area unterstützt:

      interface ShapeInterface
      {
          public function area();
      }
      

      Ändern Sie Ihre Formklassen, um das ShapeInterface mit implement zu implementieren.

      Hier ist die Aktualisierung für Square:

      class Square implements ShapeInterface
      {
          // ...
      }
      

      Und hier ist die Aktualisierung für Circle:

      class Circle implements ShapeInterface
      {
          // ...
      }
      

      In der Methode sum für AreaCalculator können Sie überprüfen, ob die bereitgestellten Formen tatsächlich Instanzen des ShapeInterface sind; andernfalls verwenden Sie „throw“ für eine Ausnahme:

       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);
          }
      }
      

      Damit ist das Open-Closed-Prinzip erfüllt.

      Liskovsches Substitutionsprinzip

      Das Liskovsche Substitutionsprinzip besagt:

      Lassen Sie q(x) eine Eigenschaft sein, die für Objekte x von Typ T beweisbar ist. Dann soll q(y) für Objekte y von Typ S beweisbar sein, wobei S ein Untertyp von T ist.

      Das bedeutet, dass jede Unterklasse oder abgeleitete Klasse für ihre Basis- oder übergeordnete Klasse ersetzbar sein sollte.

      Bedenken Sie, aufbauend auf dem Beispiel der Klasse AreaCalculator, eine neue Klasse VolumeCalculator, die die Klasse AreaCalculator erweitert:

      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];
          }
      }
      

      Erinnern Sie sich daran, dass die Klasse SumCalculatorOutputter dem ähnelt:

      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(),
                  ''
              ));
          }
      }
      

      Wenn Sie versuchen würden, ein Beispiel wie dieses auszuführen:

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

      Wenn Sie die Methode HTML auf dem Objekt $output2 aufrufen, erhalten Sie einen Fehler E_NOTICE, der Sie über eine Array-zu-String-Konvertierung informiert.

      Um dies zu beheben, geben Sie anstelle der Rückgabe eines Arrays aus der Summenmethode der Klasse VolumeCalculator $summedData zurück:

      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;
          }
      }
      

      Das $summedData können ein Float, Double oder Integer sein.

      Damit ist das Liskovsche Substitutionsprinzip erfüllt.

      Das Interface-Segregation-Prinzip

      Das Interface-Segregation-Prinzip besagt:

      Ein Client sollte nie gezwungen werden, eine Schnittstelle zu implementieren, die er nicht verwendet, oder Clients sollten nicht gezwungen werden, von Methoden abzuhängen, die sie nicht verwenden.

      Weiterhin aufbauend auf dem vorherigen Beispiel ShapeInterface, müssen Sie die neuen dreidimensionalen Formen Cuboid und Spheroid unterstützen, und diese Formen müssen auch das Volumen berechnen.

      Bedenken wir, was passieren würde, wenn Sie das ShapeInterface modifizieren würden, um einen weiteren Vertrag hinzuzufügen:

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

      Nun muss jede Form, die Sie erstellen, die Methode volume implementieren, aber Sie wissen, dass Quadrate flache Formen sind und kein Volumen haben, also würde diese Schnittstelle die Klasse Square zwingen, eine Methode zu implementieren, die sie nicht braucht.

      Dies würde das Interface-Segregation-Prinzip verletzen. Stattdessen könnten Sie eine andere Schnittstelle namens ThreeDimensionalShapeInterface erstellen, die den Vertrag volume hat und dreidimensionale Formen können diese Schnittstelle implementieren:

      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
          }
      }
      

      Dies ist ein wesentlich besserer Ansatz, aber ein Fallstrick, auf den Sie achten müssen, wenn Sie diese Schnittstellen mit Typ-Hinweisen versehen. Anstatt ein ShapeInterface oder ein ThreeDimensionalShapeInterface zu verwenden, können Sie eine andere Schnittstelle erstellen, vielleicht ManageShapeInterface, und diese sowohl für die flachen als auch für die dreidimensionalen Formen implementieren.

      Auf diese Weise können Sie eine einzige API für die Verwaltung der Formen haben:

      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();
          }
      }
      

      In der Klasse AreaCalculator können Sie den Aufruf für die Methode area durch calculate ersetzen und außerdem überprüfen, ob das Objekt eine Instanz des ManageShapeInterface und nicht des ShapeInterface ist.

      Das erfüllt das Interface-Segregation-Prinzip.

      Das Dependency-Inversion-Prinzip

      Das Dependency-Inversion-Prinzip besagt:

      Entitäten müssen von Abstraktionen abhängen, nicht von Konkretionen. Es besagt, dass das Modul auf hoher Ebene nicht vom Modul auf niedriger Ebene abhängen darf, sondern diese von Abstraktionen abhängen sollten.

      Dieses Prinzip ermöglicht die Entkopplung.

      Hier ist ein Beispiel für einen PasswordReminder der sich mit einer MySQL-Datenbank verbindet:

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

      Zuerst ist die MySQLConnection das Modul auf niedriger Ebene, während der PasswordReminder auf hoher Ebene angesiedelt ist, aber gemäß der Definition von D in SOLID, die besagt, von der Abstraktion abzuhängen und nicht von Konkretionen. Dieses obige Snippet verletzt dieses Prinzip, da die Klasse PasswordReminder gezwungen wird, von der Klasse MySQLConnection abzuhängen.

      Wenn Sie später die Datenbank-Engine ändern würden, müssten Sie auch die Klasse PasswordReminder bearbeiten, und das würde das Open-Close-Prinzip verletzen.

      Die Klasse PasswordReminder sollte sich nicht darum kümmern, welche Datenbank Ihre Anwendung verwendet. Um diese Probleme zu beheben, können Sie an eine Schnittstelle kodieren, da Module auf hoher Ebene und niedriger Ebene von der Abstraktion abhängen sollten:

      interface DBConnectionInterface
      {
          public function connect();
      }
      

      Die Schnittstelle hat eine Verbindungsmethode und die Klasse MySQLConnection implementiert diese Schnittstelle. Anstatt die Klasse MySQLConnection im Konstruktor von PasswordReminder, direkt zu typisieren, geben Sie stattdessen das DBConnectionInterface an, und unabhängig davon, welchen Datenbanktyp Ihre Anwendung verwendet, kann die Klasse PasswordReminder ohne Probleme eine Verbindung zur Datenbank herstellen und das Open-Close-Prinzip wird nicht verletzt.

      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;
          }
      }
      

      Dieser Code verdeutlicht, dass sowohl die Module auf hoher Ebene als auch auf niedriger Ebene von der Abstraktion abhängen.

      Zusammenfassung

      In diesem Artikel wurden Ihnen die fünf Prinzipien von SOLID Code vorgestellt. Projekte, die sich an die SOLID-Prinzipien halten, können mit weniger Komplikationen mit anderen Mitarbeitern geteilt, erweitert, modifiziert, getestet und refraktorisiert werden.

      Lernen Sie weiter, indem Sie über andere Praktiken für die Agile und Adaptive Softwareentwicklung lesen.



      Source link

      SOLID: Los primeros 5 principios del diseño orientado a objetos


      Introducción

      SOLID es un acrónimo de los primeros cinco principios del diseño orientado a objetos (OOD) de Robert C. Martin (también conocido como el Tío Bob).

      Nota: Aunque estos principios pueden aplicarse a varios lenguajes de programación, el código de muestra que se incluye en este artículo usará PHP.

      Estos principios establecen prácticas que se prestan al desarrollo de software con consideraciones para su mantenimiento y expansión a medida que el proyecto se amplía. Adoptar estas prácticas también puede ayudar a evitar los aromas de código, refactorizar el código y aprender sobre el desarrollo ágil y adaptativo de software.

      SOLID representa:

      En este artículo, se le presentará cada principio por separado para comprender la forma en que SOLID puede ayudarlo a ser un mejor desarrollador.

      Principio de responsabilidad única

      El principio de responsabilidad única (SRP) establece:

      Una clase debe tener una y una sola razón para cambiar, lo que significa que una clase debe tener solo un trabajo.

      Por ejemplo, considere una aplicación que toma una colección de formas, entre círculos y cuadrados, y calcula la suma del área de todas las formas de la colección.

      Primero, cree las clases de forma y haga que los constructores configuren los parámetros requeridos.

      Para las cuadrados, deberá saber la longitud de un lado:

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

      Para los círculos, deberá saber el radio:

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

      A continuación, cree la clase AreaCalculator y luego escriba la lógica para sumar las áreas de todas las formas proporcionadas. El área de un cuadrado se calcula por longitud al cuadrado. El área de un círculo se calcula mediante pi por el radio al cuadrado.

      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(),
                '',
            ]);
          }
      }
      

      Para usar la clase AreaCalculator, deberá crear una instancia de la clase y pasar una matriz de formas para mostrar el resultado en la parte inferior de la página.

      A continuación, se muestra un ejemplo con una colección de tres formas:

      • un círculo con un radio de 2
      • un cuadrado con una longitud de 5
      • un segundo cuadrado con una longitud de 6
      $shapes = [
        new Circle(2),
        new Square(5),
        new Square(6),
      ];
      
      $areas = new AreaCalculator($shapes);
      
      echo $areas->output();
      

      El problema con el método de salida es que AreaCalculator maneja la lógica para generar los datos.

      Considere un escenario en el que el resultado debe convertirse a otro formato como JSON.

      La clase AreaCalculator manejaría toda la lógica. Esto violaría el principio de responsabilidad única. La clase AreaCalculator solo debe ocuparse de la suma de las áreas de las formas proporcionadas. No debería importar si el usuario desea JSON o HTML.

      Para abordar esto, puede crear una clase SumCalculatorOutputter por separado y usarla para manejar la lógica que necesita para mostrar los datos al usuario:

      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 clase SumCalculatorOutputter funcionaría así:

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

      Ahora, la clase SumCalculatorOutputter maneja cualquier lógica que necesite para enviar los datos al usuario.

      Eso cumple con el principio de responsabilidad única.

      Principio abierto-cerrado

      Principio abierto-cerrado (S.R.P.) establece:

      Los objetos o entidades deben estar abiertos por extensión, pero cerrados por modificación.

      Esto significa que una clase debe ser ampliable sin modificar la clase en sí.

      Volvamos a ver la clase AreaCalculator y enfoquémonos en el método 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);
          }
      }
      

      Considere un escenario en el que el usuario desea la sum de formas adicionales como triángulos, pentágonos, hexágonos, etc. Tendría que editar constantemente este archivo y añadir más bloques if/else. Eso violaría el principio abierto-cerrado.

      Una forma de mejorar el método sum es eliminar la lógica para calcular el área de cada forma fuera del método de la clase AreaCalculator y adjuntarlo a la clase de cada forma.

      A continuación, se muestra area definido en Square:

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

      Y aquí es el método area definido en Circle:

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

      El método sum para AreaCalculator puede reescribirse así:

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

      Ahora, puede crear otra clase de forma y pasarla al calcular la suma sin romper el código.

      Sin embargo, ahora surge otro problema: ¿Cómo sabe que el objeto pasado a AreaCalculator es realmente una forma o si la forma tiene un método llamado area?

      La codificación de una interface es una parte integral de SOLID.

      Cree un ShapeInterface que sea compatible con area:

      interface ShapeInterface
      {
          public function area();
      }
      

      Modifique sus clases de forma para implement el ShapeInterface.

      A continuación, se muestra la actualización a Square:

      class Square implements ShapeInterface
      {
          // ...
      }
      

      Y aquí está la actualización a Circle:

      class Circle implements ShapeInterface
      {
          // ...
      }
      

      En el método sum para AreaCalculator, puede verificar si las formas proporcionadas son realmente instancias de ShapeInterface; de lo contrario, lanzamos una excepción:

       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);
          }
      }
      

      Eso cumple con el principio abierto-cerrado.

      Principio de sustitución de Liskov

      El principio de sustitución de Liskov establece:

      Digamos que q(x) sea una propiedad demostrable sobre objetos de x, de tipo T. Entonces, q(y) debe ser demostrable para los objetos y, de tipo S, donde S es un subtipo de T.

      Esto significa que cada subclase o clase derivada debe ser sustituible por su clase base o clase principal.

      A partir de la clase AreaCalculator mostrada como ejemplo, considere una nueva clase VolumeCalculator que extiende la clase 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];
          }
      }
      

      Recuerde que la clase SumCalculatorOutputter se asemeja a esto:

      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 intenta ejecutar un ejemplo como este:

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

      Cuando invoca el método HTML en el objeto $output2, obtendrá un error E_NOTICE que le informará de conversión de matriz a cadena.

      Para solucionar esto, en vez de devolver una matriz desde el método sum de la clase VolumeCalculator, devuelva $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;
          }
      }
      

      $summedData puede ser float, double o integer.

      Eso cumple con el principio de sustitución de Liskov.

      Principio de segregación de interfaz

      El principio de segregación de interfaz establece:

      Un cliente nunca debe ser forzado a implementar una interfaz que no usan ni los clientes no deben ser forzados a depender de métodos que no usan.

      Siguiendo con el ejemplo anterior de ShapeInterface, tendrá que admitir las nuevas formas tridimensionales de Cuboid y Spheroid, y estas formas también tendrán que calcular el volumen.

      Consideraremos lo que sucedería si modificara ShapeInterface para añadir otro contrato:

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

      Ahora, cualquier forma que cree debe implementar el método volume, pero sabemos que las cuadrados son formas planas y que no tienen volumen, por lo que esta interfaz forzaría a la clase Square a implementar un método que no usa.

      Esto violaría el principio de segregación de interfaz. En su lugar, podría crear otra interfaz llamada ThreeDimensionalShapeInterface que tiene el contrato de volume y las formas tridimensionales pueden implementar esta interfaz:

      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
          }
      }
      

      Este es un enfoque mucho mejor, pero hay que tener cuidado cuando se trata de escribir estas interfaces En vez de usar un ShapeInterface o un ThreeDimensionalShapeInterface, puede crear otra interfaz, quizá ManageShapeInterface e implementarla en las formas planas y en las tridimensionales.

      De esta manera, puede tener una sola API para administrar las formas:

      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();
          }
      }
      

      Ahora en la clase AreaCalculator, puede sustituir la invocación al método area con calculate y también verificar si el objeto es una instancia de ManageShapeInterface y no de ShapeInterface.

      Eso cumple con el principio de segregación de interfaz.

      Principio de inversión de dependencia

      El principio de inversión de dependencia establece:

      Las entidades deben depender de abstracciones, no de concreciones. Indica que el módulo de alto nivel no debe depender del módulo de bajo nivel, sino que deben depender de las abstracciones.

      Este principio permite el desacoplamiento.

      A continuación, se muestra un ejemplo de un PasswordReminder que se conecta a una base de datos de 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;
          }
      }
      

      Primero, MySQLConnection es el módulo de bajo nivel mientras que PasswordReminder es de alto nivel, pero según la definición de D en SOLID, que establece que Depende de la abstracción y no de las concreciones. Este fragmento de código anterior viola este principio, ya que se está forzando a la clasevPasswordReminder a depender de la clase MySQLConnection.

      Si más adelante cambiara el motor de la base de datos, también tendría que editar la clase PasswordReminder, y esto violaría el principio abierto-cerrado.

      A la clase PasswordReminder no le debe importar qué base de datos usa su aplicación. Para solucionar estos problemas, se puede codificar a una interfaz, ya que los módulos de alto nivel y bajo nivel deben depender de la abstracción:

      interface DBConnectionInterface
      {
          public function connect();
      }
      

      La interfaz tiene un método connect y la clase MySQLConnection implementa esta interfaz. Además, en lugar de escribir directamente la clase MySQLConnection en el constructor del PasswordReminder, se indica la clase DBConnectionInterface y, sin importar el tipo de base de datos que utilice su aplicación, la clase PasswordReminder puede conectarse sin ningún problema a la base de datos y no se viola el principio abierto-cerrado.

      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;
          }
      }
      

      Este código establece que los módulos de alto nivel y los de bajo nivel dependen de la abstracción.

      Conclusión

      En este artículo, se le presentaron los cinco principios del código SOLID. Los proyectos que se adhieren a los principios SOLID pueden compartirse con los colaboradores, ampliarse, modificarse, probarse y refactorizarse con menos complicaciones.

      Continúe aprendiendo leyendo sobre otras prácticas para el desarrollo ágil y adaptativo de software.



      Source link