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.
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 :
$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.
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é.
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.
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.
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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Bon article c’était super utile;
cependant j’ai une remarque, il y a une petite erreur là où tu parlais du principe d’Ouvert/Fermé :
Ouvert fermé Le principe ouvert/fermé (S.R.P.) précise ce qui suit :…
(S.R.P.) doit être remplacé par (O.C.P)
Merci et bonne journée.