it-swarm.com.de

Das Prinzip des geringsten Wissens

Ich verstehe das Motiv hinter dem Prinzip des geringsten Wissens , aber ich finde einige Nachteile, wenn ich versuche, es in meinem Design anzuwenden.

Eines der Beispiele für dieses Prinzip (eigentlich, wie man es nicht benutzt), das ich in dem Buch Head First Design Patterns Geben Sie an, dass es nach diesem Prinzip falsch ist, eine Methode für Objekte aufzurufen, die vom Aufruf anderer Methoden zurückgegeben wurden.

Aber es scheint, dass es manchmal sehr notwendig ist, solche Fähigkeiten zu nutzen.

Beispiel: Ich habe mehrere Klassen: Video-Capture-Klasse, Encoder-Klasse, Streamer-Klasse, und alle verwenden eine andere grundlegende Klasse, VideoFrame. Da sie miteinander interagieren, können sie beispielsweise Folgendes tun:

streamer Klassencode

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

Wie Sie sehen, wird dieses Prinzip hier nicht angewendet. Kann dieses Prinzip hier angewendet werden, oder kann dieses Prinzip in einem solchen Design nicht immer angewendet werden?

33
ransh

Das Prinzip, von dem Sie sprechen (besser bekannt als Gesetz von Demeter ) für Funktionen , kann angewendet werden, indem Sie eine weitere Hilfsmethode hinzufügen Ihre Streamer-Klasse mag

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

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

Jetzt "spricht jede Funktion nur noch mit Freunden", nicht mit "Freunden von Freunden".

IMHO ist es eine grobe Richtlinie, die helfen kann, Methoden zu entwickeln, die strenger dem Prinzip der Einzelverantwortung folgen. In einem einfachen Fall wie dem oben genannten ist es wahrscheinlich sehr eigensinnig, ob sich der Aufwand wirklich lohnt und ob der resultierende Code wirklich "sauberer" ist oder ob er Ihren Code nur formal ohne nennenswerten Gewinn erweitert.

21
Doc Brown

Das Prinzip des geringsten Wissens oder das Gesetz des Demeters ist eine Warnung davor, Ihre Klasse mit Details anderer Klassen zu verwickeln, die Schicht für Schicht durchlaufen. Es sagt Ihnen, dass es besser ist, nur mit Ihren "Freunden" und nicht mit "Freunden von Freunden" zu sprechen.

Stellen Sie sich vor, Sie wurden gebeten, einen Schild an eine Statue eines Ritters in glänzender Plattenrüstung zu schweißen. Sie positionieren den Schild vorsichtig auf dem linken Arm, damit er natürlich aussieht. Sie bemerken, dass es drei kleine Stellen am Unterarm, am Ellbogen und am Oberarm gibt, an denen der Schild die Rüstung berührt. Sie schweißen alle drei Stellen, weil Sie sicher sein möchten, dass die Verbindung stark ist. Stellen Sie sich jetzt vor, Ihr Chef ist verrückt, weil er den Ellbogen seiner Rüstung nicht bewegen kann. Sie gingen davon aus, dass sich die Rüstung niemals bewegen würde, und stellten so eine unbewegliche Verbindung zwischen Unterarm und Oberarm her. Der Schild sollte nur mit seinem Freund, dem Unterarm, verbunden sein. Nicht an Unterarmfreunde. Selbst wenn Sie ein Stück Metall hinzufügen müssen, damit sie sich berühren.

Metaphern sind nett, aber was meinen wir eigentlich mit Freund? Alles, was ein Objekt erstellen oder finden kann, ist ein Freund. Alternativ kann ein Objekt einfach m die Übergabe anderer Objekte bitten , von denen es nur die Schnittstelle kennt. Diese zählen nicht als Freunde, da keine Erwartung besteht, wie man sie bekommt. Wenn das Objekt nicht weiß, woher es kommt, weil etwas anderes es passiert/injiziert hat, dann ist es kein Freund eines Freundes, es ist nicht einmal ein Freund. Es ist etwas, das das Objekt nur zu bedienen weiß. Das ist gut.

Wenn Sie versuchen, solche Prinzipien anzuwenden, ist es wichtig zu verstehen, dass sie Ihnen niemals verbieten, etwas zu erreichen. Sie sind eine Warnung, dass Sie möglicherweise vernachlässigen, mehr Arbeit zu leisten, um ein besseres Design zu erzielen, das dasselbe erreicht.

