Les préfixes et suffixes n'ont pas leur place dans les noms d'interface

il y a 3 ans par David Grudl  

L'utilisation du préfixe I ou du suffixe Interface pour les interfaces, ainsi que Abstract pour les classes abstraites, est un antipattern. Il n'a rien à faire dans du code propre. La distinction des noms d'interface obscurcit en réalité les principes de la POO, introduit du bruit dans le code et complique le développement. Les raisons sont les suivantes.

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, il n'y a aucune différence du point de vue du développeur entre le fait que le type sur lequel il s'appuie soit une classe ou une interface. C'est une chose formidable, grâce à laquelle les interfaces sont en fait si utiles. 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 réfléchir.)

Regardez 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 se fiche complètement de savoir s'il reçoit un objet GettextTranslator ou DatabaseTranslator. Et en même temps, en tant qu'utilisateur, je me fiche complètement de savoir si Translator est une interface, une classe abstraite ou une classe concrète.

Je m'en fiche complètement ? En fait non, j'avoue que je suis assez curieux, alors quand j'examine une bibliothèque étrangère, je jette un coup d'œil à ce qui se cache derrière le type, et je passe la souris dessus :

Ah, maintenant je sais. Et ça s'arrête là. Savoir s'il s'agit d'une classe ou d'une interface serait important si je voulais créer son instance, mais ce n'est pas le cas, je parle juste du type de la variable. Et ici, je veux être isolé de ces détails. Et je ne veux surtout pas les introduire dans mon code ! Ce qui se cache derrière le type fait partie de sa définition, pas du type lui-même.

Et maintenant, regardez cet 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'un formulaire abstrait et d'une interface de traducteur.” Mais c'est une bêtise. Il a besoin d'un formulaire concret à afficher. Pas d'un formulaire abstrait. Et il a besoin d'un objet qui remplit le rôle de 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… sérieusement ? Pensez-vous vraiment que c'est une bonne idée d'introduire dans les conventions de nommage l'utilisation de mots qui doivent être ignorés ?

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 en héritant, que signifie alors TranslatorInterface ? » Il n'y a pas de réponse raisonnable à cela.

Lorsque nous écrivons TranslatorInterface, bien que Translator puisse également être une interface, nous commettons une tautologie. Idem lorsque nous déclarons interface TranslatorInterface. Et ainsi de suite. Jusqu'à ce qu'une blague de programmeur apparaisse :

interface TranslatorInterface
{
}

