Cinq motifs classiques de conception pour PHP

| Jack D. Herrington | Guillaume Rossolini (traducteur) |

Les motifs de conception (design patterns) sont uniquement pour les architectes JavaTM – c'est du moins ce que vous avez pu être mené à croire. En fait, les motifs de conception sont utiles à tous. Si ces outils ne sont pas exclusivement pour les astronautes de l'architecture, que sont-ils et pourquoi sont-ils utiles dans les applications PHP ?

Note de l'éditeur : Voir aussi l'article « Cinq motifs supplémentaires de conception pour PHP » pour apprendre cinq motifs pouvant également vous rendre service.

Note du traducteur : la première publication de cet article eut lieu en anglais sur IBM developerWorks (www.ibm.com/developer). Tous droits réservés par IBM et par l'auteur. Traduction par Guillaume Rossolini avec l'aimable autorisation d'IBM et de l'auteur.

Commentez ♪

- Titre : Cinq motifs classiques de conception pour PHP

- Auteurs : Jack D. Herrington, Guillaume Rossolini (traducteur)

- Parution : 15 mai 2008

- Licence : Copyright ® 2008 IBM et Jack D. Herrington. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Le motif Factory

- III. Le motif Singleton

- IV. Le motif Observer

- V. Le motif Chain-of-command

- VI. Le motif Strategy

- VII. Conclusions

- VIII. À propos de l'auteur

I - Introduction

Les motifs de conception furent présentés à la communauté du logiciel par Design Patterns d'Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides (affectueusement connus comme le Gang of Four). Le concept fondamental des motifs de conception, présenté en introduction, est simple. Au fil des années passées à développer des logiciels, Gamma et ses associés ont vu émerger certains motifs de conception solide, tout comme des architectes dessinant des plans de maisons et d'autres bâtiments peuvent développer des gabarits pour déterminer où une salle de bains devrait se trouver ou bien comment arranger une cuisine. Avoir ces gabarits, ou encore ces design patterns, signifie qu'ils peuvent imaginer plus rapidement de meilleurs bâtiments. Il se passe la même chose pour les logiciels.

Non seulement les motifs de conception proposent des moyens utiles de concevoir rapidement des applications robustes, ils sont aussi un glossaire pour regrouper des concepts génériques en termes simples. Vous pouvez par exemple dire que vous écrivez un système de messagerie pour alléger le couplage ou bien vous pouvez dire que vous écrivez un Observer, qui est le nom pour ce motif.

Il est difficile de démontrer l'utilité de motifs à l'aide de petits exemples : ils semblent généralement être une exagération, puisqu'ils entrent réellement en jeu dans les grandes collections de code. Cet article ne peut pas montrer d'application gigantesque, ainsi vous devez imaginer comment appliquer les principes de l'exemple (et pas nécessairement ce code exact) dans vos plus grandes applications. Cela ne veut pas dire que vous ne devriez pas utiliser de motifs de conception dans vos petites applications ; la plupart des applications commencent par une petite taille puis grandissent, il n'y a donc pas de raison de ne pas commencer par de bonnes pratiques comme celles-ci.

Maintenant que vous avez des notions de ce que sont les motifs de conception et pourquoi ils sont utiles, il est temps de voir cinq motifs récurrents pour PHP 5.

II - Le motif Factory

De nombreux motifs du livre original Design Patterns encouragent le couplage faible. Pour comprendre ce concept, il est plus facile de parler de la lutte de nombreux développeurs avec les grandes applications. Le problème survient lorsque vous changez un morceau de code et que vous voyez alors se dérouler une cascade de conséquences dans d'autres parts du système (des parties que vous pensiez totalement isolées).

Le problème est le couplage fort. Des fonctions et des classes d'une partie du système se reposent trop fortement sur des fonctions et classes d'autres parties du système. Vous avez besoin d'un ensemble de motifs qui laissent ces classes interagir, mais vous ne voulez pas les lier à tel point qu'elles deviennent entrelacées.

Dans de grands systèmes, beaucoup de code repose sur quelques classes clefs. Les difficultés surviennent lorsque vous devez modifier ces classes. Par exemple, supposez que vous avez une classe User qui lit depuis un fichier. Vous voulez la changer à une classe pour lire depuis une base de données, mais tout le code utilise cette classe qui lit depuis un fichier. Le motif Factory peut vous être utile ici.

Le motif Factory est une classe qui a plusieurs méthodes pouvant créer des objets pour vous. Plutôt qu'utiliser directement new, vous utilisez la classe usine pour créer les objets. De cette manière, si vous changez le type des objets créés, vous pouvez changer simplement l'usine. Tout le code utilisant l'usine est automatiquement modifié.

