Les préfixes et les suffixes n'ont pas leur place dans les noms d'interface
L'utilisation du suffixe I
prefix or Interface
pour
les interfaces, et Abstract
pour les classes abstraites, est un
anti-modèle. Il n'a pas sa place dans le code pur. La distinction des noms
d'interface brouille les principes de la POO, ajoute du bruit au code et
entraîne des complications lors du développement. En voici les raisons.
Type = Classe + Interface + Descendants
Dans le monde de la POO, les classes et les interfaces sont considérées comme des types. Si j'utilise un type lors de la déclaration d'une propriété ou d'un paramètre, du point de vue du développeur, il n'y a aucune différence entre le type sur lequel il s'appuie et une classe ou une interface. C'est ce qui rend les interfaces si utiles, en fait. C'est ce qui donne un sens à leur existence. (Sérieusement : à quoi serviraient les interfaces si ce principe ne s'appliquait pas ? Essayez d'y penser).
Jetez un coup d'oeil à ce code :
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
Le constructeur dit : "J'ai besoin d'un formulaire et d'un traducteur.
" Et il ne se soucie pas de savoir s'il obtient un objet
GettextTranslator
ou un objet DatabaseTranslator
. Et
en même temps, en tant qu'utilisateur, je ne me soucie pas de savoir si
Translator
est une interface, une classe abstraite ou une classe
concrète.
Je ne m'en soucie pas vraiment ? En fait, non, j'admets que je suis assez curieux, alors quand j'explore la bibliothèque de quelqu'un d'autre, je jette un coup d'œil à ce qui se cache derrière le type et je le survole :
Oh, je vois. Et c'est la fin de l'histoire. Savoir si c'est une classe ou une interface serait important si je voulais en créer une instance, mais ce n'est pas le cas, je parle juste du type de la variable maintenant. Et c'est là que je veux rester en dehors de ces détails d'implémentation. Et je ne veux certainement pas qu'ils soient intégrés dans mon code ! Ce qu'il y a derrière un type fait partie de sa définition, pas du type lui-même.
Maintenant, regardez l'autre code :
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Cette définition de constructeur dit littéralement : "J'ai besoin d'une forme abstraite et d'une interface de traducteur. " Mais c'est idiot. Il a besoin d'une forme concrète à rendre. Pas une forme abstraite. Et il a besoin d'un objet qui agit comme un traducteur. Il n'a pas besoin d'une interface.
Vous savez que les mots Interface
et Abstract
doivent être ignorés. Que le constructeur veut la même chose que dans
l'exemple précédent. Mais… vraiment ? Est-ce vraiment une bonne idée
d'introduire dans les conventions de nommage l'utilisation de mots à
ignorer ?
Après tout, cela crée une fausse idée des principes de la POO. Un
débutant doit être confus : “Si le type Translator
signifie
soit 1) un objet de la classe Translator
2) un objet implémentant
l'interface Translator
ou 3) un objet héritant d'eux, alors que
signifie TranslatorInterface
?” Il n'y a pas de réponse
raisonnable à cette question.
Lorsque nous utilisons TranslatorInterface
, même si
Translator
peut être une interface, nous commettons une
tautologie. Il en va de même lorsque nous déclarons
interface TranslatorInterface
. Petit à petit, une blague de
programmation va se produire :
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Constructor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Implémentation exceptionnelle
Lorsque je vois quelque chose comme TranslatorInterface
, il est
probable qu'il existe une implémentation appelée
Translator implements TranslatorInterface
. Cela me fait me demander
: qu'est-ce qui rend Translator
si spécial qu'il a le privilège
unique d'être appelé Translator ? Toutes les autres implémentations ont
besoin d'un nom descriptif, comme GettextTranslator
ou
DatabaseTranslator
, mais celle-ci est en quelque sorte “par
défaut”, comme le suggère son statut privilégié d'être appelée
Translator
sans étiquette.
Même les gens s'y perdent et ne savent pas s'ils doivent taper
Translator
ou TranslatorInterface
, puis les deux se
mélangent dans le code client, je suis sûr que vous avez rencontré ce
problème à plusieurs reprises (dans Nette par exemple dans la connexion avec
Nette\Http\Request
vs IRequest
).
Ne serait-il pas préférable de se débarrasser de l'implémentation
spéciale et de garder le nom générique Translator
pour
l'interface ? C'est-à-dire avoir des implémentations spécifiques avec un nom
spécifique + une interface générique avec un nom générique. C'est
logique.
La charge d'un nom descriptif repose alors uniquement sur les
implémentations. Si nous renommons TranslatorInterface
en
Translator
, notre ancienne classe Translator
a besoin
d'un nouveau nom. Les gens ont tendance à résoudre ce problème en l'appelant
DefaultTranslator
, même moi je suis coupable de cela. Mais encore
une fois, qu'est-ce qui la rend si spéciale qu'elle s'appelle Default ? Ne
soyez pas paresseux et réfléchissez bien à ce qu'elle fait et pourquoi elle
est différente des autres implémentations possibles.
Et si je ne peux pas imaginer plusieurs implémentations possibles ? Et si je ne peux penser qu'à une seule manière valide ? Alors ne créez pas l'interface. Du moins pour l'instant.
Eh, il y a une autre implémentation
Et c'est ici ! Nous avons besoin d'une seconde implémentation. Cela arrive tout le temps. Il n'a jamais été nécessaire de stocker les traductions de plus d'une manière éprouvée, par exemple dans une base de données, mais il y a maintenant une nouvelle exigence et nous devons avoir plus de traducteurs dans l'application.
C'est également à ce moment-là que vous réalisez clairement quelles étaient les spécificités du traducteur unique d'origine. C'était un traducteur de base de données, sans défaut.
Et alors ?
- Faire du nom
Translator
l'interface - Renommez la classe originale en
DatabaseTranslator
et elle sera implémentéeTranslator
- Et vous créez de nouvelles classes
GettextTranslator
et peut-êtreNeonTranslator
Tous ces changements sont très pratiques et faciles à faire, surtout si
l'application est construite selon les principes de l'injection de
dépendance. Pas besoin de changer quoi que ce soit dans le code, il
suffit de changer Translator
en DatabaseTranslator
dans la configuration du conteneur DI. C'est génial !
Cependant, une situation diamétralement différente se présenterait si nous
insistions sur la préfixation/suffixation. Nous devrions renommer les types de
Translator
en TranslatorInterface
dans le code de
l'application. Un tel renommage serait purement conventionnel, mais irait à
l'encontre de l'esprit de la POO, comme nous venons de le montrer. L'interface
n'a pas changé, le code utilisateur n'a pas changé, mais la convention exige
un renommage ? Alors c'est une convention défectueuse.
De plus, s'il s'avère au fil du temps qu'une classe abstraite est meilleure que l'interface, nous renommerons à nouveau. Une telle action peut ne pas être triviale du tout, par exemple lorsque le code est réparti sur plusieurs paquets ou utilisé par des tiers.
Mais tout le monde le fait
Tout le monde ne le fait pas. Il est vrai que dans le monde PHP, le Zend Framework, suivi de Symfony, les gros joueurs, ont popularisé la distinction entre les noms d'interfaces et de classes abstraites. Cette approche a été adoptée par PSR, qui ironiquement ne publie que des interfaces, tout en incluant le mot interface dans le nom de chacune d'entre elles.
D'autre part, un autre framework majeur, Laravel, ne distingue pas les
interfaces et les classes abstraites. Même la populaire couche de base de
données Doctrine ne le fait pas, par exemple. La bibliothèque standard de PHP
ne le fait pas non plus (nous avons donc les interfaces Throwable
ou Iterator
, la classe abstraite
FilterIterator
, etc.)
Si nous regardons le monde en dehors de PHP, C# utilise le préfixe
I
pour les interfaces, alors qu'en Java ou TypeScript les noms ne
sont pas différents.
Donc tout le monde ne le fait pas, mais même si c'était le cas, cela ne veut pas dire que c'est une bonne chose. Il n'est pas sage d'adopter aveuglément ce que les autres font, car vous risquez d'adopter aussi des erreurs. Des erreurs dont les autres préfèreraient se débarrasser, car c'est un problème trop important de nos jours.
Je ne sais pas ce qu'est l'interface dans le code
De nombreux programmeurs soutiendront que les préfixes/suffixes leur sont utiles car ils leur permettent de savoir immédiatement dans le code ce que sont les interfaces. Ils estiment qu'une telle distinction leur manquerait. Voyons donc, pouvez-vous dire ce qu'est une classe et ce qu'est une interface dans ces exemples ?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
X
is always a class, Y
est une interface, c'est
sans ambiguïté, même sans préfixes/postfixes. Bien sûr, l'IDE le sait aussi
et vous donnera toujours l'indication correcte dans un contexte donné.
Mais qu'en est-il ici ?
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try { ... } catch (A $x) { ... }
Dans ces cas-là, vous ne le saurez pas. Comme nous l'avons dit au tout début, du point de vue d'un développeur, il ne devrait pas y avoir de différence entre ce qui est une classe et ce qui est une interface. C'est ce qui donne aux interfaces et aux classes abstraites leur sens.
Si vous étiez capable de faire la distinction entre une classe et une interface ici, cela nierait le principe de base de la POO. Et les interfaces n'auraient plus aucun sens.
J'ai l'habitude
Changer ses habitudes, ça fait mal 🙂 Souvent, c'est l'idée même qui fait mal. Ne blâmons pas les personnes qui sont attirées par les changements et les attendent avec impatience, car pour la plupart, comme le dit le proverbe tchèque, l'habitude est une chemise de fer.
Mais il suffit de regarder le passé, comment certaines habitudes ont été
balayées par le temps. La plus célèbre est peut-être la notation dite
hongroise utilisée depuis les années 80 et popularisée par Microsoft. Cette
notation consistait à commencer le nom de chaque variable par une abréviation
symbolisant son type de données. En PHP, cela donnerait
echo $this->strName
ou $this->intCount++
. La
notation hongroise a commencé à être abandonnée dans les années 90, et
aujourd'hui, les directives de Microsoft découragent directement les
développeurs de l'utiliser.
Elle était autrefois une fonctionnalité essentielle et aujourd'hui, elle ne manque à personne.
Mais pourquoi revenir à une époque aussi lointaine ? Vous vous souvenez peut-être qu'en PHP, il était d'usage de distinguer les membres non publics des classes par un trait de soulignement (sample from Zend Framework). C'était à l'époque de PHP 5, qui avait des modificateurs de visibilité public/protégé/privé. Mais les programmeurs le faisaient par habitude. Ils étaient convaincus que sans les underscores, ils ne comprendraient plus le code. “Comment pourrais-je distinguer les propriétés publiques des propriétés privées dans le code, hein ?”
Personne n'utilise les underscores aujourd'hui. Et ils ne manquent à personne. Le temps a parfaitement prouvé que ces craintes étaient fausses.
Pourtant, c'est exactement la même chose que l'objection “Comment pourrais-je distinguer une interface d'une classe dans le code, hein ?”.
J'ai arrêté d'utiliser les préfixes/postfixes il y a dix ans. Je ne reviendrai jamais en arrière, c'était une excellente décision. Je ne connais aucun autre programmeur qui souhaite revenir en arrière non plus. Comme l'a dit un ami : “Essayez et dans un mois, vous ne comprendrez pas que vous ayez fait les choses différemment.”
Je veux maintenir la cohérence
Je peux imaginer un programmeur disant : “Utiliser des préfixes et des suffixes est vraiment un non-sens, je comprends, c'est juste que mon code est déjà construit de cette façon et qu'il est très difficile de le changer. Et si je commence à écrire correctement du nouveau code sans eux, je vais me retrouver avec de l'incohérence, ce qui est peut-être encore pire qu'une mauvaise convention.”
En fait, votre code est déjà incohérent parce que vous utilisez la bibliothèque système de PHP :
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
Et la main sur le coeur, est-ce important ? N'avez-vous jamais pensé que ce serait plus cohérent ?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Ou ça ?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Je ne le pense pas. La cohérence ne joue pas un rôle aussi important qu'il n'y paraît. Au contraire, l'œil préfère moins de bruit visuel, le cerveau préfère la clarté de la conception. Il est donc logique d'ajuster la convention et de commencer à écrire correctement les nouvelles interfaces sans préfixes ni suffixes.
Ils peuvent être délibérément supprimés, même dans les grands projets. Un exemple est le Nette Framework, qui utilisait historiquement des préfixes I dans les noms d'interface, qu'il a commencé à éliminer progressivement il y a quelques années, tout en maintenant une compatibilité totale avec le passé.
Pour soumettre un commentaire, veuillez vous connecter