Deux définitions contradictoires du principe de ségrégation d'interface - laquelle est correcte?

14

Lors de la lecture d'articles sur les FAI, il semble y avoir deux définitions contradictoires des FAI:

Selon la première définition (voir 1 , 2 , 3 ), le FAI déclare que les classes implémentant l'interface ne devraient pas être forcées d'implémenter des fonctionnalités dont elles n'ont pas besoin. Ainsi, la grosse interfaceIFat

interface IFat
{
     void A();
     void B();
     void C();
     void D();
}

class MyClass: IFat
{ ... }

devrait être divisé en interfaces plus petites ISmall_1etISmall_2

interface ISmall_1
{
     void A();
     void B();
}

interface ISmall_2
{
     void C();
     void D();
}

class MyClass:ISmall_2
{ ... }

puisque de cette façon mon MyClass est en mesure de mettre en œuvre que les méthodes dont il a besoin ( D()et C()), sans être obligé de fournir également des implémentations fictives pour A(), B()et C():

Mais selon la deuxième définition (voir 1 , 2 , réponse de Nazar Merza ), le FAI déclare que l' MyClientappel de méthodes sur MyServicene devrait pas être conscient des méthodes surMyService lesquelles il n'a pas besoin. En d'autres termes, si MyClientseulement besoin de la fonctionnalité de C()et D(), alors au lieu de

class MyService 
{
    public void A();
    public void B();
    public void C();
    public void D();
}

/*client code*/      
MyService service = ...;
service.C(); 
service.D();

nous devons séparer MyService's méthodes en interfaces spécifiques au client :

public interface ISmall_1
{
     void A();
     void B();
}

public interface ISmall_2
{
     void C();
     void D();
}

class MyService:ISmall_1, ISmall_2 
{ ... }

/*client code*/
ISmall_2 service = ...;
service.C(); 
service.D();

Ainsi, avec la première définition, le but du FAI est de " rendre la vie des classes implémentant l'interface IFat plus facile ", tandis qu'avec la seconde, le but du FAI est de " rendre la vie des clients appelant les méthodes de MyService plus facile ".

Laquelle des deux définitions différentes du FAI est réellement correcte?

@MARJAN VENEMA

1.

Ainsi, lorsque vous allez diviser IFat en une interface plus petite, quelles méthodes finissent dans quelle ISmallinterface doit être décidée en fonction de la cohésion des membres.

Bien qu'il soit logique de mettre des méthodes cohésives dans la même interface, je pensais qu'avec le modèle ISP, les besoins du client priment sur la "cohésion" d'une interface. En d'autres termes, je pensais qu'avec le FAI, nous devrions regrouper dans la même interface les méthodes nécessaires à des clients particuliers, même si cela signifie exclure de cette interface les méthodes qui devraient, par souci de cohésion, également être placées dans cette même interface?

Ainsi, s'il y avait beaucoup de clients qui ne jamais besoin d'appeler CutGreens, mais pas aussi GrillMeat, puis à respecter le modèle FAI nous ne devrions mettre à l' CutGreensintérieur ICook, mais pas aussi GrillMeat, même si les deux méthodes sont très cohésif ?!

2.

Je pense que votre confusion provient d'une hypothèse cachée dans la première définition: que les classes d'implémentation suivent déjà le principe de responsabilité unique.

Par «implémentation de classes ne suivant pas SRP», faites-vous référence aux classes qui implémentent IFatou aux classes qui implémentent ISmall_1/ ISmall_2? Je suppose que vous faites référence aux classes qui implémentent IFat? Si oui, pourquoi supposez-vous qu'ils ne suivent pas déjà le SRP?

Merci

