Methodenverkettung vs. Verkapselung

17

Es gibt das klassische OOP-Problem der Methodenverkettung im Vergleich zu "Single-Access-Point" -Methoden:

main.getA().getB().getC().transmogrify(x, y)

vs

main.getA().transmogrifyMyC(x, y)

Die erste scheint den Vorteil zu haben, dass jede Klasse nur für eine kleinere Menge von Operationen verantwortlich ist und alles viel modularer macht - das Hinzufügen einer Methode zu C erfordert keinen Aufwand in A, B oder C, um sie verfügbar zu machen.

Der Nachteil ist natürlich die schwächere Kapselung , die der zweite Code löst. Jetzt hat A die Kontrolle über jede Methode, die es durchläuft, und kann es auf Wunsch an seine Felder delegieren.

Mir ist klar, dass es keine einheitliche Lösung gibt und das hängt natürlich vom Kontext ab, aber ich würde gerne etwas über andere wichtige Unterschiede zwischen den beiden Stilen erfahren und unter welchen Umständen sollte ich einen von beiden bevorzugen - denn gerade jetzt, wenn ich es versuche Zum Entwerfen von Code habe ich das Gefühl, dass ich die Argumente einfach nicht für die eine oder andere Entscheidung benutze.

Eiche
quelle

Antworten:

25

Ich denke, das Gesetz von Demeter bietet hier eine wichtige Richtlinie (mit seinen Vor- und Nachteilen, die wie üblich auf Einzelfallbasis gemessen werden sollten).

Die Befolgung des Gesetzes von Demeter hat den Vorteil, dass die resultierende Software in der Regel besser gewartet und angepasst werden kann. Da Objekte weniger von der internen Struktur anderer Objekte abhängig sind, können Objektcontainer geändert werden, ohne dass ihre Aufrufer überarbeitet werden müssen.

Ein Nachteil des Gesetzes von Demeter ist, dass es manchmal erforderlich ist, eine große Anzahl kleiner "Wrapper" -Methoden zu schreiben, um Methodenaufrufe an die Komponenten weiterzuleiten. Darüber hinaus kann die Schnittstelle einer Klasse umfangreich werden, da sie Methoden für enthaltene Klassen hostet, was zu einer Klasse ohne zusammenhängende Schnittstelle führt. Dies könnte aber auch ein Zeichen für ein schlechtes OO-Design sein.

Péter Török
quelle
Ich habe dieses Gesetz vergessen, danke, dass Sie mich daran erinnert haben. Aber was ich über hier zu fragen ist vor allem , was die Vor- und Nachteile sind, oder genauer gesagt , wie soll ich entscheiden , einen Stil über die andere zu verwenden.
Oak
@Oak, ich habe Zitate hinzugefügt, die die Vor- und Nachteile beschreiben.
Péter Török
10

Im Allgemeinen versuche ich, die Verkettung der Methoden so gering wie möglich zu halten (basierend auf dem Gesetz von Demeter ).

Die einzige Ausnahme, die ich mache, ist für fließende Schnittstellen / interne DSL-Programmierung.

Martin Fowler unterscheidet in domänenspezifischen Sprachen ähnlich, jedoch aus Gründen der Trennung von Befehlsabfragen, in denen Folgendes angegeben ist:

dass jede Methode entweder ein Befehl sein sollte, der eine Aktion ausführt, oder eine Abfrage, die Daten an den Aufrufer zurückgibt, aber nicht beide.

Fowler sagt in seinem Buch auf Seite 70:

Die Trennung von Befehlen und Abfragen ist ein äußerst wertvolles Prinzip bei der Programmierung, und ich empfehle den Teams nachdrücklich, es zu verwenden. Eine der Konsequenzen der Verwendung von Method Chaining in internen DSLs besteht darin, dass dieses Prinzip normalerweise verletzt wird. Jede Methode ändert den Status, gibt jedoch ein Objekt zurück, um die Kette fortzusetzen. Ich habe viele Dezibel verwendet, die Leute herabsetzen, die der Trennung von Befehlen und Abfragen nicht folgen, und werde dies auch wieder tun. Fließende Benutzeroberflächen unterliegen jedoch anderen Regeln. Daher bin ich froh, dies hier zuzulassen.

KeesDijk
quelle
3

Ich denke die Frage ist, ob Sie eine passende Abstraktion verwenden.

Im ersten Fall haben wir

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Wobei main vom Typ ist IHasGetA. Die Frage ist: Ist diese Abstraktion geeignet? Die Antwort ist nicht trivial. Und in diesem Fall sieht es ein bisschen anders aus, aber es ist trotzdem ein theoretisches Beispiel. Aber um ein anderes Beispiel zu konstruieren:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Ist oft besser als

