Archives mensuelles : février 2014

PHP : le design pattern Adaptateur

Chez Auto Load, tout est permis !

Pour illustrer notre exemple du design pattern Adaptateur, nous allons cette fois nous prendre pour des inspecteurs du permis de conduire ! Chez AUTO LOAD, nous nous vantons d’obtenir un taux de réussite de 99% à l’examen du permis de conduire et ce sans soudoyer de quelque manière que ce soit nos candidats ! Voyons sans tarder les principales classes qui composent notre exemple. Tout d’abord, la classe InspecteurPermisConduire, qui est notre classe cliente. C’est elle qui va faire usage de nos autres classes.

class InspecteurPermisConduire {
    private $_candidat;
	
    public function __construct(ConducteurInterface $conducteur)
    {
        $this->_candidat = $conducteur;
    }
    public function changerCandidat(ConducteurInterface $conducteur)
    {
        $this->_candidat = $conducteur;
    }
    public function fairePasserExamen()
    {
        $this->_candidat->demarrer();
        $this->_candidat->accelerer();
        $this->_candidat->tournerDroite();
        $this->_candidat->accelerer();
        $this->_candidat->tournerGauche();
        $this->_candidat->ralentir();
        $this->_candidat->reculer();
        $this->_candidat->immobiliser();
    }
}

Une instance donnée d’InspecteurPermisConduire prend en composition tout objet se conformant à l’interface ConducteurInterface : tout d’abord, elle l’exige dans son constructeur, c’est à dire qu’aucune instance d’InspecteurPermisConduire ne peut exister sans un objet qui implémente en totalité ConducteurInterface. Ensuite, nous nous réservons la possibilité de changer de type de candidat à l’exécution (runtime) grâce à un setter. Enfin, la valeur ajoutée de notre classe réside dans sa méthode fairePasserExamen, qui fait effectuer à tout conducteur les manœuvres de base, indispensables pour décrocher le précieux sésame !

Voici justement notre Automobiliste :

interface ConducteurInterface {
	public function demarrer();
	public function tournerGauche();
	public function tournerDroite();
	public function accelerer();
	public function ralentir();
	public function reculer();
	public function immobiliser();
}

class Automobiliste implements ConducteurInterface {
	public function demarrer() {
		echo "tourner la clé de contact ou mettre la carte";
	}
	public function tournerGauche() {
		echo "tourner le volant vers la gauche";
	}
	public function tournerDroite() {
		echo "tourner le volant vers la droite";
	}
	public function accelerer() {
		echo "appuyer sur la pédale d'accélération";
	}
	public function ralentir() {
		echo "relâcher la pédale d'accélération et/ou", 
                      "appuyer sur la pédale de frein";
	}
	public function reculer() {
		echo "passer la marche arrière et accélérer";
	}
	public function immobiliser() {
		echo "mettre le frein à main";
	}
}

Notre classe concrète Automobiliste implémente les fonctions imposées par l’interface ConducteurInterface. Cette interface recense les principales manoeuvres dont la maîtrise est exigée de tout conducteur de véhicule qui se respecte : démarrer et arrêter le véhicule, avancer, reculer, tourner, accélérer et ralentir. Attention cependant, certains véhicules – comme un vélo – ne se démarrent pas, il va falloir prendre en compte cet aspect là…

Bref, à ce stade là, tout est pour le mieux dans le meilleur des mondes ! [Candide]

Schéma UML de l'exemple de design pattern adaptateur

Dès que le vent soufflera, je recodera… (air bien connu)

Jusqu’au jour où Otto Hekohl, le propriétaire de notre école de conduite, vient nous annoncer qu’étant donné que nous sommes situés en bord de mer, il serait plutôt judicieux – en plus d’être diablement bénéfique pour notre chiffre d’affaire – que nous puissions faire passer le permis bateau à des candidats qui sont de plus en plus nombreux et demandeurs…

Photos de gens sautant d'un yacht

Elle est parfois dure la vie à bord d’un frêle esquif !

Avant de se faire congédier par notre patron pour avoir regardé trop de vidéos de chatons montant sur le dos de tortues durant ses heures de travail, Jojo Dingo, le deuxième programmeur de l’entreprise, avait envisagé d’intégrer ce changement de notre stratégie commerciale. Voici la structure logicielle dont nous héritons, maintenant que Jojo a toutes ses journées de libre pour s’adonner à sa félinophilie maladive :

interface NavigateurInterface {
	public function demarrer();
	public function reculer();
	public function tournerBabord();
	public function tournerTribord();
	public function accelerer();
	public function ralentir();
	public function jeterAncre();
}

abstract class Marin implements NavigateurInterface {
	public function jeterAncre() {
		echo "jeter l'ancre à la mer";
	}
}

class UnsupportedMethodException extends Exception {}

class MarinVoile extends Marin {
	public function demarrer() {
		throw new UnsupportedMethodException
                     ("Cette fonctionnalité n'est pas disponible");
	}
	public function tournerBabord() {
		echo "diriger les voiles et la barre pour aller à babord";
	}
	public function tournerTribord() {
		echo "diriger les voiles et la barre pour aller à tribord";
	}
	public function accelerer() {
		echo "positionner les voiles et déterminer l'allure";
	}
	public function ralentir() {
		echo "positionner les voiles et déterminer l'allure";
	}
	public function reculer() {
		echo "positionner les voiles et manœuvrer pour reculer";
	}
}

class MarinMoteur extends Marin {
	public function demarrer() {
		echo "démarrer le moteur";
	}
	public function tournerBabord() {
		echo "manoeuvrer la barre ou le volant pour aller à babord";
	}
	public function tournerTribord() {
		echo "manoeuvrer la barre ou le volant pour aller à tribord";
	}
	public function accelerer() {
		echo "augmenter la vitesse du moteur";
	}
	public function ralentir() {
		echo "dimininuer la vitesse du moteur ou le couper";
	}
	public function reculer() {
		echo "passer la marche arrière";
	}
}