Le code ci-dessous montre un exemple de classe usine. La partie serveur de l'application est en deux parties : la base de données et un ensemble de scripts PHP qui vous permettent d'ajouter des flux, de demander la liste des flux et d'obtenir l'article associé à un flux particulier.

Listing 1. Factory1.php

<?php
interface IUser
{
function getName();
}

class User implements IUser
{
public function __construct( $id ) { }

public function getName()
{
return "Jack";
}
}

class UserFactory
{
public static function Create( $id )
{
return new User( $id );
}
}

$uo = UserFactory::Create( 1 );
echo( $uo->getName()."\n" );
?>

Une interface appelée IUser définit ce qu'un objet doit faire. L'implémentation d'IUser est appelée User, et une classe usine UserFactory créé des objets IUser. Cette relation est représentée dans la figure ci-dessous :

Figure 1. Les classes Factory et User ainsi que l'interface IUser

Si vous exécutez ce code dans l'interpréteur PHP, vous obtenez :

% php factory1.php
Jack
%

Le code d'exemple demande un objet User à l'usine puis affiche le résultat de la méthode getName().

Une variante du motif usine utilise des méthodes de l'usine. Ces méthodes statiques de la classe construisent des objets de ce type. Cette approche est utile lorsque la création d'un objet de ce type n'est pas aisée. Par exemple, supposez que vous devez créer l'objet puis lui donner de nombreuses propriétés. Cette version du motif Factory encapsule ce procédé en un seul endroit, de telle manière que le code complexe d'initialisation n'est pas recopié dans toute la collection de code source.

Voici un exemple d'utilisation des méthodes d'usine :

Listing 2. Factory2.php

<?php
interface IUser
{
function getName();
}

class User implements IUser
{
public static function Load( $id )
{
return new User( $id );
}

public static function Create( )
{
return new User( null );
}

public function __construct( $id ) { }

public function getName()
{
return "Jack";
}
}

$uo = User::Load( 1 );
echo( $uo->getName()."\n" );
?>

Ce code est bien plus simple. Il a une seule interface, IUser, et une classe appelée User qui implémente l'interface. La classe User a deux méthodes statiques qui créent l'objet. Cette relation est montrée dans le schéma ci-dessous :

Figure 2. L'interface IUser et la classe user avec des méthodes d'usine

Exécuter le script en lignes de commande produit le même résultat que le premier code :

% php factory2.php
Jack
%

Comme nous l'avons dit plus haut, ces motifs semblent parfois exagérés pour de petites situations. Toutefois, il est bon d'apprendre de solides méthodes comme celle-ci afin de les utiliser dans n'importe quelle taille de projet.

III - Le motif Singleton

Certaines ressources d'application sont exclusives, dans le sens où il y a une et une seule ressource de ce type. Par exemple, la connexion à une base par son handle est exclusive. Vous avez besoin de partager le handle dans une application, car ouvrir et fermer continuellement des connexions est une perte de ressources, en particulier lors du traitement d'une seule page.

Le motif Singleton répond à ce besoin. Un objet est un singleton si l'application ne peut contenir qu'un seul objet de ce type à un instant donné. Le Listing 3 montre un singleton de connexion à une base de données en PHP 5 :

Listing 3. Singleton.php

<?php
require_once("DB.php");

class DatabaseConnection
{
public static function get()
{
static $db = null;
if ( $db == null )
$db = new DatabaseConnection();
return $db;
}

private $_handle = null;

private function __construct()
{
$dsn = 'mysql://root:password@localhost/photos';
$this->_handle =& DB::Connect( $dsn, array() );
}

public function handle()
{
return $this->_handle;
}
}

print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
?>

Ce code montre une seule classe appelée DatabaseConnection. Vous ne pouvez pas créer votre propre DatabaseConnection puisque le constructeur est privé. Cependant, vous pouvez obtenir le seul et unique objet DatabaseConnection en utilisant la méthode statique get(). L'UML pour cet exemple est montré en Schéma 3 :

Figure 3. Le singleton de connexion à une base de données

La preuve est que le handle de connexion à la base renvoyé par la méthode handle() est le même pendant les deux appels. Vous pouvez le confirmer en exécutant en ligne de commande :

% php singleton.php
Handle = Object id #3
Handle = Object id #3
%

Les deux handles sont le même objet. Si vous utilisez le motif Singleton à travers toute l'application, vous réutilisez partout le même handle.

Vous pourriez utiliser une variable globale pour conserver le handle, mais cette approche ne fonctionne que pour de petites applications. Dans de plus grandes applications, évitez les globales et utilisez des objets et des méthodes pour l'accès aux ressources.

