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

il y a 1 an de David Grudl  

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 ?

  1. Faire du nom Translator l'interface
  2. Renommez la classe originale en DatabaseTranslator et elle sera implémentée Translator
  3. Et vous créez de nouvelles classes GettextTranslator et peut-être NeonTranslator

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é.