I. Introduction▲
Le livre Design Patterns m'a introduit à la notion que de tels concepts existent. À l'époque, j'apprenais encore l'Orienté Objet (OO), de nombreux concepts du livre me semblaient donc difficiles à saisir. Cependant, en devenant plus familier des concepts OO (en particulier l'utilisation des interfaces et l'héritage), j'ai commencé à comprendre la valeur réelle des motifs de conception. En tant que développeur d'applications, vous pouvez très bien faire toute votre carrière sans connaître le nom d'un seul des motifs, ou encore comment et quand ils sont utilisés. Néanmoins, j'estime qu'une bonne connaissance pratique de ces motifs (ainsi que ceux présentés dans l'article Cinq motifs classiques de conception pour PHP), vous permet de faire deux choses :
- réduire le temps des communications ;
- réduire la fréquence des leçons difficiles.
Dans le premier cas : Si vous connaissez des motifs de conception, vous êtes en mesure de construire des applications OO robustes plus rapidement. Lorsque votre équipe entière connaît les divers motifs, vous pouvez alors avoir des conversations très rapides. Vous n'avez plus besoin de parler de toutes les classes que vous utiliseriez ici ou là ; au lieu de cela, vous pouvez parler en termes de motifs les uns aux autres. « Eh bien, j'utilise un Singleton ici, puis un Itérateur pour parcourir une collection de mes objets et… » est bien, bien plus rapide que de lister toutes les classes, méthodes et interfaces qui fondent ces motifs. À elle seule, cette efficacité de communication peut valoir le temps nécessaire à quelques sessions pour étudier les motifs ensemble, en équipe.
Dans l'autre cas : Chaque motif décrit une méthode reconnue pour résoudre un problème standard. Ainsi, vous n'avez plus besoin de vous soucier de l'exactitude de votre conception, tant que vous avez choisi le motif qui répond à vos besoins.
I-A. Pièges▲
En anglais, il existe le proverbe suivant : « Lorsque vous portez un marteau, tout semble être un clou. » Lorsque vous apprenez un motif que vous aimez bien, vous pourriez essayer de l'utiliser à tort et à travers, même dans des situations inadaptées. Souvenez-vous de considérer les objectifs des motifs que vous apprenez, et de ne pas forcer l'utilisation de motifs dans des portions de votre application pour le simple fait de les utiliser.
Cet article couvre cinq motifs que vous pouvez utiliser pour améliorer votre code PHP. Chaque motif couvre un scénario bien précis. Le code PHP des motifs est disponible dans la section Téléchargement.
I-B. Prérequis▲
Pour tirer le meilleur parti de cet article et pour utiliser les exemples, installez ceci sur votre machine :
- PHP 5 ou ultérieur (cet article a été écrit avec PHP 5.2.4) ;
- un programme de décompression tel que Winzip (pour décompresser le code de l'archive).
Bien que vous n'ayez pas absolument besoin d'un éditeur autre qu'un éditeur de texte, il me semble réellement utile d'avoir une coloration syntaxique et une correction syntaxique automatiques. Les exemples de cet article sont écrits à l'aide d'Eclipse PHP Development Tools (PDT).
II. Le motif Adapter▲
Utilisez le motif Adapter (adaptateur) lorsque vous avez besoin de convertir un objet d'un type vers un autre type. Les développeurs s'en chargent par du code d'assignation comme indiqué en Listing 1. Le motif Adapter est un bon moyen pour nettoyer ce code et pour réutiliser les assignations à d'autres endroits. Il cache également le code d'assignation, ce qui peut grandement simplifier les choses si modifiez le format des données en même temps.
class AddressDisplay
{
private
$addressType
;
private
$addressText
;
public
function
setAddressType($addressType
)
{
$this
->
addressType =
$addressType
;
}
public
function
getAddressType()
{
return
$this
->
addressType;
}
public
function
setAddressText($addressText
)
{
$this
->
addressText =
$addressText
;
}
public
function
getAddressText()
{
return
$this
->
addressText;
}
}
class
EmailAddress
{
private
$emailAddress
;
public
function
getEmailAddress()
{
return
$this
->
emailAddress;
}
public
function
setEmailAddress($address
)
{
$this
->
emailAddress =
$address
;
}
}
$emailAddress
=
new
EmailAddress();
/* Remplir l'objet EmailAddress */
$address
=
new
AddressDisplay();
/* Voici le code d'assignation, où j'assigne les valeurs
d'un objet à l'autre... */
$address
->
setAddressType("email"
);
$address
->
setAddressText($emailAddress
->
getEmailAddress());
Cet exemple utilise un objet AddressDisplay pour afficher une adresse à un utilisateur. Cet objet a deux parties : le type de l'adresse et une chaîne correctement formée de l'adresse.
Après avoir implémenté le motif (voir en Listing 2), le script PHP n'a plus besoin de savoir dans le détail comment l'objet EmailAddress est converti en objet AddressDisplay. C'est une bonne chose, surtout si l'objet DisplayAddress est modifié ou si les règles de passage d'un objet à l'autre changent. Rappelez-vous que l'un des avantages de concevoir son code de façon modulaire, est de tirer parti de n'avoir que peu de modifications à faire lorsque quelque chose change dans le domaine des traitements ou lorsque vous devez ajouter une fonctionnalité au programme. Pensez-y même lorsque vous effectuez des tâches triviales comme assigner des propriétés d'un objet à un autre.
class EmailAddressDisplayAdapter extends
AddressDisplay
{
public
function
__construct
($emailAddr
)
{
$this
->
setAddressType("email"
);
$this
->
setAddressText($emailAddr
->
getEmailAddress());
}
}
$email
=
new
EmailAddress();
$email
->
setEmailAddress("user@example.com"
);
$address
=
new
EmailAddressDisplayAdapter($email
);
echo($address
->
getAddressType() .
"
\n
"
) ;
echo($address
->
getAddressText());
II-A. Méthode alternative▲
Une méthode alternative d'écrire un Adapter (préférée par certaines personnes) est d'implémenter une interface pour adapter le comportement, plutôt qu'étendre une classe. C'est une méthode très propre de créer un adaptateur et qui n'a pas les inconvénients d'étendre une classe. Cependant, l'un des inconvénients d'utiliser une interface est que vous devez ajouter l'implémentation à la classe adaptateur, tel qu'en Figure 1 :
III. Le motif Iterator▲
Le motif Iterator (itérateur) propose une solution pour parcourir une collection ou un tableau d'objets. C'est particulièrement utile si vous voulez parcourir différents types d'objets dans la collection.
Regardez l'exemple d'adresse e-mail et postale dans le Listing 1. Avant d'ajouter un motif Iterator, si vous parcourez les adresses d'une personne, vous pourriez parcourir les adresses postales et les afficher, puis parcourir les adresses e-mail et les afficher également, puis parcourir les adresses de messagerie instantanée et les montrer à leur tour. Convenez que c'est très laid !
Au lieu de cela, en implémentant un itérateur, tout ce que vous avez à faire est d'appeler while($itr->hasNext()) et de s'occuper de l'élément renvoyé par $itr->next(). Un exemple de l'un des itérateurs est montré en Listing 3. Un Iterator est puissant, car vous pouvez ajouter de nouveaux types d'éléments à parcourir sans devoir changer le code qui les parcourt. Dans l'exemple Person, vous pourriez notamment ajouter un tableau d'adresses de messagerie instantanée ; en actualisant uniquement l'Iterator, vous n'avez besoin de changer aucun code qui affiche en boucle les adresses.
class PersonAddressIterator implements
AddressIterator
{
private
$emailAddresses
;
private
$physicalAddresses
;
private
$position
;
public
function
__construct
($emailAddresses
)
{
$this
->
emailAddresses =
$emailAddresses
;
$this
->
position =
0
;
}
public
function
hasNext()
{
if
($this
->
position >=
count($this
->
emailAddresses) ||
$this
->
emailAddresses[
$this
->
position]
==
null
) {
return
false
;
}
else
{
return
true
;
}
}
public
function
next()
{
$item
=
$this
->
emailAddresses[
$this
->
position];
$this
->
position =
$this
->
position +
1
;
return
$item
;
}
}
Si l'objet Person est modifié pour renvoyer une instance de l'interface AddressIterator, le code applicatif qui utilise l'itérateur n'a pas besoin d'être modifié si l'implémentation est étendue au parcours d'objets supplémentaires. Vous pouvez utiliser un itérateur composé qui encapsule les itérateurs qui parcourent tous les types d'adresses comme celui montré en Listing 3. Un exemple est disponible (cf. les téléchargements).
Note du traducteur : la SPL permet également de construire facilement un motif Iterator en PHPStandard PHP Library (SPL).
IV. Le motif Decorator▲
Regardez le code en Listing 4. Son objectif est d'ajouter quelques fonctionnalités à une voiture pour un site « Construisez votre propre voiture ». Chaque modèle de voiture a davantage de fonctionnalités et un coût associé. Avec seulement deux modèles, il serait trivial d'ajouter ces fonctionnalités avec des instructions if/else. Cependant, dès qu'un nouveau modèle survient, vous auriez à retourner dans le code et à vous assurer qu'il fonctionne correctement pour le nouveau modèle.
require('
classes.php
'
);
$auto
=
new Automobile();
$model
=
new BaseAutomobileModel();
$model
=
new SportAutomobileModel($model
);
$model
=
new TouringAutomobileModel($model
);
$auto
->
setModel($model
);
$auto
->
printDescription();
Voici le motif Decorator (décorateur), qui vous permet d'ajouter des fonctionnalités à AutomobileModel d'une manière propre et organisée. Chaque classe reste concernée uniquement par son prix, par ses options et par la manière dont elle est ajoutée au modèle de base.
Un avantage du motif Decorator est que vous pouvez aisément brancher plus d'un décorateur à la fois à la base.
Si vous avez beaucoup travaillé avec des flux, vous avez utilisé un Decorator. La plupart des conduits de flux, comme un flux de sortie, sont des décorateurs qui prennent un flux d'entrée et qui le décorent en ajoutant des fonctionnalités (comme prendre des fichiers comme point d'entrée, ou bien une mémoire tampon, etc.).
V. Le motif Delegate▲
Le motif Delegate (délégué) donne une méthode de délégation de comportement d'après divers critères. Regardez le code en Listing 5. Il contient diverses conditions. Le code choisit le bon type d'objet pour traiter la requête en fonction de la condition.
pkg =
new Package("
Heavy Package
"
);
$pkg
->
setWeight(100
);
if ($pkg
->
getWeight() >
99
)
{
echo("
Envoi de
"
.
$pkg
->
getDescription() .
"
par train.
"
);
}
else {
echo("
Envoi de
"
.
$pkg
->
getDescription() .
"
par camion.
"
);
}
Avec un motif Delegate, un objet internalise cette étape de transmission en affectant une référence interne lorsqu'une méthode est appelée, comme useRail() dans le Listing 6. C'est particulièrement pratique si les critères changent pour traiter différents paquetages ou si on ajoute une nouvelle méthode d'envoi.
require_once('
classes.php
'
);
$pkg
=
new Package("
Heavy Package
"
);
$pkg
->
setWeight(100
);
$shipper
=
new ShippingDelegate();
if ($pkg
->
getWeight() >
99
)
{
$shipper
->
useRail();
}
$shipper
->
deliver($pkg
);
Le motif Delegate offre l'avantage que le comportement puisse changer dynamiquement en appelant useRail() ou useTruck() pour changer la classe de traitement.
VI. Le motif State▲
Le motif State (état) est similaire au motif Command mais l'intention est très différente. Voyez le code ci-dessous :
class Robot
{
private
$state
;
public
function
powerUp()
{
if
(strcmp($state
,
"poweredUp"
) ==
0
)
{
echo("Déjà allumé...
\n
"
);
/* Implémentation... */
}
else
if
( strcmp($state
,
"powereddown"
) ==
0
) {
echo("Allumage...
\n
"
);
/* Implémentation... */
}
}
public
function
powerDown()
{
if
(strcmp($state
,
"poweredUp"
) ==
0
)
{
echo("Allumage...
\n
"
);
/* Implémentation... */
}
else
if
( strcmp($state
,
"powereddown"
) ==
0
) {
echo("Déjà allumé...
\n
"
);
/* Implémentation... */
}
}
/* etc... */
}
Dans ce code, le code PHP représente un système d'exploitation pour un puissant robot qui devient une voiture. Le robot peut être allumé, éteint, devenir un robot quand c'est un véhicule et vice versa. Le code est correct pour le moment, mais vous voyez qu'il peut devenir complexe si l'une des règles change ou si un nouvel état entre en jeu.
Regardez maintenant le Listing 8, qui a la même logique pour traiter les états du robot, mais qui cette fois met la logique dans le motif State. Le code du Listing 8 fait la même chose que le code original, mais la logique de gestion des états a été mise dans un objet pour chacun d'eux. Afin d'illustrer les avantages à utiliser un motif State, imaginez qu'après un moment, les robots découvrent qu'ils ne devaient pas d'éteindre lorsqu'ils sont en mode robot. En fait, s'ils s'éteignent, ils doivent d'abord se changer en véhicules. S'ils sont déjà en mode véhicule, le robot s'éteint simplement. Avec le motif State, les changements sont très simples.
$robot
=
new Robot();
echo("
\n
"
);
$robot
->
powerUp();
echo("
\n
"
);
$robot
->
turnIntoRobot();
echo("
\n
"
);
$robot
->
turnIntoRobot();
/* Celui-ci me donnera simplement un message */
echo("
\n
"
);
$robot
->
turnIntoVehicle();
echo("
\n
"
);
class NormalRobotState implements
RobotState
{
private
$robot
;
public
function
__construct
($robot
)
{
$this
->
robot =
$robot
;
}
public
function
powerUp()
{
/* implémentation... */
}
public
function
powerDown()
{
/* Changer d'abord en véhicule */
$this
->
robot->
setState(new
VehicleRobotState($this
->
robot));
$this
->
robot->
powerDown();
}
public
function
turnIntoVehicle()
{
/* implémentation... */
}
public
function
turnIntoRobot()
{
/* implémentation... */
}
}
Une chose qui n'est pas évidente en regardant la Figure 6 est que chaque objet du motif State a une référence à l'objet de contexte (le robot), tel que chaque objet puisse changer son état dans l'objet approprié.
VII. Résumé▲
Utiliser les motifs de conception dans votre code PHP est une manière de le rendre plus lisible et maintenable. En utilisant des motifs établis, vous tirez parti de solutions standardisées qui permettent à d'autres développeurs d'une équipe de comprendre les objectifs de votre code. Cela vous permet également de bénéficier du travail effectué par d'autres développeurs, ainsi vous n'avez pas à apprendre les pénibles leçons des idées de conception qui ne fonctionnent pas.