IV - Le motif Observer

Le motif Observer vous donne une autre tactique pour éviter le couplage fort entre les composants. Ce motif est simple : un objet se rend observable en ajoutant une méthode permettant à un autre objet, l'observateur, de s'enregistrer. Quand l'objet observable change, il envoie un message aux observateurs enregistrés. Ce que ces observateurs font avec cette information n'est ni important ni du ressort de l'objet observé. Le résultat est un moyen pour les objets de communiquer les uns avec les autres sans nécessairement comprendre pourquoi.

Un exemple simple est une liste d'utilisateurs dans un système. Le Listing 4 montre une liste d'utilisateurs qui envoie un message quand des utilisateurs sont ajoutés. Cette liste est observée par un observateur qui affiche un message quand un utilisateur est ajouté.

Listing 4. Observer.php

<?php
interface IObserver
{
function onChanged( $sender, $args );
}

interface IObservable
{
function addObserver( $observer );
}

class UserList implements IObservable
{
private $_observers = array();

public function addCustomer( $name )
{
foreach( $this->_observers as $obs )
$obs->onChanged( $this, $name );
}

public function addObserver( $observer )
{
$this->_observers []= $observer;
}
}

class UserListLogger implements IObserver
{
public function onChanged( $sender, $args )
{
echo( "'$args' added to user list\n" );
}
}

$ul = new UserList();
$ul->addObserver( new UserListLogger() );
$ul->addCustomer( "Jack" );
?>

Ce code définit quatre éléments : deux interfaces et deux classes. L'interface IObservable définit un objet qui peut être observé, et UserList implémente une interface pour s'enregistrer comme observable. La liste IObserver définit ce qu'il faut pour être observateur, et la classe UserListLogger implémente l'interface IObserver. Tout cela est montré dans Schéma 4 :

Figure 4. La liste observable d'utilisateurs et l'enregistreur d'évènements de la liste d'utilisateurs

Si vous exécutez ceci en ligne de commande, vous obtenez :

% php observer.php
'Jack' added to user list
%

Le code d'exemple crée un objet UserList et y ajoute l'observateur UserListLogger. Le code ajoute ensuite un utilisateur, et UserListLogger est notifié de ce changement.

Il est fondamental de comprendre que UserList ne sait pas ce que va faire UserListLogger. Il pourrait y avoir un ou plusieurs observateurs pour effectuer d'autres traitements. Vous pourriez par exemple avoir un observateur qui envoie un message à l'utilisateur pour lui souhaiter la bienvenue dans le système. L'intérêt de cette approche est que la UserList est ignorante de tous les objets qui en dépendent, elle se concentre sur son rôle de maintenir la liste d'utilisateurs et d'envoyer des messages quand la liste est modifiée.

Ce motif n'est pas limité aux objets en mémoire. C'est la fondation des systèmes de file d'attente (par base de données) de messages utilisés dans les grandes applications.

Note du traducteur : La SPL permet également de construire facilement un motif Observer en PHP.

V - Le motif Chain-of-command

Améliorant le concept du couplage faible, le motif chaîne de commande renvoie un message, une commande, une requête ou ce que vous voulez à travers un jeu de gestionnaires. Chaque gestionnaire décide lui-même s'il peut prendre en charge la requête. S'il le peut, la requête est prise en charge et le processus est arrêté. Vous pouvez ajouter ou supprimer des gestionnaires sans influencer les autres gestionnaires. Le Listing 5 montre un exemple de ce motif :

Listing 5. Chain.php

<?php
interface ICommand
{
function onCommand( $name, $args );
}

class CommandChain
{
private $_commands = array();

public function addCommand( $cmd )
{
$this->_commands []= $cmd;
}

public function runCommand( $name, $args )
{
foreach( $this->_commands as $cmd )
{
if ( $cmd->onCommand( $name, $args ) )
return;
}
}
}

class UserCommand implements ICommand
{
public function onCommand( $name, $args )
{
if ( $name != 'addUser' ) return false;
echo( "UserCommand handling 'addUser'\n" );
return true;
}
}

class MailCommand implements ICommand
{
public function onCommand( $name, $args )
{
if ( $name != 'mail' ) return false;
echo( "MailCommand handling 'mail'\n" );
return true;
}
}

$cc = new CommandChain();
$cc->addCommand( new UserCommand() );
$cc->addCommand( new MailCommand() );
$cc->runCommand( 'addUser', null );
$cc->runCommand( 'mail', null );
?>

Ce code définit une classe ChainOfCommand qui maintient une liste d'objets ICommand. Deux classes implémentent l'interface ICommand : l'un répond aux demandes d'e-mails et l'autre répond aux ajouts d'utilisateurs. L'UML est montré en Figure 5 :

