Le principe de moindre connaissance

32

Je comprends le motif qui sous-tend le principe de moindre connaissance , mais je trouve certains inconvénients si je tente de l’appliquer dans ma conception.

L'un des exemples de ce principe (en fait, comment ne pas l'utiliser), que j'ai trouvé dans le livre Head First Design Patterns, spécifie qu'il est erroné d'appeler une méthode sur des objets renvoyés par d'autres méthodes, en termes de principe .

Mais il semble que parfois, il est très nécessaire d'utiliser une telle capacité.

Par exemple: J'ai plusieurs classes: classe de capture vidéo, classe d'encodage, classe de streamer, et elles utilisent toutes une autre classe de base, VideoFrame, et puisqu'elles interagissent les unes avec les autres, elles peuvent faire par exemple quelque chose comme ceci:

streamer code de la classe

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

Comme vous pouvez le constater, ce principe n’est pas appliqué ici. Ce principe peut-il être appliqué ici, ou est-ce que ce principe ne peut pas toujours être appliqué dans une conception comme celle-ci?

rançon
la source
duplication possible de la méthode chaining vs encapsulation
gnat
4
C'est probablement un doublon plus proche.
Robert Harvey
1
Connexes: Est-ce que ce code est une "épave de train" (en violation de la loi de Demeter)? (divulgation complète: c'est ma propre question)
un CVn

Réponses:

21

Le principe dont vous parlez (mieux connu sous le nom de loi de Demeter ) pour les fonctions peut être appliqué en ajoutant une autre méthode d'assistance à votre classe de streamer, telle que

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

Désormais, chaque fonction "parle uniquement à des amis", pas à des "amis d'amis".

À mon humble avis, il s’agit là d’un guide approximatif qui peut aider à créer des méthodes qui respectent plus strictement le principe de responsabilité unique. Dans un cas simple comme celui ci-dessus, il est probablement très difficile de dire si cela en vaut vraiment la peine et si le code résultant est vraiment "plus propre", ou s'il ne fait que développer votre code de manière formelle sans aucun gain notable.

Doc Brown
la source
Cette approche présente l’avantage de faciliter le test unitaire, le débogage et la raisonnement de votre code.
Gntskn
+1 Cependant, il existe une très bonne raison de ne pas modifier le code car l'interface de classe peut être surchargée de méthodes dont le travail consiste simplement à effectuer un ou plusieurs appels à plusieurs autres méthodes (sans logique supplémentaire). Dans certains types d’environnement de programmation, disons C ++ COM (notamment WIC et DirectX), l’ajout de chaque méthode à une interface COM a un coût élevé par rapport aux autres langages.
Rwong
1
Dans la conception d'interface C ++ COM, la décomposition de grandes classes en petites classes (ce qui signifie que vous avez plus de chances de parler à plusieurs objets) et la réduction du nombre de méthodes d'interface sont deux objectifs de la conception avec des avantages réels (réduction des coûts) basés sur une compréhension approfondie des mécanismes internes (tables virtuelles, possibilité de réutilisation de code, etc.). Par conséquent, les programmeurs COM C ++ doivent généralement ignorer LoD.
Rwong
10
+1: La loi de Demeter devrait vraiment être nommée La suggestion de Demeter: YMMV
Binary Worrier
La loi de Demeter est un conseil pour faire des choix architecturaux, tandis que vous suggérez un changement esthétique, il semble donc que vous respectiez cette "loi". Ce serait une idée fausse, car un changement de syntaxe ne veut pas dire que tout va bien. Autant que je sache, la loi de Demeter voulait dire fondamentalement: si vous voulez faire de la POO, arrêtez d'écrire du code prodecural avec des fonctions de getter partout.
user2180613
39

Le principe de moindre connaissance ou la loi de Demeter est un avertissement contre l'enchevêtrement de votre classe avec des détails sur les autres classes qui traversent couche après couche. Il vous dit qu'il est préférable de ne parler qu'avec vos "amis" et non avec les "amis des amis".

Imaginez que l'on vous demande de souder un bouclier sur la statue d'un chevalier en armure de plaques brillante. Vous placez soigneusement le bouclier sur le bras gauche pour qu'il paraisse naturel. Vous remarquez qu'il y a trois petites zones sur l'avant-bras, le coude et le haut du bras où le bouclier touche l’armure. Vous souder les trois endroits parce que vous voulez être sûr que la connexion est solide. Maintenant, imaginez que votre patron est en colère parce qu'il ne peut pas bouger le coude de son armure. Vous avez supposé que l'armure n'allait jamais bouger et a ainsi créé une connexion immobile entre l'avant-bras et le bras. Le bouclier ne devrait se connecter qu'à son ami, l'avant-bras. Pas aux amis de l'avant-bras. Même si vous devez ajouter un morceau de métal pour les faire toucher.

Les métaphores sont bien, mais qu'entendons-nous réellement par ami? Tout ce qu'un objet sait créer ou trouver est un ami. Alternativement, un objet peut simplement demander à être remis à d'autres objets , dont il ne connait que l'interface. Ceux-ci ne comptent pas comme des amis car aucune attente sur la façon de les obtenir n'est imposée. Si l'objet ne sait pas d'où il vient car quelque chose d'autre l'a injecté / injecté, alors ce n'est pas l'ami d'un ami, ce n'est même pas un ami. C'est quelque chose que seul l'objet sait utiliser. C'est une bonne chose.

Lorsque vous essayez d'appliquer de tels principes, il est important de comprendre qu'ils ne vous interdisent jamais d'accomplir quelque chose. Ils vous avertissent que vous négligez peut-être de faire plus de travail pour obtenir un meilleur design qui réalise la même chose.

Personne ne veut travailler sans raison, il est donc important de comprendre ce que vous en retirez. Dans ce cas, votre code reste flexible. Vous pouvez apporter des modifications et avoir moins de classes concernées par les modifications à prendre en compte. Cela sonne bien mais ne vous aide pas à décider quoi faire à moins que vous ne le preniez comme une sorte de doctrine religieuse.

Au lieu de suivre aveuglément ce principe, prenons une version simple de ce problème. Ecrire une solution qui ne suit pas le principe et qui fait. Maintenant que vous avez deux solutions, vous pouvez comparer leur réceptivité aux changements en essayant de les intégrer.

Si vous ne pouvez pas résoudre un problème en suivant ce principe, il vous manque probablement une autre compétence.

Une solution à votre problème particulier consiste à injecter framequelque chose (une classe ou une méthode) qui sait comment parler aux images afin de ne pas avoir à étaler tous les détails de la discussion dans votre classe, qui sait seulement quand et comment obtenir un cadre.

Cela découle en réalité d'un autre principe: séparer l'utilisation de la construction.

frame = encoder->WaitEncoderFrame()

En utilisant ce code, vous avez en quelque sorte pris la responsabilité d’acquérir un Frame. Vous n’avez pas encore assumé la responsabilité de parler à a Frame.

frame->DoOrGetSomething(); 

Maintenant, vous devez savoir comment parler à un Frame, mais remplacez-le par:

new FrameHandler(frame)->DoOrGetSomething();

Et maintenant, il vous suffit de savoir comment parler à votre ami, FrameHandler.

Il existe de nombreuses façons d’y parvenir et c’est peut-être pas la meilleure, mais cela montre que le respect du principe ne rend pas le problème insoluble. C'est juste exiger plus de travail de votre part.

Toute bonne règle a une exception. Les meilleurs exemples que je connaisse sont les langages internes spécifiques à un domaine . Un DSL de la chaîne de procédésemble abuser de la loi de Demeter tout le temps parce que vous appelez constamment des méthodes qui renvoient différents types et les utilisez directement. Pourquoi ça va? Parce que dans un DSL, tout ce qui est renvoyé est un ami soigneusement conçu auquel vous devez parler directement. De par sa conception, vous avez le droit de vous attendre à ce qu'une chaîne de méthodes DSL ne change pas. Vous n’avez pas ce droit si vous plongez au hasard dans la base de code en enchaînant tout ce que vous trouvez. Les meilleurs DSL sont des représentations très fines ou des interfaces avec d'autres objets que vous ne devriez probablement pas approfondir non plus. Je n’en parle que parce que j’ai compris que je comprenais beaucoup mieux la loi de Demeter une fois que j’ai compris pourquoi les DSL sont une bonne conception. Certains vont même jusqu'à dire que les DSL n'enfreignent même pas la véritable loi de Demeter.

Une autre solution consiste à laisser autre chose framevous injecter . Si vous framevenez d'un poseur ou de préférence d'un constructeur, vous ne prenez aucune responsabilité pour la construction ou l'acquisition d'un cadre. Cela signifie que votre rôle ici est beaucoup plus semblable à ce qu'il FrameHandlersallait être. Au lieu de cela, c’est maintenant vous qui discutez Frameet faites quelque chose d’autre qui permet de déterminer comment obtenir un résultat. Frame En un sens, c’est la même solution avec un simple changement de perspective.

Les principes SOLID sont les grands que j'essaie de suivre. Deux principes respectés sont les principes de responsabilité unique et d'inversion de dépendance. Il est vraiment difficile de respecter ces deux et de finir par enfreindre la loi de Demeter.

La mentalité qui consiste à violer Demeter revient à manger dans un restaurant-buffet où il faut prendre ce que vous voulez. Avec un peu de travail en amont, vous pouvez vous fournir un menu et un serveur qui vous apportera tout ce que vous voulez. Asseyez-vous, détendez-vous et donnez un bon pourboire.

confits_orange
la source
2
"Cela vous dit qu'il est préférable de ne parler qu'avec vos" amis "et non avec les" amis d'amis "" ++
RubberDuck
1
Le deuxième paragraphe est-il volé quelque part, ou l'avez-vous inventé? Cela m'a fait comprendre le concept entièrement . Cela rendait flagrant ce que sont les "amis des amis" et les inconvénients qu’il ya à emmêler trop de classes (articulations). Une citation.
Maus maus
2
@diemaus Si j'ai volé ce paragraphe de bouclier quelque part, sa source a depuis coulé de ma tête. À l'époque, mon cerveau pensait qu'il était malin. Ce dont je me souviens, c’est que je l’ai ajouté après la plupart des votes positifs, c’est donc agréable de le voir validé. Content que cela ait aidé.
candied_orange
19

La conception fonctionnelle est-elle meilleure que la conception orientée objet? Ça dépend.

MVVM est-il meilleur que MVC? Ça dépend.

Amos & Andy ou Martin et Lewis? Ça dépend.

Qu'est-ce que cela dépend? Les choix que vous faites dépendent de la manière dont chaque technique ou technologie répond aux exigences fonctionnelles et non fonctionnelles de votre logiciel, tout en satisfaisant vos objectifs de conception, de performance et de maintenabilité.

[Certains livres] disent que [quelque chose] est faux.

Lorsque vous lisez ceci dans un livre ou un blog, évaluez la demande en fonction de son mérite. c'est pourquoi demander. Il n’ya pas de bonne ou de mauvaise technique dans le développement logiciel, il n’existe que "dans quelle mesure cette technique répond-elle à mes objectifs? Est-elle efficace ou inefficace? Résout-il un problème mais en crée-t-il un nouveau? Peut-il être bien compris par l'ensemble équipe de développement, ou est-ce trop obscur? "

Dans ce cas particulier - l'acte d'appeler une méthode sur un objet renvoyé par une autre méthode - puisqu'il existe un modèle de conception qui codifie cette pratique (Factory), il est difficile d'imaginer comment on pourrait affirmer catégoriquement qu'il s'agit d'une pratique. faux.

La raison pour laquelle on l'appelle "Principe de moindre connaissance" est que "Couplage faible" est une qualité souhaitable d'un système. Les objets qui ne sont pas étroitement liés les uns aux autres fonctionnent de manière plus indépendante et sont donc plus faciles à gérer et à modifier individuellement. Toutefois, comme le montre votre exemple, il est parfois plus souhaitable de recourir à un couplage élevé afin que les objets puissent coordonner leurs efforts plus efficacement.

Robert Harvey
la source
2

La réponse de Doc Brown montre une implémentation classique de Law of Demeter dans les manuels scolaires - et l'agacement / le désordre de code désorganisé consistant à ajouter des dizaines de méthodes de cette façon est probablement la raison pour laquelle les programmeurs, y compris moi-même, ne se donnent souvent pas la peine de le faire, même s'ils le devraient.

Il existe un autre moyen de découpler la hiérarchie des objets:

Exposez les interfacetypes, plutôt que les classtypes, via vos méthodes et propriétés.

Dans le cas de Poster Original (OP), encoder->WaitEncoderFrame()renverrait un IEncoderFrameau lieu de Frameet définirait les opérations autorisées.


SOLUTION 1

Dans le cas le plus simple, Frameet les Encoderclasses sont à la fois sous votre contrôle, IEncoderFrameest un sous - ensemble de méthodes Cadre déjà publiquement expose, et la Encoderclasse ne se soucie pas vraiment ce que vous faites à cet objet. Ensuite, la mise en œuvre est triviale ( code en c # ):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

SOLUTION 2

Dans un cas intermédiaire, où la Framedéfinition n'est pas sous votre contrôle, ou s'il ne serait pas approprié d'ajouter IEncoderFramedes méthodes à Frame, une bonne solution est alors un adaptateur . C’est ce que la réponse de CandiedOrange explique, en tant que new FrameHandler( frame ). IMPORTANT: si vous faites cela, il est plus flexible si vous l'exposez en tant qu'interface et non en tant que classe . Encoderdoit savoir class FrameHandler, mais les clients doivent seulement savoir interface IFrameHandler. Ou comme je l’ai nommé, interface IEncoderFrame- pour indiquer qu’il s’agit spécifiquement de Frame vu de POV de Encoder :

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

COST: Allocation et validation d'un nouvel objet, EncoderFrameWrapper, à chaque encoder.TheFrameappel. (Vous pouvez mettre en cache ce wrapper, mais cela ajoute plus de code. Et ce n'est facile de coder de manière fiable que si le champ frame de l'encodeur ne peut pas être remplacé par un nouveau frame.)


SOLUTION 3

Dans le cas le plus difficile, le nouvel emballage doit connaître à la fois Encoderet Frame. Cet objet violerait lui-même le LoD - il manipule une relation entre Encoder et Frame qui devrait incomber à Encoder - et il serait probablement difficile de bien faire les choses. Voici ce qui peut arriver si vous vous engagez dans cette voie:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

C'est devenu moche. Il existe une implémentation moins compliquée, lorsque le wrapper doit toucher les détails de son créateur / propriétaire (Encoder):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

Certes, si je savais que je finirais ici, je ne ferais peut-être pas cela. Pourrait juste écrire les méthodes LoD, et en finir avec ça. Pas besoin de définir une interface. D'un autre côté, j'aime bien que l'interface encapsule les méthodes associées. J'aime ce que l'on ressent lorsque l'on fait les "opérations de type cadre" pour créer ce qui ressemble à un cadre.


COMMENTAIRES FINAUX

Considérez ceci: si le réalisateur de Encoderestimait que l'exposition Frame frameétait appropriée à leur architecture globale, ou était "tellement plus facile que de mettre en œuvre un LoD", il aurait été beaucoup plus sûr de se fier au premier extrait que je montre - exposer un sous-ensemble limité de Frame, en tant qu'interface. D'après mon expérience, c'est souvent une solution tout à fait réalisable. Ajoutez simplement des méthodes à l'interface selon vos besoins. (Je parle d'un scénario dans lequel nous "savons" que Frame dispose déjà des méthodes nécessaires, sinon elles seraient faciles et sans controverse. Le travail de "mise en œuvre" pour chaque méthode consiste à ajouter une ligne à la définition de l'interface.) Et sachez que même dans le pire des scénarios futurs, il est possible de faire fonctionner cette API - ici,IEncoderFrameFrameEncoder.

Notez également que si vous n'êtes pas autorisé à ajouter IEncoderFrameà Frame, ou les méthodes nécessaires ne correspondez bien à la générale Frameclasse, et la solution n ° 2 ne vous convient pas, peut - être à cause de la création et destruction d'objets supplémentaires, La solution n ° 3 peut être considérée simplement comme un moyen d’organiser les méthodes de Encoderréalisation du LoD. Ne faites pas que passer par des dizaines de méthodes. Enveloppez-les dans une interface et utilisez "implémentation d'interface explicite" (si vous êtes en c #), de sorte qu'ils ne soient accessibles que lorsque l'objet est visualisé via cette interface.

Un autre point que je tiens à souligner est que la décision d' exposer la fonctionnalité en tant qu'interface a traité les 3 situations décrites ci-dessus. Dans le premier, IEncoderFrameest simplement un sous-ensemble de Framela fonctionnalité de. Dans le second, IEncoderFrameest un adaptateur. Dans le troisième, IEncoderFrameest une partition en Encoderfonctionnalité s. Peu importe si vos besoins changent entre ces trois situations: l'API reste la même.

OutilleurSteve
la source
Coupler votre classe à une interface renvoyée par l'un de ses collaborateurs, plutôt qu'à une classe concrète, tout en étant une amélioration, reste une source de couplage. Si l'interface doit changer, cela signifie que votre classe devra changer. Le point important à retenir est que le couplage n'est pas intrinsèquement mauvais tant que vous veillez à éviter tout couplage inutile avec des objets instables ou des structures internes . C'est pourquoi la loi de Demeter doit être prise avec une grosse pincée de sel; il vous conseille d'éviter toujours quelque chose qui pourrait ne pas être un problème, selon les circonstances.
Periata Breatta
@ PeriataBreatta - Je ne peux certainement pas et je ne suis pas en désaccord avec cela. Mais je voudrais souligner une chose: Une interface , par définition, représente ce qui doit être connu à la frontière entre les deux classes. S'il est «nécessaire de changer», c'est fondamental - aucune approche alternative n'aurait pu magiquement éviter le codage nécessaire. Comparez cela avec l'une des trois situations que je décris, mais sans utiliser d'interface, mais retournez une classe concrète. Dans 1, Frame, dans 2, EncoderFrameWrapper, dans 3, Encoder. Vous vous enfermez dans cette approche. L'interface peut s'adapter à tous.
ToolmakerSteve
@PeriataBreatta ... qui démontre l'avantage de le définir explicitement comme une interface. J'espère éventuellement améliorer un IDE pour rendre cela tellement pratique que les interfaces seront beaucoup plus utilisées. La plupart des accès à plusieurs niveaux se font via une interface, ce qui facilite grandement la gestion des modifications. (Si cela est "excessif" dans certains cas, l’analyse de code, combinée à des annotations indiquant où nous sommes disposés à prendre le risque de ne pas avoir d’interface, en échange du petit gain de performances, pourrait "le compiler" - le remplacer par une de ces 3 classes concrètes parmi les 3 "solutions".)
ToolmakerSteve
@PeriataBreatta - et quand je dis "l'interface peut s'adapter à tous", je ne dis pas "ahhhh, c'est merveilleux que cette fonctionnalité d'un langage puisse couvrir ces différents cas". Je dis que définir des interfaces minimise les changements que vous pourriez avoir à faire. Dans le meilleur des cas, la conception peut changer du cas le plus simple (solution 1) au cas le plus difficile (solution 3), sans que l'interface ne change du tout - seuls les besoins internes du fabricant sont devenus plus complexes. Et même lorsque des changements sont nécessaires, ils ont tendance à être moins omniprésents, à mon humble avis.
ToolmakerSteve