Niemand möchte ohne Grund arbeiten, daher ist es wichtig zu verstehen, was Sie davon haben, wenn Sie dies befolgen. In diesem Fall bleibt Ihr Code flexibel. Sie können Änderungen vornehmen und weniger andere Klassen von den Änderungen betroffen machen, über die Sie sich Sorgen machen müssen. Das hört sich gut an, hilft Ihnen aber nicht bei der Entscheidung, was zu tun ist, es sei denn, Sie nehmen es als eine Art religiöse Doktrin.

Anstatt blind diesem Prinzip zu folgen, nehmen Sie eine einfache Version dieses Problems. Schreiben Sie eine Lösung, die nicht dem Prinzip folgt und die es tut. Nachdem Sie nun zwei Lösungen haben, können Sie vergleichen, wie empfänglich jede für Änderungen ist, indem Sie versuchen, sie in beiden zu machen.

Wenn Sie ein Problem nicht lösen können, während Sie diesem Prinzip folgen, fehlt Ihnen wahrscheinlich eine andere Fähigkeit.

Eine Lösung für Ihr spezielles Problem besteht darin, das frame in etwas (eine Klasse oder Methode) einzufügen, das weiß, wie man mit Frames spricht, damit Sie nicht alle diese Frame-Chat-Details in Ihrer Klasse verteilen müssen, die jetzt weiß nur, wie und wann man einen Rahmen bekommt.

Das folgt eigentlich einem anderen Prinzip: Trennung von Konstruktion.

frame = encoder->WaitEncoderFrame()

Mit diesem Code haben Sie die Verantwortung dafür übernommen, ein Frame zu erwerben. Sie haben noch keine Verantwortung für das Gespräch mit einem Frame übernommen.

frame->DoOrGetSomething(); 

Jetzt müssen Sie wissen, wie man mit einem Frame spricht, aber ersetzen Sie das durch:

new FrameHandler(frame)->DoOrGetSomething();

Und jetzt müssen Sie nur noch wissen, wie Sie mit Ihrem Freund FrameHandler sprechen können.

Es gibt viele Möglichkeiten, dies zu erreichen, und dies ist vielleicht nicht die beste, aber es zeigt, wie die Befolgung des Prinzips das Problem nicht unlösbar macht. Es fordert nur mehr Arbeit von Ihnen.

Jede gute Regel hat eine Ausnahme. Die besten Beispiele, die ich kenne, sind interne domänenspezifische Sprachen . A DSL s Methodenkette scheint das Gesetz von Demeter ständig zu missbrauchen, da Sie ständig Methoden aufrufen, die verschiedene Typen und zurückgeben mit ihnen direkt. Warum ist das in Ordnung? Denn in einem DSL ist alles, was zurückgegeben wird, ein sorgfältig gestalteter Freund, mit dem Sie direkt sprechen sollen. Sie haben das Recht, zu erwarten, dass sich die Methodenkette eines DSL nicht ändert. Sie haben das Recht nicht, wenn Sie nur zufällig in die Codebasis eintauchen und alles verketten, was Sie finden. Die besten DSLs sind sehr dünne Darstellungen oder Schnittstellen zu anderen Objekten, mit denen Sie sich wahrscheinlich auch nicht befassen sollten. Ich erwähne dies nur, weil ich festgestellt habe, dass ich das Gesetz des Demeters viel besser verstanden habe, als ich erfuhr, warum DSLs ein gutes Design sind. Einige gehen sogar so weit zu sagen, dass DSLs nicht einmal gegen das wahre Gesetz des Demeters verstoßen. Sie scheinen nur, wenn man das Gesetz oberflächlich betrachtet.

Eine andere Lösung besteht darin, etwas anderes frame in Sie injizieren zu lassen. Wenn frame von einem Setter oder vorzugsweise einem Konstruktor stammt, übernehmen Sie keine Verantwortung für die Erstellung oder den Erwerb eines Frames. Dies bedeutet, dass Ihre Rolle hier viel ähnlicher ist als FrameHandlers. Stattdessen sind Sie jetzt derjenige, der mit Frame plaudert und etwas anderes herausfindet, wie man ein Frame erhält. In gewisser Weise ist dies die gleiche Lösung mit einem einfachen Perspektivwechsel.

Die SOLIDEN Prinzipien sind die großen, denen ich zu folgen versuche. Zwei, die hier beachtet werden, sind die Prinzipien der Einzelverantwortung und der Abhängigkeitsinversion. Es ist wirklich schwer, diese beiden zu respektieren und trotzdem gegen das Gesetz von Demeter zu verstoßen.

