Proxy, proxy…proxy de cache, proxy Web…vous avez sans doute déjà lu ce terme quelque part, n’est-ce pas ? Si oui, alors vous avez déjà sa principale raison d’être en tête : un proxy s’intercale entre vous et…quelque chose !
En programmation, ce quelque chose est un objet « distant »…distant parce qu’il peut se trouver ailleurs sur le réseau mais pas seulement ! Il peut très bien se trouver sur la même machine mais dans un autre espace d’adressage. En pratique, le proxy implémente la même interface que l’objet auquel il sert d’écran, car il va se substituer à lui !
Ce design pattern fait état de relations entre des objets, voilà pourquoi on dit qu’il est structurel ! Proxy (ou Procuration ou encore Surrogate en anglais) a des similitudes avec un autre pattern structurel : Décorateur. Cependant, il convient de bien garder à l’esprit que si Décorateur a pour but d’ajouter des fonctionnalités à l’objet décoré, Proxy est souvent là pour effectuer un contrôle d’accès à un objet.
Si vous avez déjà travaillé avec le framework Symfony et l’ORM Doctrine, cette notion de proxy ne vous est pas inconnue car vous manipulez des proxies en bien des occasions !
L’interprète, un mandataire idéal !
Voilà un nouveau terme français pour notre Proxy : mandataire. Quel meilleur exemple que celui de l’interprète, par lequel il est impératif de passer lors des sommets internationaux, si l’on veut être compris des grands de ce monde (et éventuellement lui faire porter la responsabilité d’un incident diplomatique) ! Voilà un exemple très simple pour illustrer ça : des interfaces que le sujet réel (le président russe) et son proxy (l’interprète russe-français) implémentent tous deux et un président hôte qui prend en composition un interprète avec lequel il va parler tandis que celui-ci va de son côté discuter avec le sujet réel. Dans cet exemple, j’en profite pour utiliser la dérivation d’interfaces, le mot clé final, une classe abstraite…En réalité j’aurais largement pu simplifier le code, j’aurais pu aussi injecter directement l’interprète dans le constructeur du président hôte, qui mène les conversations, au lieu de mettre un setter dont je ne suis pas spécialement adepte, bref j’aurais pu faire des tas de choses autrement mais je veux garder le code efficace et amusant autant que possible !
interface PersonneInterface { public function parlerDuTemps(): string; } interface PresidentInterface extends PersonneInterface { public function parlerDuRechauffementClimatique(): string; public function parlerDesGuerres(): string; } final class PresidentRusse implements PresidentInterface { public function parlerDuTemps(): string { return 'Я очень рад быть здесь, погода прекрасная в Париже'; } public function parlerDuRechauffementClimatique(): string { return 'это очень серьезная проблема !'; } public function parlerDesGuerres(): string { return 'какая война?'; } } interface InterpreteInterface { public function boireUnVerreEau(): string; } abstract class Interprete implements InterpreteInterface { protected $presidentHote; protected $presidentInvite; public function __construct(PresidentInterface $presidentHote, PresidentInterface $presidentInvite) { $this->presidentHote = $presidentHote; $this->presidentInvite = $presidentInvite; } public function boireUnVerreEau(): string { return 'Glou Glou Glou'.PHP_EOL; } } final class InterpreteRusse extends Interprete implements PresidentInterface { public function parlerDuTemps(): string { $temps = 'Au sujet du temps, le président russe me dit : "'; $temps .= $this->presidentInvite->parlerDuTemps().'"'.PHP_EOL; return $temps; } public function parlerDuRechauffementClimatique(): string { $climat = 'Au sujet du climat, le président russe me dit : "'; $climat .= $this->presidentInvite->parlerDuRechauffementClimatique().'"'; return $climat.PHP_EOL; } public function parlerDesGuerres(): string { if ($this->presidentHote instanceof PresidentFrancais) { return 'Le président russe ne souhaite pas évoquer le sujet avec le président français ! Prenons plutôt Vodka !'.PHP_EOL; } $guerre = 'Au sujet des guerres, le président russe me dit : "'; $guerre .= $this->presidentInvite->parlerDesGuerres().'"'.PHP_EOL; return $guerre; } } final class PresidentFrancais implements PresidentInterface { private $interprete; public function attacherInterprete(InterpreteInterface $interprete): void { $this->interprete = $interprete; } public function parlerDuTemps(): string { return 'Président, il fait bon vivre à Paris, n'est-ce pas ?'.PHP_EOL; } public function parlerDuRechauffementClimatique(): string { return 'Que pensez-vous du réchauffement climatique ?'.PHP_EOL; } public function parlerDesGuerres(): string { return 'Que vous inspirent les conflits mondiaux ?'.PHP_EOL; } public function discuterSurPerron(): void { if (!$this->interprete) { throw new RuntimeException('Où est l\'interprète ?'); } echo $this->parlerDuTemps(); echo $this->interprete->parlerDuTemps(); echo $this->parlerDuRechauffementClimatique(); echo $this->interprete->parlerDuRechauffementClimatique(); echo $this->parlerDesGuerres(); echo $this->interprete->parlerDesGuerres(); } } $presidentFrancais = new PresidentFrancais(); $presidentRusse = new PresidentRusse(); $InterpreteRusse = new InterpreteRusse($presidentFrancais, $presidentRusse); $presidentFrancais->attacherInterprete($InterpreteRusse); try { $presidentFrancais->discuterSurPerron(); } catch (RuntimeException $exception) { echo "Allô, ici le chef du protocole !", PHP_EOL; echo "Le président vient de me dire '" . $exception->getMessage(), "'", PHP_EOL; echo "Vite, allez chercher un interprète !", PHP_EOL; } echo $InterpreteRusse->boireUnVerreEau();
Une fois de plus, voilà un design pattern fait la part belle à l’abstraction !
Vous notez ici que la tentation serait grande pour notre interprète de proposer des fonctionnalités que ne propose pas le président dont il assure la traduction. En faisant ainsi, nous nous éloignerions de Proxy – dont le rôle consiste majoritairement à faire de la délégation – pour adopter une approche Décorateur.
Notre interprète russe joue également le rôle de proxy de protection en filtrant les accès au président russe sur des questions épineuses: en l’occurrence, le président russe n’a pas tellement envie de parler des guerres avec son homologue français et l’interprète a reçu lors de son briefing des instructions sans équivoque…heureusement qu’en bon russe, il a prévu un verre de vodka pour désamorcer tout début de crise diplomatique ! Faire des vérifications de droit d’accès, voilà aussi un autre aspect de ce design pattern.
Le chargement fainéant
Personne n’emploie ce terme en réalité, mais il me fait rire ! Le lazy loading consiste à différer le chargement du vrai objet (celui qui nous mandate, qui nous donne procuration) jusqu’au moment où l’on s’en servira effectivement ! Ceci est notamment utile lorsqu’un objet est un plutôt gourmand en ressources.
Prenons l’exemple d’une classe qui sert habituellement à manipuler des images. Lorsqu’on l’instancie, elle range dans une variable d’instance prévue à cette effet la totalité du flux de l’image qu’on lui passe en paramètre.
Notre proxy, qui possède le même super-type que la classe image (une classe abstraite implémentant une interface) ne va pas effectuer ce chargement mais il va attendre le dernier moment pour instancier la classe qui le mandate et appeler dessus les méthodes demandées par le client :
interface ImageInterface { public function donnerTaille(): int; public function aContenu(): bool; public function afficherContenu(): ?string; } abstract class AbstractImage implements ImageInterface { protected $cheminFichier; protected $contenuFichier; protected $tailleFichier; public function __construct($cheminFichier) { $this->cheminFichier = $cheminFichier; } public function donnerTaille(): int { return $this->tailleFichier; } public function aContenu(): bool { return null !== $this->contenuFichier; } } class StandardImage extends AbstractImage { public function __construct($cheminFichier) { parent::__construct($cheminFichier); $this->contenuFichier = file_get_contents( $this->cheminFichier); $this->tailleFichier = filesize( $this->cheminFichier); } public function afficherContenu(): ?string { return $this->contenuFichier; } } class ProxyImage extends AbstractImage { private $vraieImage; public function __construct($cheminFichier) { parent::__construct($cheminFichier); $this->tailleFichier = filesize($this->cheminFichier); } public function afficherContenu(): ?string { if (!$this->vraieImage) { $this->vraieImage = new StandardImage( $this->cheminFichier); } return $this->vraieImage->afficherContenu(); } } final class GestionnaireImage { public function traiterImage (ImageInterface $image): void { echo $image->donnerTaille() . ' octets'; echo 'Contenu présent ?'.($image->aContenu()); echo $image->afficherContenu(); } } $gestionnaireImage = new GestionnaireImage(); $image = new StandardImage('elephant.jpg'); echo $gestionnaireImage->traiterImage($image); $proxy = new ProxyImage('elephant.jpg'); echo $gestionnaireImage->traiterImage($proxy);
Le client (GestionnaireImage) travaille d’ordinaire avec des objets de la classe StandardImage, qui, dès qu’elle est instanciée, stocke le flux complet du fichier ciblé dans une variable d’instance. Ceci peut s’avérer extrêmement coûteux si l’image est de grande taille ou si un grand nombre d’images sont requises en même temps par différents utilisateurs de notre classe, voire les deux !
Nous intercalons donc un objet Proxy entre le client de notre code et le sujet ciblé : ProxyImage possède le même super-type que StandardImage, il est donc tout à fait capable d’agir en qualité de mandataire ! Son rôle sera de différer la construction du sujet ciblé jusqu’au moment où son utilisation sera requise; il doit pour cela posséder une référence à ce sujet, voilà pourquoi vous voyez la donnée membre privée $vraieImage dans ProxyImage ! C’est le mandataire qui instancie le sujet, au départ il possède sur le fichier image une référence indirecte (le nom de fichier) puis finit par obtenir une référence directe (l’objet StandardImage, avec le flux complet).
Dans le cas de StandardImage, le contenu du fichier cible est intégralement stocké dans la propriété privée dédiée à cet effet, mais pas dans le cas de ProxyImage qui n’instanciera la classe mandatée que lorsque la méthode afficherContenu sera invoquée. La valeur retournée par $image->aContenu() vaudra TRUE dans le cas de StandardImage et FALSE dans le cas du Proxy; c’est bien le signe que le Proxy fait l’économie de la lecture du flux du fichier image sur lequel nous travaillons. Cependant, lorsque le client demande l’affichage de l’image, le Proxy ne peut faire autrement que d’instancier StandardImage pour invoquer dessus la méthode qui va retourner le flux utile à cet affichage.
Au final, le mandataire est plus efficace que le sujet réel qu’il masque puisque lors de l’appel à donnerTaille dans traiterImage, il n’a pas récupéré l’intégralité du flux binaire du fichier cible. C’est évidemment ce qu’il fait lorsqu’il doit afficher celui-ci, ne pouvant faire autrement.
Pour utiliser cet exemple, il vous faudra bien entendu une image nommée elephant.jpg que je vous fournis un peu plus bas !
Quand on doit différer les opérations qui s’avèrent coûteuses lors de la création d’un objet au moment où elle seront effectivement requises alors le design pattern Proxy peut s’avérer d’une aide précieuse !