2010-03-31 8 views
5

Ich habe auf diesem "Gesetz des Demeter" Ding gelesen, und es (und reine "Wrapper" Klassen im Allgemeinen) scheinen im Allgemeinen Antimuster zu sein. Betrachten Sie eine Implementierungsklasse:Wrapper/Gesetz des Demeter scheint ein Anti-Muster zu sein

class FluidSimulator { 
    void reset() { /* ... */ } 
} 

Betrachten wir nun zwei verschiedene Implementierungen einer anderen Klasse:

class ScreenSpaceEffects1 { 
    private FluidSimulator _fluidDynamics; 
    public FluidSimulator getFluidSimulator() { return _fluidDynamics; } 
} 

class ScreenSpaceEffects2 { 
    private FluidSimulator _fluidDynamics; 
    public void resetFluidSimulation() { _fluidDynamics.reset(); } 
} 

Und die Art und Weise zu nennen, sagte Methoden:

callingMethod() { 
    effects1.getFluidSimulator().reset(); // Version 1 
    effects2.resetFluidSimulation();  // Version 2 
} 

Auf den ersten Blick, Version 2 scheint ein bisschen einfacher, und folgt der "Demeter-Regel", verstecken Foo's Implementierung, etc, etc. Aber dies bindet alle Änderungen in FluidSimulator an ScreenSpaceEffects. Zum Beispiel, wenn ein Parameter nicht zurückgesetzt wird hinzugefügt, dann haben wir:

class FluidSimulator { 
    void reset(bool recreateRenderTargets) { /* ... */ } 
} 

class ScreenSpaceEffects1 { 
    private FluidSimulator _fluidDynamics; 
    public FluidSimulator getFluidSimulator() { return _fluidDynamics; } 
} 

class ScreenSpaceEffects2 { 
    private FluidSimulator _fluidDynamics; 
    public void resetFluidSimulation(bool recreateRenderTargets) { _fluidDynamics.reset(recreateRenderTargets); } 
} 

callingMethod() { 
    effects1.getFluidSimulator().reset(false); // Version 1 
    effects2.resetFluidSimulation(false);  // Version 2 
} 

In beiden Versionen muss callingMethod geändert werden, aber in Version 2, ScreenSpaceEffects auch geändert werden muss. Kann jemand den Vorteil eines Wrappers/einer Fassade erklären (mit Ausnahme von Adaptern oder dem Umhüllen einer externen API oder dem Freigeben einer internen API)?

EDIT: Eines von vielen reellen Beispielen, für die ich in dieses eher als ein triviales Beispiel lief.

+0

Meinst du "Version 2 scheint ein bisschen einfacher"? –

+0

Ja, tut mir leid, wird sich ändern –

+0

Version 1 folgt nicht der Demeter-Regel. Mistyp? – Corwin

Antwort

13

Der Hauptunterschied ist, dass in Version 1, als Anbieter der Bar Abstraktion, haben Sie keine Kontrolle darüber, wie Foo ausgesetzt ist. Jede Änderung in Foo wird Ihren Kunden ausgesetzt, und sie werden damit zu rechnen haben.

Mit Version 2, als Anbieter von Abstraktion Bar, können Sie entscheiden, ob und wie Sie die Entwicklungen anzeigen möchten. Es hängt nur von der Bar Abstraktion ab, und nicht Foo 's. In Ihrem Beispiel könnte Ihre Bar Abstraktion möglicherweise bereits wissen, welche Ganzzahl als Argument übergeben werden soll. Auf diese Weise können Sie Ihren Benutzern die neue Version Foo ohne Änderungen transparent zur Verfügung stellen.