class FormRendererClass
{
	/**
	 * Constructeur
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

Implémentation exceptionnelle

Quand je vois quelque chose comme TranslatorInterface, il est probable qu'il existera aussi une implémentation nommée Translator implements TranslatorInterface. Cela me fait réfléchir : qu'est-ce qui rend Translator si exceptionnel qu'il a le droit unique de s'appeler Translator ? Toute autre implémentation a besoin d'un nom descriptif, par exemple GettextTranslator ou DatabaseTranslator, mais celle-ci est en quelque sorte “par défaut”, comme l'indique sa position privilégiée, puisqu'elle s'appelle Translator sans qualificatif.

Cela rend même les gens incertains et ils ne savent pas s'ils doivent écrire un typehint pour Translator ou TranslatorInterface. Dans le code client, les deux se mélangent alors, vous l'avez certainement déjà rencontré de nombreuses fois (dans Nette par exemple en relation avec Nette\Http\Request vs IRequest).

Ne serait-il pas préférable de se débarrasser de l'implémentation exceptionnelle et de conserver le nom générique Translator pour l'interface ? C'est-à-dire avoir des implémentations concrètes avec un nom concret + une interface générique avec un nom générique. Cela a du sens, n'est-ce pas ?

Le fardeau du 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, je suis moi-même coupable. Mais encore une fois, qu'est-ce qui la rend si exceptionnelle qu'elle s'appelle Default ? Ne soyez pas paresseux et réfléchissez bien à ce qu'elle fait et pourquoi cela diffère des autres implémentations possibles.

Et si je ne peux pas imaginer plusieurs implémentations ? Et si une seule méthode valide me vient à l'esprit ? Alors ne créez tout simplement pas d'interface. Du moins pour l'instant.

Tiens, une autre implémentation est apparue

Et voilà ! Nous avons besoin d'une deuxième implémentation. Cela arrive couramment. Il n'y a jamais eu besoin de stocker les traductions autrement que d'une seule manière éprouvée, par exemple dans une base de données, mais maintenant une nouvelle exigence est apparue et il faut avoir plusieurs traducteurs dans l'application.

C'est aussi le moment où vous réalisez clairement quelle était la spécificité du traducteur unique d'origine. C'était un traducteur de base de données, pas un traducteur par défaut.

Que faire ?

  1. Faites du nom Translator une interface
  2. Renommez la classe d'origine en DatabaseTranslator et faites-la implémenter Translator
  3. Et créez de nouvelles classes GettextTranslator et peut-être NeonTranslator

Tous ces changements se font très confortablement et facilement, surtout si l'application est construite conformément aux principes de l'injection de dépendances. Il n'est pas nécessaire de modifier quoi que ce soit dans le code, seulement dans la configuration du conteneur DI, nous changeons Translator en DatabaseTranslator. C'est génial !

Une situation diamétralement différente se produirait cependant si nous insistions sur le préfixage/suffixage. Nous devrions renommer les types de Translator en TranslatorInterface dans toute l'application. Un tel renommage serait purement fonctionnel pour respecter la convention, mais irait à l'encontre du sens de la POO, comme nous l'avons montré tout à l'heure. L'interface n'a pas changé, le code utilisateur n'a pas changé, mais la convention exige de renommer ? Alors c'est une mauvaise convention.

De plus, si avec le temps il s'avérait qu'une classe abstraite serait meilleure qu'une interface, nous renommerions à nouveau. Une telle intervention peut ne pas être triviale du tout, par exemple si le code est réparti sur plusieurs paquets ou s'il est utilisé par des tiers.

Mais tout le monde le fait comme ça

Pas tout le monde. Il est vrai que dans le monde PHP, la distinction des noms des interfaces et des classes abstraites a été popularisée par Zend Framework puis par Symfony, c'est-à-dire de grands acteurs. Cette approche a également été adoptée par PSR, qui paradoxalement ne publie que des interfaces, et pourtant indique pour chacune le mot interface dans le nom.

D'un autre côté, un autre framework important, Laravel, ne distingue en aucune façon les interfaces et les classes abstraites. La populaire couche de base de données Doctrine ne le fait pas non plus, par exemple. Et la bibliothèque standard de PHP ne le fait pas non plus (nous avons ainsi les interfaces Throwable ou Iterator, la classe abstraite FilterIterator, etc.).

Si nous regardions en dehors du monde PHP, par exemple C# utilise le préfixe I pour les interfaces, tandis qu'en Java ou TypeScript, les noms ne sont pas distingués.

Donc, tout le monde ne le fait pas, mais même s'ils le faisaient, cela ne signifie pas que c'est bien. Adopter sans réfléchir ce que font les autres n'est pas raisonnable, car vous pouvez aussi adopter des erreurs. Des erreurs dont l'autre aimerait peut-être beaucoup se débarrasser lui-même, mais c'est trop difficile.

Je ne reconnais pas dans le code ce qui est une interface

De nombreux programmeurs objecteront que les préfixes/suffixes leur sont utiles, car grâce à eux, ils reconnaissent immédiatement dans le code ce qui sont des interfaces. Ils ont l'impression qu'une telle distinction leur manquerait. Voyons voir, reconnaissez-vous dans ces exemples ce qui est une classe et ce qui est une interface ?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X est toujours une classe, Y est une interface, c'est sans ambiguïté même sans préfixes/postfixes. Bien sûr, l'IDE le sait aussi et dans le contexte donné, il vous suggérera toujours correctement.

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, vous ne le reconnaîtrez pas. Comme nous l'avons dit au tout début, il ne devrait pas y avoir de différence du point de vue du développeur entre ce qui est une classe et ce qui est une interface. C'est précisément ce qui donne un sens aux interfaces et aux classes abstraites.

Si vous étiez capable de distinguer ici une classe d'une interface, cela nierait le principe fondamental de la POO. Et les interfaces perdraient leur sens.

J'y suis habitué

Changer ses habitudes fait mal, tout simplement 🙂 Combien de fois même l'idée. Mais ne soyons pas injustes, de nombreuses personnes sont au contraire attirées par les changements et s'en réjouissent, mais pour la plupart, l'habitude est une seconde nature.

Mais il suffit de regarder dans le passé, comment certaines habitudes ont été emportées par le temps. La plus célèbre est probablement la notation hongroise utilisée depuis les années 80 et popularisée par Microsoft. La notation consistait en ce que le nom de chaque variable commençait par une abréviation symbolisant son type de données. En PHP, cela ressemblerait à ceci echo $this->strName ou $this->intCount++. On a commencé à abandonner la notation hongroise dans les années 90 et aujourd'hui, Microsoft dans ses directives décourage directement les développeurs de l'utiliser.

Autrefois, c'était indispensable et aujourd'hui, cela ne manque à personne.

Mais pourquoi remonter si loin dans le passé ? 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 (exemple de Zend Framework). C'était à l'époque où PHP 5 existait depuis longtemps, qui avait les modificateurs de visibilité public/protected/private. Mais les programmeurs le faisaient par habitude. Ils étaient convaincus que sans les traits de soulignement, ils cesseraient de s'orienter dans le code. « Comment reconnaîtrais-je dans le code les variables publiques des privées, hein ? »

Aujourd'hui, personne n'utilise les traits de soulignement. Et ils ne manquent à personne. Le temps a parfaitement prouvé que les craintes étaient vaines.

Pourtant, c'est exactement la même chose que l'objection : « Comment reconnaîtrais-je dans le code une interface d'une classe, hein ? »

J'ai arrêté d'utiliser les préfixes/postfixes il y a dix ans. Je ne reviendrais jamais en arrière, c'était une excellente décision. Je ne connais aucun autre programmeur qui voudrait revenir en arrière. Comme l'a dit un ami : « Essayez-le et dans un mois, vous ne comprendrez pas comment vous avez pu faire autrement. »

Je veux maintenir la cohérence

Je peux imaginer qu'un programmeur se dise : « Utiliser des préfixes et des suffixes est vraiment absurde, je le comprends, mais j'ai déjà construit mon code comme ça et le changement est très difficile. Et si je commençais à écrire le nouveau code correctement sans eux, j'aurais une incohérence, ce qui est peut-être encore pire qu'une mauvaise convention. »

En réalité, votre code est déjà incohérent, car vous utilisez la bibliothèque système PHP, qui n'a pas de préfixes ni de postfixes :

class Collection implements ArrayAccess, Countable, IteratorAggregate
{
	public function add(string|Stringable $item): void
	{
	}
}

Et honnêtement, est-ce que ça dérange ? Avez-vous déjà pensé que ce serait plus cohérent comme ça ?

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 pense pas. La cohérence ne joue pas un rôle aussi important qu'il pourrait y paraître. Au contraire, l'œil préfère moins de bruit visuel, le cerveau la pureté de la conception. Donc, modifier la convention et commencer à écrire de nouvelles interfaces correctement sans préfixes ni suffixes a du sens.

Il est possible de les supprimer délibérément même dans de grands projets. Un exemple est Nette Framework, qui utilisait historiquement les préfixes I dans les noms d'interface, dont il a commencé à se débarrasser progressivement et en conservant pleinement la compatibilité ascendante il y a quelques années.