Dans un tout autre registre, mais toujours lié à l'héritage, il faut absolument que je vous présente une idée : les interfaces.
Les interfaces sont des idées. Elles se déclarent comme une classe, mais avec le mot clé interface
au lieu de class
, et ne peuvent contenir que des signatures de méthode.
De quoi ?
Oui, alors, quand vous écrivez une méthode, vous lui donnez un nom, une visibilité, des arguments avec des types, obligatoires ou optionnels, des valeurs par défaut, et un type de retour. Tout ça, c'est la signature. Puis, entre accolades, vous écrivez l'algorithme qui la compose.
Eh bien, dans une interface on se contente d'écrire des signatures. Pas le contenu.
Pourquoi faire ?
Pour deux choses. Lorsque vous travaillez en équipe, définir des interfaces sans écrire le code réel permet de se répartir les tâches et de commencer à travailler comme si les classes existaient. Eh oui, parce que vous pouvez préciser une interface comme un type dans vos arguments de méthodes !
La seconde, pour spécifier des comportements communs attendus entre différentes classes appartenant à des domaines différents. Exactement comme dans notre exemple précédent, oui. :)
Imaginez une classe MessagePrinter
dont le rôle est d'afficher un message.
<?php
class MessagePrinter
{
public static function printMessage($message)
{
echo sprintf('%s %s', $message->getContent(), $message->getAuthor()->name);
}
}
Remarquez que je n'ai pas mis de type à mon argument $message
. Jusqu'ici, on ne pouvait pas à la fois mettre le type en provenance du domaine Forum
et celui en provenance du domaine Messenger
. Ce n'était pas possible, jusqu'en PHP 8 avec un |
en séparateur. Ça ressemble à ça :
<?php
use Domain\Forum\Message as ForumMessage;
use Domain\Messenger\Message as MessengerMessage;
class MessagePrinter
{
public static function printMessage(ForumMessage|MessengerMessage $message)
{
echo sprintf('%s %s', $message->getContent(), $message->getAuthor()->name);
}
}
Mais ça ne résout pas vraiment le problème de fond. Si demain une nouvelle classe de message vient s'ajouter, et que je veux pouvoir l'afficher, je vais devoir venir modifier printMessage
pour y ajouter une classe... Si j'en ai 10, je dois mettre les 10 ? C'est là qu'entre en jeu l'interface.
Découvrez la syntaxe
Créons une interface pour nos messages :
<?php
namespace Domain\Display;
use Domain\User\User;
interface MessageInterface
{
public function getContent(): string;
public function getAuthor(): User;
}
Toute classe utilisant cette interface sera obligée d'avoir les méthodes getContent
et getAuthor
qui renverront une chaîne de caractères et un utilisateur. Peu importe la classe, et peu importent son implémentation, son code.
On peut donc définir cette interface dans notre méthode printMessage
.
<?php
declare(strict_types=1);
namespace use Domain\Display;
class MessagePrinter
{
public static function printMessage(MessageInterface $message)
{
echo sprintf('%s %s', $message->getContent(), $message->getAuthor()->name);
}
}
Tant que notre code garantit que l'objet passé en argument est issu d'une classe utilisant l'interface, alors ça marchera.
Ajoutons l'interface sur les classes, à présent ! Cela s'effectue par le biais du mot clé implements
suivi du nom de l'interface, en dernier, juste après le nom de la classe. Et si la classe en étend une autre, après le nom de la classe étendue.
<?php
declare(strict_types=1);
namespace Forum {
use Domain\Display\MessageInterface;
class Message implements MessageInterface
{
// ... implémentation des méthodes de l'interface
}
}
namespace Messenger {
use Domain\Display\MessageInterface;
class Message implements MessageInterface
{
// ... implémentation des méthodes de l'interface
}
}
Contrairement à l'extension de classe, vous pouvez implémenter plusieurs interfaces en même temps ! La solution précédente aurait pu être la suivante :
<?php
namespace Domain\Display {
use Domain\User\User;
interface ContentAwareInterface
{
public function getContent(): string;
}
interface AuthorAwareInterface
{
public function getAuthor(): User;
}
}
namespace Domain\Forum {
use Domain\Display;
class Message implements Display\AuthorAwareInterface, Display\ContentAwareInterface {
// Implémentation des méthodes des interfaces
}
}
Il y a une règle à respecter : une interface par ensemble de méthodes inséparables. On appelle ceci la ségrégation des interfaces. Plus la "surface" de ces interfaces est petite, plus les dépendances demandées dans les autres classes seront ciblées. Cela veut dire que le code n'aura qu'un très faible couplage avec de vraies classes, et que ce sera d'autant plus facile de les refactoriser si nécessaire.
Attends, attends ! Ton dernier exemple ne marche plus avec la méthode printMessage
, n’est-ce pas ?
C'est vrai, mais il y a un dernier point. ;) Une interface peut hériter de plusieurs interfaces avec le mot clé extends
!
<?php
namespace Domain\Display {
use Domain\User\User;
interface ContentAwareInterface
{
public function getContent(): string;
}
interface AuthorAwareInterface
{
public function getAuthor(): User;
}
interface MessageInterface extends ContentAwareInterface, AuthorAwareInterface
{
}
}
namespace Domain\Forum {
use Domain\Display;
class Message implements MessageInterface {
// Implémentation des méthodes des interfaces
}
}
Voilà qui est mieux. Vous pouvez composer vos interfaces pour qu'elles soient le moins couplées possible. Dit autrement, il faut essayer de faire en sorte que lorsque vous utilisez une interface, vous ayez besoin d'utiliser l'intégralité des méthodes réclamées. Dans le cas contraire, il serait peut-être bon de séparer les interfaces en de plus petites, plus spécialisées.
Utilisez les interfaces comme type d'argument dans vos méthodes
Voyons ensemble dans cette vidéo comment utiliser les interfaces en tant qu’argument de méthode, et un piège commun dans lequel il vaut mieux ne pas tomber.
Exercez-vous
Reprenons notre cher MatchMaker ! C’est à vous de créer des interfaces pour les classes Player
, QueuingPlayer
et Lobby
. Puis, remplacez les arguments attendus en utilisant les interfaces :).
Vous trouverez le code sur la branche P3C4, et la correction sur la branche P3C4-correction.
En résumé
Les interfaces vous offrent plus de souplesse et d'anticipation en permettant de :
typer des arguments et retours de méthodes qui n'existent pas encore ;
ou encore garantir qu'un code fonctionnera toujours sans s'imposer d'avoir à étendre des classes entières lors d'évolutions futures.
Et d'ailleurs en parlant de "composé", ça tombe bien (Comment ça, c'est fait exprès ?). Plus nous avançons avec ces mécanismes, plus nous nous éloignons du "simple héritage" de classes . C'est une bonne chose ! C'est qu'on progresse.
Créer des classes c'est bien, les faire communiquer entre elles, c'est mieux . C'est le sujet de notre prochain chapitre !