EdvRusj
la source
4
Pourquoi ne peut-il pas y avoir plusieurs définitions qui sont toutes deux servies par le même principe?
Bobson
5
Ces définitions ne se contredisent pas.
Mike Partridge
1
Non, bien sûr, les besoins du client ne prévalent pas sur la cohésion d'une interface. Vous pouvez aller loin dans cette "règle" et vous retrouver avec des interfaces à méthode unique partout qui n'ont absolument aucun sens. Arrêtez de suivre les règles et commencez à penser aux objectifs pour lesquels ces règles ont été créées. Avec "classes ne suivant pas SRP", je ne parlais pas de classes spécifiques dans votre exemple ou du fait qu'elles ne suivaient pas déjà SRP. Lire à nouveau. La première définition ne conduit à fractionner une interface que si l'interface ne suit pas ISP et que la classe suit SRP.
Marjan Venema
2
La deuxième définition ne se soucie pas des implémenteurs. Il définit les interfaces du point de vue des appelants et ne fait aucune hypothèse quant à savoir si les implémenteurs existent déjà ou non. Cela suppose probablement que lorsque vous suivez ISP et que vous venez d'implémenter ces interfaces, vous suivrez bien sûr SRP lors de leur création.
Marjan Venema
2
Comment savez-vous à l'avance quels clients existeront et quelles méthodes auront-ils besoin? Tu ne peux pas. Ce que vous pouvez savoir à l'avance, c'est la cohésion de votre interface.
Tulains Córdova

Réponses:

6

Les deux sont corrects

D'après ma lecture, le but de l'ISP (Interface Segregation Principle) est de garder les interfaces petites et concentrées: tous les membres de l'interface devraient avoir une cohésion très élevée. Les deux définitions visent à éviter les interfaces "jack-of-all-trades-master-of-none".

La ségrégation des interfaces et le principe de responsabilité unique (SRP) ont le même objectif: garantir de petits composants logiciels hautement cohésifs. Ils se complètent. La ségrégation des interfaces garantit que les interfaces sont petites, ciblées et hautement cohésives. Suivre le principe de la responsabilité unique garantit que les classes sont petites, ciblées et très cohésives.

La première définition que vous mentionnez se concentre sur les implémenteurs, la seconde sur les clients. Ce qui, contrairement à @ user61852, je considère être les utilisateurs / appelants de l'interface, pas les implémenteurs.

Je pense que votre confusion provient d'une hypothèse cachée dans la première définition: que les classes d'implémentation suivent déjà le principe de responsabilité unique.

Pour moi, la deuxième définition, avec les clients comme appelants de l'interface, est un meilleur moyen d'atteindre l'objectif visé.

Séparation

Dans votre question, vous déclarez:

puisque de cette façon ma MyClass ne peut implémenter que les méthodes dont elle a besoin (D () et C ()), sans être forcée de fournir également des implémentations factices pour A (), B () et C ():

Mais cela bouleverse le monde.

  • Une classe implémentant une interface ne dicte pas ce dont elle a besoin dans l'interface qu'elle implémente.
  • Les interfaces dictent les méthodes qu'une classe d'implémentation doit fournir.
  • Les appelants d'une interface sont vraiment ceux qui dictent quelles fonctionnalités ils ont besoin de l'interface pour leur fournir et donc ce qu'un implémenteur doit fournir.

Ainsi, lorsque vous allez vous diviser IFaten une interface plus petite, quelles méthodes se retrouvent dans quelle ISmallinterface doit être décidée en fonction de la cohésion des membres.

Considérez cette interface:

interface IEverythingButTheKitchenSink
{
     void DoDishes();
     void CleanSink();
     void CutGreens();
     void GrillMeat();
}

Quelles méthodes mettriez-vous en place ICooket pourquoi? Souhaitez-vous mettre en CleanSinkplace GrillMeatsimplement parce que vous avez une classe qui fait exactement cela et quelques autres choses, mais rien comme les autres méthodes? Ou le diviseriez-vous en deux interfaces plus cohésives, telles que:

interface IClean
{
     void DoDishes();
     void CleanSink();
}

interface ICook
{
     void CutGreens();
     void GrillMeat();
}

Note de déclaration d'interface