main.superTransmogrify(v, w, x, y, z);

Da im letzteren Beispiel beide thisund ist mainabhängig von den Typen v, w, x, yund z. Außerdem sieht der Code nicht viel besser aus, wenn jede Methodendeklaration ein halbes Dutzend Argumente enthält.

Ein Service Locator benötigt tatsächlich den ersten Ansatz. Sie möchten nicht auf die Instanz zugreifen, die über den Service Locator erstellt wird.

Das "Durchgreifen" eines Objekts kann also zu einer großen Abhängigkeit führen, zumal es auf Eigenschaften tatsächlicher Klassen basiert.
Eine Abstraktion zu erschaffen, bei der es darum geht, ein Objekt bereitzustellen, ist jedoch eine ganz andere Sache.

Zum Beispiel könnten Sie haben:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Wo mainist eine Instanz von Main. Wenn die Klassenkenntnis maindie Abhängigkeit auf IHasGetAanstatt auf reduziert Main, wird die Kopplung tatsächlich als recht gering empfunden. Der aufrufende Code weiß nicht einmal, dass er tatsächlich die letzte Methode für das ursprüngliche Objekt aufruft, was den Grad der Entkopplung tatsächlich veranschaulicht.
Sie gehen eher einen Weg von präzisen und orthogonalen Abstraktionen, als tief in die Interna einer Implementierung hinein.

back2dos
quelle
Sehr interessanter Punkt über die große Zunahme der Anzahl der Parameter.
Oak
2

Das Gesetz von Demeter schlägt, wie @ Péter Török betont, die "kompakte" Form vor.

Je mehr Methoden Sie explizit in Ihrem Code erwähnen, desto mehr Klassen sind für Ihre Klasse von Bedeutung, wodurch die Wartungsprobleme zunehmen. In Ihrem Beispiel hängt die kompakte Form von zwei Klassen ab, während die längere Form von vier Klassen abhängt. Die längere Form widerspricht nicht nur dem Gesetz von Demeter; Außerdem können Sie Ihren Code jedes Mal ändern, wenn Sie eine der vier referenzierten Methoden ändern (im Gegensatz zu zwei in der kompakten Form).

CesarGon
quelle
Auf der anderen Seite bedeutet das blinde Befolgen dieses Gesetzes, dass die Anzahl der Methoden Aexplodieren wird, Awenn viele Methoden sowieso abtreten möchten. Trotzdem stimme ich den Abhängigkeiten zu - das reduziert die Anzahl der vom Client-Code benötigten Abhängigkeiten drastisch.
Oak
1
@Oak: Handeln etwas blind ist nie gut. Man muss die Vor- und Nachteile betrachten und Entscheidungen auf der Grundlage von Beweisen treffen. Dies schließt auch das Gesetz von Demeter ein.
CesarGon
2

Ich habe mit diesem Problem selbst gerungen. Der Nachteil, wenn Sie tief in verschiedene Objekte "hineingreifen", ist, dass Sie beim Refactoring eine Menge Code ändern müssen, da es so viele Abhängigkeiten gibt. Außerdem wird Ihr Code etwas aufgebläht und schwerer zu lesen.

Andererseits bedeutet Klassen, die Methoden einfach "weitergeben", auch den Aufwand, mehrere Methoden an mehr als einer Stelle deklarieren zu müssen.

Eine Lösung, die dies abschwächt und in einigen Fällen angemessen ist, besteht darin, eine Factory-Klasse zu haben, die ein Fassadenobjekt erstellt, indem Daten / Objekte aus den entsprechenden Klassen kopiert werden. Auf diese Weise können Sie gegen Ihr Fassadenobjekt codieren und bei der Umgestaltung einfach die Logik der Fabrik ändern.

Homde
quelle
1

Ich stelle oft fest, dass die Logik eines Programms mit verketteten Methoden einfacher zu verstehen ist. Für mich customer.getLastInvoice().itemCount()passt das besser in mein Gehirn als customer.countLastInvoiceItems().

Ob sich der Wartungsaufwand für die zusätzliche Kupplung lohnt, liegt bei Ihnen. (Ich mag auch kleine Funktionen in kleinen Klassen, deshalb neige ich dazu zu verketten. Ich sage nicht, dass es richtig ist - es ist genau das, was ich tue.)

JT Grimes
quelle
Das sollte IMO customer.NrLastInvoices oder customer.LastInvoice.NrItems sein. Diese Kette ist nicht zu lang, daher lohnt es sich wahrscheinlich nicht, sie zu
reduzieren,