Découvrez le principe "Ouvert/Fermé"
Le second principe SOLID est l’"Open/Closed Principle" (OCP) : les classes d’un projet devraient être ouvertes à l’extension, mais fermées à la modification.
Mais de quoi s’agit-il ? :euh:
L’idée de fond derrière ce principe est la suivante : ajouter de nouvelles fonctionnalités ne devrait pas casser les fonctionnalités déjà existantes.
Une classe est considérée comme ouverte s’il est possible de l’étendre et d’en changer le comportement.
En PHP, si une classe n’est pas final
, vous pouvez l’étendre et donc hériter de ses propriétés, fonctions et constantes publiques.
<?php
class A
{
public function helloWorld()
{
return 'Hello World';
}
}
class B extends A
{
// vide
}
$b = new B();
$b->helloWorld(); // 'Hello World'
Si par contre, une classe est déclarée final
(on dit également "complète"), il n’est pas possible de l’étendre, et une erreur fatale sera levée par PHP.
<?php
final class A {}
class B extends A {}
// PHP Fatal error: Class B may not inherit from final class (A)
Enfin, il existe un type de classe considéré comme "incomplet" par défaut : il s’agit des classes abstract
(abstraites). Une classe abstract
ne peut pas être instanciée directement, elle ne peut qu’être héritée. De plus, on peut définir des fonctions abstraites qu’il faudra implémenter dans les classes dites "enfants" :
<?php
abstract class A
{
public function helloWorld()
{
return 'Hello World';
}
abstract public function warning();
}
$a = new A();
// PHP Warning: Uncaught Error: Cannot instantiate abstract class A
class B extends A
{
// vide
}
// PHP Fatal error: Class B contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (A::warning)
Est-ce que l’on peut avoir une classe qui est final
et abstract
en même temps ?
Non, et cela n’aurait pas de sens ! :lol:
Pour résumer, plutôt que de changer le comportement d’une classe existante pour l’adapter à un nouveau besoin, il vaut mieux étendre cette classe et en adapter le comportement : elle est donc ouverte à l’extension et fermée à la modification.
L’intérêt de faire cela, c’est d’éviter de casser ou d’introduire des bugs dans une application qui fonctionne correctement.
Mettez en pratique le principe OCP
Imaginons une classe qui doit calculer les coûts de transport d’une commande lors d’un achat sur une plateforme e-commerce. Ce coût peut dépendre du moyen de transport, du mode de livraison (l’option "rapide" ou au contraire "économique"), et aussi du poids des produits de la commande.
Voici à quoi la classe pourrait ressembler :
<?php
class Order
{
// eco ou rapide
private $shippingPlan;
// en kg
private $orderWeight;
// avion ou train par exemple
private $shippingMode;
/*...*/
public function getShippingCost()
{
$cost = '10';
if ($this->shippingMode == 'avion') {
$cost = $this->orderWeight * 3;
if ($this->shippingPlan == 'rapide') {
$cost = 1.3 * $cost;
}
}
if ($this->shippingMode == 'train') {
$cost = $this->orderWeight * 2;
if ($this->shippingPlan == 'rapide') {
$cost = 1.1 * $cost;
}
}
return $cost;
}
}
Si nous devions rajouter un autre mode de livraison, ou alors un tarif différent selon le nom du transporteur ou encore selon le pays, le code de cette fonction deviendrait complètement in-maintenable.
Nous allons donc rendre ce code compatible avec le principe OCP, en utilisant une nouvelle notion : le polymorphisme.
Le polymorphisme, c’est la capacité d’un langage à supporter différentes implémentations d’un même système.
Mais nous sommes en train de parler de l’héritage, en fait, non ?
Oui ! L’héritage de classe est une forme de polymorphisme.
Si l’on réfléchit au code présenté, le coût d’un envoi dépend du mode d’envoi, du poids et du véhicule utilisé... est-ce bien la responsabilité de la classe Order
de calculer cela ?
Créer un objet ShippingType
qui prend une commande en paramètre permet de :
bien séparer les responsabilités de chacun (SRP) ;
pouvoir créer un type de
ShippingType
par véhicule (OCP).
Voici le code mis à jour pour respecter les deux principes SOLID abordés :
<?php
use Order;
abstract class ShippingType
{
abstract public function getCost(Order $order);
}
class AircraftShipping extends ShippingType
{
public function getCost(Order)
{
/*...*/
}
}
class TrainShipping extends ShippingType
{
public function getCost(Order)
{
/*...*/
}
}
class Order
{
private $shippingType;
public function __construct(ShippingType)
{
$this->shippingType = $shippingType;
}
public function getShippingCost()
{
return $this->shippingType->getCost();
}
}
Qu’avons-nous fait ici ?
Tout d’abord, nous avons créé une classe abstraite ShippingType
avec la méthode abstraite getCost(Order)
.
En faisant cela, pour implémenter une nouvelle méthode de livraison, il nous suffira de créer une nouvelle classe qui étend ShippingType
.
Livraison par drone ?
<?php
use Order;
class DroneShipping extends ShippingType
{
public function getCost(Order $order)
{
/*...*/
}
}
Il suffit ensuite de sélectionner la bonne méthode d’envoi pour que la classe Order
soit "capable" de calculer le coût d’envoi de la commande. Pour cela, nous avons décidé d’injecter la classe correspondante en constructeur. En faisant cela, nous déléguons la responsabilité de ce calcul à l’implémentation de la classe ShippingType
.
Exercez-vous !
Dans le projet qui nous sert de fil directeur, vous remarquerez qu’il existe une classe appelée Music
. L’idée originale de l’auteur de ce projet était que l’application puisse supporter différents types de formats audio comme le MP3 ou le Ogg, par exemple.
Malheureusement, il ne maîtrisait pas les principes SOLID, et le code produit a quelques problèmes de conception que vous êtes maintenant capable de corriger.
Téléchargez l'archive de l'exercice et améliorez le code des classes Music
et MP3
pour qu’elles respectent le principe Open/Closed.
En résumé
Une classe, une méthode doit pouvoir être étendue de sorte à pouvoir en modifier le comportement sans en changer directement le code (ou l'implémentation).
Pour cela, nous utilisons le polymorphisme : ça peut être l'héritage ou encore la composition.
Au final, cela revient à réappliquer le premier principe que nous avions abordé (1 classe = 1 responsabilité), avec un complément fonctionnel : 1 comportement nécessite sa propre classe, sa propre fonction, et les comportements doivent pouvoir être interchangés sans problème.
Je vous attends dans le prochain chapitre où nous aborderons – entre autres – la notion d’interface. À tout de suite ! :)