Figure 5. La chaîne de commande et ses commandes

Si vous exécutez le script, qui contient du code d'exemple, vous obtenez :

% php chain.php
UserCommand handling 'addUser'
MailCommand handling 'mail'
%

Le code crée d'abord un objet CommandChain et y ajoute des instances des deux objets Command. Il exécute ensuite deux commandes pour voir qui leur répond. Si le nom de la commande correspond à UserCommand ou à MailCommand, le code se poursuit et rien ne se produit.

Le motif Chaîne de commande peut être utile pour créer une architecture flexible (pour traiter des requêtes) qui peut être ensuite appliquée à de nombreux problèmes.

VI - Le motif Strategy

Le dernier motif de conception que nous allons voir est le pattern Strategy. Dans ce motif, des algorithmes sont extraits de classes complexes afin de les remplacer facilement. Par exemple, le motif Strategy est éligible si vous voulez changer la manière dont des pages sont évaluées dans un moteur de recherche. Pensez le moteur de recherche comme étant composé de plusieurs parties : la première parcourt les pages, une autre leur attribue un rang et la dernière trie les résultats en fonction de ce rang. Dans un exemple complexe, toutes ces parties seraient dans la même classe. En utilisant le motif Strategy, vous prenez la portion d'assignation de rang et vous la mettez dans une autre classe. Cela permet de modifier la manière dont les rangs sont attribués sans que cela interfère avec le reste du code du moteur de recherche.

Comme exemple plus simple, le Listing 6 montre une classe de liste d'utilisateurs disposant d'une méthode pour trouver des utilisateurs en fonction d'un ensemble modulable de stratégies :

Listing 6. Strategy.php

<?php
interface IStrategy
{
function filter( $record );
}

class FindAfterStrategy implements IStrategy
{
private $_name;

public function __construct( $name )
{
$this->_name = $name;
}

public function filter( $record )
{
return strcmp( $this->_name, $record ) <= 0;
}
}

class RandomStrategy implements IStrategy
{
public function filter( $record )
{
return rand( 0, 1 ) >= 0.5;
}
}

class UserList
{
private $_list = array();

public function __construct( $names )
{
if ( $names != null )
{
foreach( $names as $name )
{
$this->_list []= $name;
}
}
}

public function add( $name )
{
$this->_list []= $name;
}

public function find( $filter )
{
$recs = array();
foreach( $this->_list as $user )
{
if ( $filter->filter( $user ) )
$recs []= $user;
}
return $recs;
}
}

$ul = new UserList( array( "Andy", "Jack", "Lori", "Megan" ) );
$f1 = $ul->find( new FindAfterStrategy( "J" ) );
print_r( $f1 );

$f2 = $ul->find( new RandomStrategy() );
print_r( $f2 );
?>

L'UML de ce code est montré dans le Schéma 6 :

Figure 6. La chaîne de commande et ses commandes

La classe UserList est une surcouche autour d'un tableau de noms. Elle implémente une méthode find() qui prend l'une des stratégies pour sélectionner un sous-ensemble de ces noms. Ces stratégies sont définies par l'interface IStrategy, qui a deux implémentations : l'une choisit les noms au hasard, et l'autre choisit tous ceux après le nom spécifié. Quand vous exécutez le code, vous obtenez le résultat suivant :

% php strategy.php
Array
(
[0] => Jack
[1] => Lori
[2] => Megan
)
Array
(
[0] => Andy
[1] => Megan
)
%

Le code d'exemple donne la même liste à deux stratégies et affiche les résultats. Dans le premier cas, la stratégie cherche tous les noms triés après « J » de manière à renvoyer Jack, Lori et Megan. L'autre stratégie prend les noms au hasard et renvoie différents résultats à chaque fois. Dans le cas présent, ce sont Andy et Megan.

Le motif Strategy est excellent pour des systèmes complexes de gestion et de traitement de données nécessitant une grande flexibilité dans la manière dont les données sont filtrées, recherchées et traitées.

VII - Conclusions

Ce sont simplement quelques-uns des motifs les plus connus et utilisés dans des applications PHP. De nombreux autres sont expliqués dans le livre Design Patterns. Ne soyez pas désarçonné par le mystique de l'architecture. Les motifs sont d'excellentes idées que vous pouvez utiliser dans n'importe quel langage et à n'importe quel niveau de compétences.

VIII - À propos de l'auteur

Développeur logiciel confirmé de plus de 20 ans d'expérience, Jack Herrington est l'auteur de trois livres : Code Generation in Action, Podcasting Hacks et PHP Hacks. Il est aussi l'auteur de plus de trente articles.