Angenommen, Foo entwickelt sich und erfordert, dass der Benutzer foo.init() vor jedem Anruf an doSomething aufrufen. Mit Version 1 müssen alle Benutzer von Bar sehen, dass Foo geändert wurde und ihren Code anpassen. Bei der Version 2 muss nur Bar geändert werden, bei Bedarf doSomething . Dies führt zu weniger Bugs (nur der Autor der Abstraktion Bar muss Abstraktion Foo und weniger Kopplung zwischen Klassen kennen und verstehen.

+0

Ich bin nicht davon überzeugt, dass dies die zusätzliche Komplexität wert ist (besonders wenn die Klassen bereits gekoppelt sind), aber danke für die Antwort trotzdem :-). –

+0

Fast immer, wenn sich etwas ändert, muss sich etwas anderes ändern. Das Beste, was man tun kann, ist, alles so zu arrangieren, dass die Teile, die man konstant halten will, auch dann konstant bleiben können, wenn sich die Dinge, die sich ändern können, auch tun. Jedes der angegebenen Designs wird in der Lage sein, einige Arten von möglichen zukünftigen API-Änderungen leichter als die anderen zu berücksichtigen; Welches Design besser ist, hängt in gewissem Maße davon ab, wie die Wahrscheinlichkeit verschiedener möglicher zukünftiger Veränderungen beurteilt wird. – supercat

2

Dies ist offensichtlich ein künstliches Beispiel. In vielen echten Fällen, CallingMethod (im wirklichen Leben kann es mehrere (CallingMethods) kann glücklicherweise nicht bemerken, dass sich Foo.doSomething geändert hat, weil Bar es isoliert. Wenn ich zum Beispiel eine stabile Druck-API verwende, muss ich nicht befürchten, dass die Firmware meines Druckers Unterstützung für Hochglanzdruck bietet "Ich glaube, Sie würden dies unter" Adapter "gruppieren, was ich glaube, dass es viel häufiger ist, als Sie meinen.

Sie haben recht, dass manchmal CallingMethod geändert werden muss ed auch. Aber wenn das Gesetz von Demeter richtig angewendet wird, wird dies nur selten vorkommen, normalerweise um neue Funktionen zu nutzen (im Gegensatz zu einer neuen Schnittstelle).

EDIT: Es scheint durchaus möglich, dass CallingMethod ist egal, ob Rendern Ziele neu erstellt werden (ich gehe davon aus, dass dies eine Frage der Leistung v. Genauigkeit ist). Immerhin: "Wir sollten kleine Wirkungsgrade vergessen, sagen wir etwa 97% der Zeit" (Knuth). ScreenSpaceEffects2 könnte also eine resetFluidSimulation(bool) Methode hinzufügen, aber resetFluidSimulation() funktionieren (ohne Änderung an CallingMethod) durch Aufruf von _fluidDynamics.reset(true) hinter den Kulissen.

+0

Eigentlich stelle ich diese Frage, weil sie zu oft in meinem Hobbyprojekt auftaucht. –

+1

Was ist los? Vielleicht ein echtes Beispiel, anstatt aller 'Foo' dies und' Bar' das? –

+1

Ein echtes Beispiel hinzugefügt. –

2

Die Frage ist, muss callingMethod() wissen, ob die Rendering-Tabellen neu erstellt werden?

Angenommen, eine bestimmte Ausführung von callingMethod() muss die Renderingtabellen neu erstellen oder nicht. In diesem Fall erweitern Sie den Wrapper um eine neue Methode. Dann müssen Sie die neue Methode nur von den entsprechenden Instanzen von callingMethod() aufrufen.

class ScreenSpaceEffects2 { 
    private FluidSimulator _fluidDynamics; 
    public void resetFluidSimulation() { _fluidDynamics.reset(false); } 
    public void resetFluidSimulationWithRecreate() { _fluidDynamics.reset(true); } 
} 

Alternativ kann die Entscheidung woanders neu erstellen kann ganz gehören ...

class ScreenSpaceEffects2 { 
    private FluidSimulator _fluidDynamics; 
    public void resetFluidSimulation() { 
      _fluidDynamics.reset(someRuleEngine.getRecreateRenderTables()); } 
} 

... in whch Fall nichts in callingMethod() braucht überhaupt zu ändern.