Une définition d'interface devrait de préférence être seule dans une unité distincte, mais si elle doit absolument vivre avec l'appelant ou l'implémenteur, elle devrait vraiment l'être avec l'appelant. Sinon, l'appelant obtient une dépendance immédiate vis-à-vis de l'implémenteur qui va complètement à l'encontre de l'objectif des interfaces. Voir aussi: Déclarer l'interface dans le même fichier que la classe de base, est-ce une bonne pratique? sur les programmeurs et pourquoi devrions-nous placer des interfaces avec les classes qui les utilisent plutôt que celles qui les implémentent? sur StackOverflow.

Marjan Venema
la source
1
Pouvez-vous voir la mise à jour que j'ai faite?
EdvRusj
"l'appelant obtient une dépendance immédiate de l'implémenteur " ... uniquement si vous violez le DIP (principe d'inversion de dépendance), si les variables internes de l'appelant, les paramètres, les valeurs de retour, etc. sont de type ICookau lieu de type SomeCookImplementor, comme les mandats DIP, alors il ne pas dépendre SomeCookImplementor.
Tulains Córdova
@ user61852: Si la déclaration d'interface et l'implémenteur sont dans la même unité, j'obtiens immédiatement une dépendance sur cet implémenteur. Pas nécessairement au moment de l'exécution, mais très certainement au niveau du projet, simplement par le fait qu'il est là. Le projet ne peut plus être compilé sans lui ou quoi qu'il utilise. De plus, l'injection de dépendance n'est pas la même que le principe d'inversion de dépendance. Vous pourriez être intéressé par DIP dans la nature
Marjan Venema
J'ai réutilisé vos exemples de code dans cette question programmers.stackexchange.com/a/271142/61852 , en l'améliorant après qu'il ait déjà été accepté. Je vous ai rendu hommage pour les exemples.
Tulains Córdova
Cool @ user61852 :) (et merci pour le crédit)
Marjan Venema
14

Vous confondez le mot «client» tel qu'il est utilisé dans les documents Gang of Four avec un «client» comme consommateur de service.

Un "client", comme le veulent les définitions de Gang of Four, est une classe qui implémente une interface. Si la classe A implémente l'interface B, alors ils disent que A est un client de B. Sinon, l'expression "les clients ne devraient pas être forcés d'implémenter des interfaces qu'ils n'utilisent pas" n'aurait pas de sens puisque "les clients" (comme chez les consommateurs) ne 'implémentez rien. L'expression n'a de sens que lorsque vous voyez "client" comme "implémenteur".

Si "client" signifiait une classe qui "consomme" (appelle) les méthodes d'une autre classe qui implémente la grande interface, alors en appelant les deux méthodes qui vous intéressent et en ignorant le reste, cela suffirait pour vous garder découplé du reste de les méthodes que vous n'utilisez pas.