Die Mentalität, die dazu führt, dass Demeter verletzt wird, ist wie das Essen in einem Buffetrestaurant, in dem Sie einfach alles holen, was Sie wollen. Mit ein wenig Arbeit im Voraus können Sie sich ein Menü und einen Server zur Verfügung stellen, die Ihnen alles bringen, was Sie möchten. Lehnen Sie sich zurück, entspannen Sie sich und geben Sie ein gutes Trinkgeld.

40
candied_orange

Ist funktionales Design besser als objektorientiertes Design? Es hängt davon ab, ob.

Ist MVVM besser als MVC? Es hängt davon ab, ob.

Amos & Andy oder Martin und Lewis? Es hängt davon ab, ob.

Wovon hängt es ab? Die Entscheidungen, die Sie treffen, hängen davon ab, wie gut jede Technik oder Technologie die funktionalen und nicht funktionalen Anforderungen Ihrer Software erfüllt und gleichzeitig Ihre Design-, Leistungs- und Wartbarkeitsziele angemessen erfüllt.

[Ein Buch] sagt, dass [etwas] falsch ist.

Wenn Sie dies in einem Buch oder Blog lesen, bewerten Sie die Behauptung anhand ihres Verdienstes. das heißt, fragen Sie warum. Es gibt keine richtige oder falsche Technik in der Softwareentwicklung, es gibt nur "Wie gut erfüllt diese Technik meine Ziele? Ist sie effektiv oder ineffektiv? Löst sie ein Problem, schafft aber ein neues? Kann sie vom Ganzen gut verstanden werden?" Entwicklungsteam, oder ist es zu dunkel? "

In diesem speziellen Fall - dem Aufrufen einer Methode für ein Objekt, das von einer anderen Methode zurückgegeben wird - ist es schwer vorstellbar, wie man die Behauptung aufstellen kann, dass es kategorisch ist, da es ein tatsächliches Entwurfsmuster gibt, das diese Praxis kodifiziert (Factory) falsch.

Der Grund, warum es als "Prinzip des geringsten Wissens" bezeichnet wird, ist, dass "niedrige Kopplung" eine wünschenswerte Qualität eines Systems ist. Objekte, die nicht eng miteinander verbunden sind, arbeiten unabhängiger und sind daher einfacher zu pflegen und individuell zu ändern. Wie Ihr Beispiel zeigt, gibt es Zeiten, in denen eine hohe Kopplung wünschenswerter ist, damit Objekte ihre Bemühungen effektiver koordinieren können.

19
Robert Harvey

Doc Browns Antwort zeigt eine klassische Lehrbuchimplementierung von Law of Demeter - und das Ärgernis/unorganisierte Aufblähen von Dutzenden von Methoden auf diese Weise ist wahrscheinlich der Grund, warum Programmierer, auch ich, sich oft nicht darum kümmern also, auch wenn sie sollten.

Es gibt eine alternative Möglichkeit, die Hierarchie von Objekten zu entkoppeln:

Stellen Sie interface Typen anstelle von class Typen über Ihre Methoden und Eigenschaften bereit.

Im Fall von Original Poster (OPs) würde encoder->WaitEncoderFrame() ein IEncoderFrame anstelle eines Frame zurückgeben und definieren, welche Operationen zulässig sind.


LÖSUNG 1

Im einfachsten Fall stehen die Klassen Frame und Encoder unter Ihrer Kontrolle, IEncoderFrame ist eine Teilmenge von Methoden, die Frame bereits öffentlich verfügbar macht, und die Klasse Encoder nicht Es ist mir eigentlich egal, was Sie mit diesem Objekt machen. Dann ist die Implementierung trivialer ( Code in 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; } }
    ...
}

LÖSUNG 2

In einem Zwischenfall, in dem die Definition von Frame nicht unter Ihrer Kontrolle steht oder es nicht angebracht wäre, die Methoden von IEncoderFrame zu Frame hinzuzufügen, ist eine gute Lösung ein Adapter . Das ist es, was CandiedOranges Antwort als new FrameHandler( frame ) bespricht. WICHTIG: Wenn Sie dies tun, ist es flexibler, wenn Sie es als Schnittstelle und nicht als Klasse verfügbar machen. Encoder muss über class FrameHandler Bescheid wissen, aber Clients müssen nur interface IFrameHandler kennen. Oder wie ich es nannte, interface IEncoderFrame - um anzuzeigen, dass es sich speziell um Frame handelt, wie aus POV von Encoder ersichtlich:

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 ); } }

    ...
}

