Existe-t-il un principe d'interface «ne demandez que ce dont vous avez besoin»?

9

J'ai grandi en utilisant un principe de conception et de consommation d'interfaces qui dit essentiellement: «ne demandez que ce dont vous avez besoin»

Par exemple, si j'ai un tas de types qui peuvent être supprimés, je ferai une Deletableinterface:

interface Deletable {
   void delete();
}

Ensuite, je peux écrire une classe générique:

class Deleter<T extends Deletable> {
   void delete(T t) {
      t.delete();
   }
}

Ailleurs dans le code, je demanderai toujours la plus petite responsabilité possible pour répondre aux besoins du code client. Donc, si je dois seulement supprimer un File, je demanderai toujours un Deletable, pas un File.

Ce principe est-il de notoriété publique et a-t-il déjà un nom accepté? Est-ce controversé? Est-ce discuté dans les manuels?

glenviewjeff
la source
1
Accouplement lâche peut-être? Ou des interfaces étroites?
tdammers

Réponses:

16

Je crois que cela fait référence à ce que Robert Martin appelle le principe de ségrégation d'interface . Les interfaces sont séparées en petites et concises afin que les consommateurs (clients) n'aient qu'à connaître les méthodes qui les intéressent. Vous pouvez en savoir plus sur SOLID .

Vadim
la source
4

Pour développer la très bonne réponse de Vadim, je répondrai à la question "est-ce controversé" par "non, pas vraiment".

En général, la ségrégation des interfaces est une bonne chose, car elle réduit le nombre global de «raisons de changer» des différents objets impliqués. Le principe de base est que lorsqu'une interface avec plusieurs méthodes doit être modifiée, par exemple pour ajouter un paramètre à l'une des méthodes d'interface, tous les consommateurs de l'interface doivent au moins être recompilés, même s'ils n'ont pas utilisé la méthode qui a changé.. "Mais c'est juste une recompilation!", Je vous entends dire; cela peut être vrai, mais gardez à l'esprit qu'en général, tout ce que vous recompilez doit être expulsé dans le cadre d'un correctif logiciel, quelle que soit l'importance de la modification du binaire. Ces règles ont été conçues à l'origine au début des années 90, lorsque la station de travail de bureau moyenne était moins puissante que le téléphone dans votre poche, la connexion à distance de 14,4 bauds était flamboyante et 3,5 "1,44 Mo de" disquettes "étaient le principal support amovible. Même à l'ère actuelle de la 3G / 4G, les utilisateurs d'Internet sans fil ont souvent des plans de données avec des limites, donc lors de la publication d'une mise à niveau, moins il y a de binaires à télécharger, mieux c'est.

Cependant, comme toutes les bonnes idées, la ségrégation d'interface peut mal tourner si elle n'est pas correctement mise en œuvre. Tout d'abord, il est possible qu'en séparant les interfaces tout en gardant l'objet qui implémente ces interfaces (remplissant les dépendances) relativement inchangé, vous pouvez vous retrouver avec une "Hydra", un parent de l'anti-modèle "God Object" où le la nature omnisciente et toute-puissante de l'objet est cachée aux dépendants par les interfaces étroites. Vous vous retrouvez avec un monstre à plusieurs têtes qui est au moins aussi difficile à entretenir que l'Objet divin le serait, plus les frais généraux liés à la maintenance de toutes ses interfaces. Il n'y a pas un nombre fixe d'interfaces que vous ne devriez pas dépasser, mais chaque interface que vous implémentez sur un seul objet doit être préfacée en répondant à la question "Cette interface contribue-t-elle à l'objet"

Deuxièmement, une interface par méthode peut ne pas être nécessaire, malgré ce que SRP peut vous dire. Vous pouvez vous retrouver avec "code ravioli"; tant de morceaux de la taille d'une bouchée qu'il est difficile de retracer pour savoir exactement où les choses se produisent réellement. Il n'est pas non plus nécessaire de diviser une interface avec deux méthodes si tous les utilisateurs actuels de cette interface ont besoin des deux méthodes. Même si l'une des classes dépendantes n'a besoin que d'une des deux méthodes, il est généralement acceptable de ne pas diviser l'interface si ses méthodes ont conceptuellement une cohésion très élevée (de bons exemples sont les "méthodes antonymiques" qui sont exactement opposées les unes aux autres).