L'esprit du principe est d'éviter que le "client" (la classe implémentant l'interface) n'ait à implémenter des méthodes factices afin de se conformer à l'ensemble de l'interface lorsqu'il ne se soucie que d'un ensemble de méthodes liées.

Il vise également à avoir le moins de couplage possible afin que les modifications apportées en un seul endroit entraînent le moins d'impact. En séparant les interfaces, vous réduisez le couplage.

Ce problème apparaît lorsque l'interface fait trop et a des méthodes qui devraient être divisées en plusieurs interfaces au lieu d'une seule.

Vos deux exemples de code sont OK . C'est seulement que dans le second, vous supposez que "client" signifie "une classe qui consomme / appelle les services / méthodes offerts par une autre classe".

Je ne trouve aucune contradiction dans les concepts expliqués dans les trois liens que vous avez donnés.

Gardez simplement à l'esprit que "client" est un implémenteur , dans SOLID talk.

Tulains Córdova
la source
Mais selon @pdr, alors que les exemples de code dans tous les liens adhèrent au FAI, la définition du FAI vise davantage à "isoler le client (une classe qui appelle des méthodes d'une autre classe) de mieux connaître le service" que " éviter que les clients (implémenteurs) soient obligés d'implémenter des interfaces qu'ils n'utilisent pas. "
EdvRusj
1
@EdvRusj Ma réponse est basée sur les documents du site Web Object Mentor (entreprise Bob Martin), écrits par Martin lui-même lorsqu'il faisait partie du célèbre Gang of Four. Comme vous le savez, Gnag of Four était un groupe d'ingénieurs logiciels, dont Martin, qui a inventé l'acronyme SOLID, identifié et documenté les principes. docs.google.com/a/cleancoder.com/file/d/…
Tulains Córdova
Vous êtes donc en désaccord avec @pdr et vous trouvez donc la première définition de FAI (voir mon article d'origine) plus agréable?
EdvRusj
@EdvRusj Je pense que les deux ont raison. Mais le second ajoute une confusion inutile en utilisant la métaphore client / serveur. Si je devais en choisir un, j'irais avec le Gang of Four officiel, qui est le premier. Mais ce qui est important c'est de réduire le couplage et les dépendances inutiles qui est l'esprit des principes SOLID après tout. Peu importe lequel a raison. L'important est de séparer les interfaces selon les comportements. C'est tout. Mais en cas de doute, allez à la source d'origine.
Tulains Córdova
3
Je suis donc en désaccord avec votre affirmation selon laquelle "client" est le réalisateur dans SOLID Talk. D'une part, il est absurde linguistique d'appeler un fournisseur (implémenteur) un client de ce qu'il fournit (implémentant). Je n'ai également vu aucun article sur SOLID qui essaie de transmettre cela, mais je l'ai peut-être simplement manqué. Plus important encore, il définit l'implémenteur d'une interface comme celui qui décide de ce qui devrait être dans l'interface. Et cela n'a aucun sens pour moi. Les appelants / utilisateurs d'une interface définissent ce dont ils ont besoin d'une interface et les implémenteurs (pluriel) de cette interface sont tenus de le fournir.
Marjan Venema
5

Le FAI consiste à isoler le client de mieux connaître le service qu'il ne devrait en savoir (le protéger contre des changements indépendants, par exemple). Votre deuxième définition est correcte. À ma lecture, un seul de ces trois articles suggère le contraire ( le premier ) et c'est tout simplement faux. (Edit: Non, pas mal, juste trompeur.)

La première définition est beaucoup plus étroitement liée au LSP.

pdr
la source
3
Dans ISP, les clients ne devraient pas être obligés de CONSOMMER les composants d'interface qu'ils n'utilisent pas. Dans LSP, SERVICES ne devrait pas être forcé d'implémenter la méthode D car le code appelant nécessite la méthode A. Ils ne sont pas contradictoires, ils sont complémentaires.
pdr
2
@EdvRusj, l'objet qui implémente InterfaceA que ClientA appelle peut en fait être exactement le même objet qui implémente InterfaceB nécessaire au client B.Dans les rares cas où le même client doit voir le même objet que différentes classes, le code ne sera pas habituellement "toucher". Vous le considérerez comme un A pour un but et un B pour l'autre.
Amy Blankenship
1
@EdvRusj: Cela pourrait aider si vous repensez votre définition d'interface ici. Ce n'est pas toujours une interface en termes C # / Java. Vous pouvez avoir un service complexe avec un certain nombre de classes simples qui l'entourent, de sorte que le client A utilise la classe wrapper AX pour "s'interfacer" avec le service X. Ainsi, lorsque vous modifiez X d'une manière qui affecte A et AX, vous n'êtes pas forcé d'affecter BX et B.
pdr
1
@EdvRusj: Il serait plus exact de dire que A et B ne se soucient pas s'ils appellent tous les deux X ou l'un appelle Y et l'autre appelle Z. C'EST le point fondamental du FAI. Vous pouvez donc choisir l'implémentation que vous recherchez et changer facilement d'avis plus tard. ISP ne favorise pas une route ou l'autre, mais LSP et SRP le pourraient.
pdr
1
@EdvRusj Non, le client A pourrait remplacer le service X par le service y, qui implémenteraient tous deux l'interface AX. X et / ou Y peuvent implémenter d'autres Interfaces, mais lorsque le Client les appelle en tant que AX, il ne se soucie pas de ces autres Interfaces.
Amy Blankenship