Jojo a bien compris qu’il nous fallait des classes qui nous permettent de gérer les gens qui s’adonnent à la navigation de plaisance motorisée ou à voile et qu’un bateau ne se manœuvrait pas comme une voiture.

Attardons-nous sur la structure de ces classes : nous avons une classe abstraite Marin qui implémente une interface NavigateurInterface et dont les deux filles concrètes MarinVoile et MarinMoteur réalisent la majeure partie des fonctions.

Schéma UML du design pattern adaptateur

Marin est abstraite car trop générique, elle réalise cependant l’implémentation de jeterAncre, qui est commune à toute forme de navigation (bon, c’est vrai, les Optimist n’ont pas d’ancre). Jojo a choisi de créer une exception pour gérer le cas où la fonction démarrer n’est pas prise en charge, plutôt que de l’isoler dans une interface NavigateurMoteurInterface dédiée aux bateaux qui ont ce mode de propulsion. C’est un choix de conception, qui a le mérite de favoriser la généricité, inutile de faire une vérification de la présence de cette méthode en utilisant par exemple method_exists dans la classe client InspecteurPermisConduire. C’est la classe concrète qui gère l’absence de cette méthode en levant une exception correctement typée.

Comment ne pas se faire mener en bateau ?

Adeptes de longue date de la programmation orientée objet, nous souhaitons minimiser l’impact de ce changement d’architecture logicielle dans nos classes existantes. C’est là où l’adaptateur entre en jeu !

L’adaptateur fonctionne exactement sur le même principe que l’adaptateur de votre sèche-cheveux quand vous allez, en Irlande, par exemple ! Il prend en entrée la prise française de votre sèche-cheveux favori et l’adapte aux prises à trois broches que vous trouvez sur l’île Émeraude ! Vous n’avez pas eu à dénuder les fils de votre appareil pour y fixer une prise irlandaise, et heureusement : l’adaptateur fait le travail pour vous ! Et vous n’y avez vu que du bleu !

S’adapter…au changement

L’adaptateur a pour but de faire collaborer des interfaces de prime abord incompatibles, comme ConducteurInterface et NavigateurInterface. C’est un intermédiaire précieux (et réutilisable, ce qui nous met en joie !) qui va nous éviter de déstabiliser notre code base et qui va découpler le client (notre inspecteur du permis de conduire) de notre adapté (ici, nos marins). Plus le couplage est lâche, mieux c’est, je ne vous apprends rien !

L’adaptateur prend en composition l’adapté, et il sera chargé d’effectuer la translation entre les demandes à destination de l’interface originelle (ConducteurInterface) et la nouvelle interface cible (NavigateurInterface).

Pour que cette translation s’effectue de manière transparente pour le système, il faut naturellement que l’adaptateur implémente l’interface originelle. Regardez donc :

class AdaptateurMarin implements ConducteurInterface {
	private $_marin;
	
	public function __construct(Marin $marin) {
		$this->_marin = $marin;
	}
	public function demarrer() {
		$this->_marin->demarrer();
	}
	public function tournerGauche() {
		$this->_marin->tournerBabord();
	}
	public function tournerDroite() {
		$this->_marin->tournerTribord();
	}
	public function accelerer() {
		$this->_marin->accelerer();
	}
	public function ralentir() {
		$this->_marin->ralentir();
	}
	public function reculer() {
		$this->_marin->reculer();
	}
	public function immobiliser() {
		$this->_marin->jeterAncre();
	}
}

Le polymorphisme avant tout ! Notre adaptateur prend en charge des objets de la classe Marin, c’est à dire potentiellement toutes les sous-classes qui en dérivent. Tant mieux car nous en avons deux : MarinVoile et MarinMoteur !

Nous n’avons pas du tout modifié notre classe existante, nous avons isolé la nouveauté dans une nouvelle classe, autonome, avec des responsabilités clairement identifiées.

Les comportements que vous isolez dans des unités de code indépendantes sont réutilisables, ceux que vous enfermez dans des classes ne le sont pas forcément !

Pour que le système prenne en compte cette nouvelle possibilité (à nous l’argent facile !), il nous faudra juste modifier le code appelant. Notre client avait déjà un modificateur d’implémenté, à la bonne heure, servons nous en donc !

$adaptateur = new AdaptateurMarin(new MarinMoteur());
$inspecteur->changerCandidat($adaptateur);
$inspecteur->fairePasserExamen();

La méthode changerCandidat exige en paramètre une implémentation de la classe ConducteurInterface, ça tombe bien c’est précisément ce qu’est Marin et ses filles concrètes.

Souvenez vous toutefois qu’une exception est levée dans notre adaptateur MarinVoile, il faut donc en réalité modifier quelque peu le code appelant, qui va devoir les intercepter et les traiter le cas échéant. N’allons pas chercher midi à quatorze heures, nous nous contenterons d’en afficher le message. « KISS » is the law !

try {
	$inspecteur->fairePasserExamen();
} catch (UnsupportedMethodException $exception) {
	echo $exception->getMessage();
}

Voici maintenant l’architecture de notre code, avec l’adaptateur qui est venu s’intercaler entre l’existant et nos nouvelles fonctionnalités, qui vont nous permettre de gérer marins d’eau douce, flibustiers, jeunes loups de mer, moules à gaufres et autres ectoplasmes !

schema2

Pour lire cet article dans des conditions optimales, il est fortement conseillé d’écouter autant de fois que nécessaire ce titre (en plus d’aller les voir en concert en mai 2014) :