La ségrégation d'interface doit être basée sur les classes qui dépendent de l'interface:

  • S'il n'y a qu'une seule classe dépendante de l'interface, ne séparez pas. Si la classe n'utilise pas une ou plusieurs des méthodes d'interface, et qu'elle est le seul consommateur de l'interface, il y a de fortes chances que vous n'ayez pas dû exposer ces méthodes en premier lieu.

  • S'il y a plus d'une classe qui dépend de l'interface et que toutes les personnes à charge utilisent toutes les méthodes de l'interface, ne séparez pas; si vous devez changer l'interface (pour ajouter une méthode ou modifier une signature), tous les consommateurs actuels seront affectés par la modification, que vous les sépariez ou non (bien que si vous ajoutez une méthode dont au moins une personne à charge n'aura pas besoin, envisagez soigneusement si le changement doit être implémenté en tant que nouvelle interface, héritant éventuellement de celle existante).

  • S'il existe plusieurs classes dépendantes de l'interface et qu'elles n'utilisent pas toutes les mêmes méthodes, c'est un candidat à la ségrégation. Regardez la "cohérence" de l'interface; toutes les méthodes poursuivent-elles un seul objectif de programmation très spécifique? Si vous pouvez identifier plus d'un objectif principal pour l'interface (et ses implémenteurs), envisagez de fractionner les interfaces le long de ces lignes pour créer des interfaces plus petites avec moins de «raisons de changer».

KeithS
la source
Il est également intéressant de noter que la ségrégation d'interface peut être fine et dandy si l'on utilise un langage / système OOP qui peut permettre au code de spécifier une combinaison précise d'interfaces, mais au moins en .NET, ils peuvent causer de graves maux de tête, car il n'y a pas de décent manière de spécifier une collection de "choses qui implémentent IFoo et IBar, mais qui autrement pourraient n'avoir rien en commun".
supercat
Les paramètres de type générique peuvent être définis avec des critères incluant l'implémentation de plusieurs interfaces, mais vous avez raison en ce que les expressions nécessitant un type statique ne peuvent généralement pas prendre en charge la spécification de plusieurs. S'il est nécessaire qu'un type statique implémente à la fois IFoo et IBar, et que vous contrôlez ces deux interfaces, il peut être judicieux de l'implémenter IBaz : IFoo, IBaret de l'exiger à la place.
KeithS
Si le code client peut avoir besoin de quelque chose qui peut être utilisé au fur IFooet à mesure IBar, la définition d'un composite IFooBarpeut être une bonne idée, mais si les interfaces sont finement divisées, il est facile de finir par nécessiter des dizaines de types d'interface distincts. Considérez les fonctionnalités suivantes que les collections peuvent avoir: énumérer, rapporter le nombre, lire le nième élément, écrire le nième élément, insérer avant le nième élément, supprimer le nième élément, nouvel élément (agrandir la collection et retourner l'index du nouvel espace) et ajouter. Neuf méthodes: ECRWIDNA. Je pourrais probablement décrire des dizaines de types qui prendraient naturellement en charge de nombreuses combinaisons différentes.
supercat
Les tableaux, par exemple, prendraient en charge ECRW. Un arrayliste soutiendrait ECRWIDNA. Une liste thread-safe peut prendre en charge ECRWNA [bien que A ne soit généralement utile que pour préremplir la liste]. Un wrapper de tableau en lecture seule peut prendre en charge ECR. Une interface de liste covariante pourrait prendre en charge ECRD. Une interface non générique pourrait fournir une prise en charge de type C ou CD. Si Swap était une option, certains types pourraient prendre en charge CS mais pas D (par exemple les tableaux) tandis que d'autres prendraient en charge CDS. Essayer de définir des types d'interface distincts pour chaque combinaison de capacités nécessaire serait un cauchemar.
supercat
Imaginez maintenant que l'on souhaite pouvoir envelopper une collection avec un objet qui peut faire tout ce que la collection peut faire, mais qui enregistre chaque transaction. De combien de wrappers aurait-on besoin? Si toutes les collections héritaient d'une interface commune qui comprenait des propriétés pour identifier leurs capacités, un wrapper suffirait. Si toutes les interfaces sont distinctes, cependant, il faudrait des dizaines.
supercat