KOSTEN: Zuweisung und GC eines neuen Objekts, des EncoderFrameWrapper, bei jedem Aufruf von encoder.TheFrame. (Sie könnten diesen Wrapper zwischenspeichern, aber das fügt mehr Code hinzu. Und es ist nur dann einfach, zuverlässig zu codieren, wenn das Rahmenfeld des Encoders nicht durch einen neuen Rahmen ersetzt werden kann.)


LÖSUNG 3

Im schwierigeren Fall müsste der neue Wrapper sowohl über Encoder als auch über Frame Bescheid wissen. Dieses Objekt würde selbst gegen LoD verstoßen - es manipuliert eine Beziehung zwischen Encoder und Frame, für die Encoder verantwortlich sein sollte - und ist wahrscheinlich ein Problem, um es richtig zu machen. Folgendes kann passieren, wenn Sie diesen Weg einschlagen:

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;

    ...
}

Das wurde hässlich. Es gibt eine weniger komplizierte Implementierung, wenn der Wrapper Details seines Erstellers/Besitzers (Encoder) berühren muss:

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

Zugegeben, wenn ich wüsste, dass ich hier landen würde, würde ich das vielleicht nicht tun. Könnte einfach die LoD-Methoden schreiben und damit fertig sein. Keine Notwendigkeit, eine Schnittstelle zu definieren. Andererseits gefällt es mir, dass die Schnittstelle verwandte Methoden zusammenfasst. Mir gefällt, wie es sich anfühlt, die "rahmenartigen Operationen" an einem Rahmen durchzuführen.


SCHLUSSKOMMENTARE

Beachten Sie Folgendes: Wenn der Implementierer von Encoder der Ansicht war, dass das Offenlegen von Frame frame Für die Gesamtarchitektur angemessen oder "so viel einfacher als das Implementieren von LoD" ist, dann Es wäre viel sicherer gewesen, wenn sie stattdessen das erste von mir gezeigte Snippet gemacht hätten - eine begrenzte Teilmenge von Frame als Schnittstelle verfügbar zu machen. Nach meiner Erfahrung ist dies oft eine vollständig praktikable Lösung. Fügen Sie der Schnittstelle einfach nach Bedarf Methoden hinzu. (Ich spreche von einem Szenario, in dem wir "wissen", dass Frame bereits über die erforderlichen Methoden verfügt, oder dass das Hinzufügen einfach und unumstritten wäre. Die "Implementierungs" -Arbeit für jede Methode besteht darin, der Schnittstellendefinition eine Zeile hinzuzufügen.) Und wissen, dass es auch im schlimmsten zukünftigen Szenario möglich ist, diese API funktionsfähig zu halten - hier, indem Sie IEncoderFrame aus Frame entfernen und auf Encoder setzen.

Beachten Sie auch, dass wenn Sie keine Berechtigung zum Hinzufügen von IEncoderFrame zu Frame haben oder die erforderlichen Methoden nicht gut zur allgemeinen Klasse Frame und zur Lösung 2 passen passt nicht zu Ihnen, vielleicht aufgrund der zusätzlichen Objekterstellung und -zerstörung, kann Lösung 3 einfach als eine Möglichkeit angesehen werden, die Methoden von Encoder zu organisieren, um LoD zu erreichen. Gehen Sie nicht nur Dutzende von Methoden durch. Wickeln Sie sie in eine Schnittstelle ein und verwenden Sie "explizite Schnittstellenimplementierung" (wenn Sie sich in c # befinden), sodass auf sie nur zugegriffen werden kann, wenn das Objekt über diese Schnittstelle angezeigt wird.

Ein weiterer Punkt, den ich hervorheben möchte , ist die Entscheidung, Funktionalität als Schnittstelle verfügbar zu machen alle 3 oben beschriebenen Situationen behandelt. Im ersten Fall ist IEncoderFrame einfach eine Teilmenge der Funktionalität von Frame. Im zweiten Fall ist IEncoderFrame ein Adapter. Im dritten Fall ist IEncoderFrame eine Partition in die Funktionalität von Encoder. Es spielt keine Rolle, ob sich Ihre Anforderungen zwischen diesen drei Situationen ändern: Die API bleibt gleich.

2
ToolmakerSteve