Muster zum Übergeben des Kontexts durch eine Methodenkette

19

Hierbei handelt es sich um eine Entwurfsentscheidung, die anscheinend ziemlich häufig zu treffen ist: Wie kann der Kontext durch eine Methode geleitet werden, die ihn nicht benötigt? Gibt es eine richtige Antwort oder kommt es auf den Kontext an?

Beispielcode, der eine Lösung erfordert

// needs the dependency
function baz(session) {
  session('baz');
}

// doesn't care about the dependency
function bar() {
  baz();
}

// needs the dependency
function foo(session) {
   session('foo')
   bar();
}

// creates the dependency
function start() {
  let session = new Session();
  foo(session);
}

Mögliche Lösungen

  • threadlocal
  • global
  • Kontextobjekt
  • die Abhängigkeit weitergeben
  • curry baz und gib es in bar mit der abhängigkeit als erstes arg
  • Abhängigkeitsspritze

Beispiele dafür, wo es herkommt

HTTP-Anforderungsverarbeitung

Kontextobjekte in Form von Anforderungsattributen werden häufig verwendet: siehe expressjs, Java Servlets oder .net's owin.

Protokollierung

Für die Java-Protokollierung werden häufig Globals / Singletons verwendet. Siehe die typischen Log4J- / Commons-Logging- / Java-Logging-Muster.

Transaktionen

Thread-Locals werden häufig verwendet, um eine Transaktion oder Sitzung mit einer Kette von Methodenaufrufen zu verknüpfen, damit sie nicht als Parameter an alle Methoden übergeben werden müssen, die sie nicht benötigen.

Jamie McCrindle
quelle
Bitte verwenden Sie ein aussagekräftigeres Beispiel.
Tulains Córdova
Ich habe einige Beispiele hinzugefügt, wo es auftaucht.
Jamie McCrindle
3
Ich meinte einen aussagekräftigeren Beispielcode.
Tulains Córdova

Antworten:

11

Die einzig faire Antwort ist, dass es von den Redewendungen Ihres Programmierparadigmas abhängt. Wenn Sie OO verwenden, ist es mit ziemlicher Sicherheit falsch, eine Abhängigkeit von Methode zu Methode zu Methode zu übergeben. Es ist ein Code-Geruch in OO. Tatsächlich ist dies eines der Probleme, die OO löst - ein Objekt repariert einen Kontext. In OO besteht ein korrekter Ansatz (es gibt immer andere Möglichkeiten) darin, die Abhängigkeit über einen Konstruktor oder eine Eigenschaft zu liefern. Ein Kommentator erwähnt "Dependency Injection" und das ist absolut legitim, aber nicht unbedingt notwendig. Geben Sie einfach die Abhängigkeit an, damit sie als Mitglied für foound verfügbar ist baz.

Sie erwähnen das Currying, also gehe ich davon aus, dass funktionale Programmierung nicht ausgeschlossen ist. In diesem Fall ist die Schließung ein philosophisches Äquivalent des Objektkontexts. Jeder Ansatz, der die Abhängigkeit erneut behebt, sodass sie für abhängige Personen verfügbar ist, funktioniert einwandfrei. Currying ist ein solcher Ansatz (und es lässt Sie klug klingen). Denken Sie daran, dass es andere Möglichkeiten gibt, eine Abhängigkeit zu schließen. Einige von ihnen sind elegant und einige schrecklich.

Vergessen Sie nicht die aspektorientierte Programmierung . Es scheint in den letzten Jahren in Ungnade gefallen zu sein, aber sein primäres Ziel ist es, genau das Problem zu lösen , das Sie beschreiben. Das klassische Aspektbeispiel ist die Protokollierung. In AOP wird die Abhängigkeit automatisch hinzugefügt, nachdem anderer Code geschrieben wurde. AOP-Leute nennen das " Weben ". Gemeinsame Aspekte werden an den entsprechenden Stellen in den Code eingearbeitet. Dies erleichtert das Nachdenken über Ihren Code und ist verdammt cool, fügt aber auch eine neue Testlast hinzu. Sie müssen einen Weg finden, um festzustellen, ob Ihre endgültigen Artefakte einwandfrei sind. AOP hat auch dafür Antworten, also lassen Sie sich nicht einschüchtern.

Scant Roger
quelle
Die Behauptung, dass das Weitergeben von Parametern innerhalb von OO-Methoden ein Codegeruch ist, ist eine äußerst kontroverse Aussage. Ich würde das genaue Gegenteil behaupten: Das Ermutigen, Zustand und Funktionalität innerhalb einer Klasse zu mischen, ist einer der größten Fehler, die das OO-Paradigma begangen hat, und es zu vermeiden, indem Abhängigkeiten direkt in Methoden und nicht über einen Konstruktor eingefügt werden, ist ein Zeichen für eine gut gestaltete Arbeit Code, ob OO oder nicht.
David Arno
3
@DavidArno Ich würde vorschlagen, ein anderes Paradigma zu verwenden als die Schlussfolgerung, dass Objektstatus "einer der größten Fehler ist, die das OO-Paradigma macht", und dann das Paradigma zu umgehen. Ich habe nichts gegen fast jeden Ansatz, aber im Allgemeinen mag ich keinen Code, in dem der Autor gegen sein Werkzeug kämpft. Der Privatstaat ist ein Unterscheidungsmerkmal von OO. Wenn Sie diese Funktion meiden, verlieren Sie einen Teil der Leistung von OO.
Scant Roger
1
@DavidArno Eine Klasse, die nur aus dem Status und keiner Funktionalität besteht, verfügt über keinen Mechanismus zum Erzwingen invarianter Beziehungen im Status. Eine solche Klasse ist überhaupt nicht OO.
Kevin Krumwiede
@ KevinKrumwiede, bis zu einem gewissen Grad haben Sie auf meinen Kommentar reducto ad absudium angewendet, aber Ihr Standpunkt ist immer noch gut gemacht. Die Invarianz in Bezug auf den Staat ist ein wichtiger Teil des "Fortschreitens von OO". Das Vermeiden des Vermischens von Funktionalität und Zustand muss daher genügend Funktionalität in einem Zustandsobjekt ermöglichen, um eine Invarianz zu erreichen (eingekapselte Felder, die vom Konstruktor festgelegt wurden und auf die über Getter zugegriffen wird).
David Arno
@ScantRoger, ich stimme zu, dass ein anderes Paradigma übernommen werden kann, nämlich das Funktionsparadigma. Interessanterweise haben die meisten modernen "OO" -Sprachen eine wachsende Liste von Funktionsmerkmalen, und so ist es möglich, sich an diese Sprachen zu halten und das Funktionsparadigma zu übernehmen, ohne "das Werkzeug zu bekämpfen".
David Arno
10

Ist barabhängig von baz, was wiederum erfordert dependency, dann barerfordert dies dependencyauch, um richtig zu verwenden baz. Daher wäre es die richtige Vorgehensweise, die Abhängigkeit entweder als Parameter an weiterzuleiten baroder sie zu curry bazund weiterzuleiten bar.

Der erste Ansatz ist einfacher zu implementieren und zu lesen, schafft jedoch eine Kopplung zwischen barund baz. Der zweite Ansatz entfernt diese Kopplung, kann jedoch zu weniger klarem Code führen. Welcher Ansatz am besten ist, hängt daher wahrscheinlich von der Komplexität und dem Verhalten beider Funktionen ab. Wenn beispielsweise Nebenwirkungen auftreten bazoder dependencyauftreten, ist die Testfreundlichkeit wahrscheinlich ein wichtiger Faktor für die Auswahl der Lösung.

Ich würde vorschlagen, dass alle anderen von Ihnen vorgeschlagenen Optionen "hacky" sind und wahrscheinlich sowohl zu Problemen beim Testen als auch bei der Suche nach schwer auffindbaren Fehlern führen.

David Arno
quelle
1
Ich stimme fast vollständig zu. Abhängigkeitsinjektion kann ein weiterer nicht "hackiger" Ansatz sein.
Jonathan van de Veen
1
@JonathanvandeVeen, dependencyist die Abhängigkeitsinjektion sicherlich der eigentliche Vorgang des Umweges über Parameter?
David Arno
2
@DavidArno Abhängigkeiten-Injection-Frameworks entfernen diese Art von Abhängigkeiten nicht, sondern verschieben sie nur. Die Magie ist, dass sie sie außerhalb Ihres Unterrichts an einen Ort bringen, an dem das Testen das Problem von jemand anderem ist.
Kevin Krumwiede
@ JonathanvandeVeen Ich stimme zu, Abhängigkeitsinjektion ist eine gültige Lösung. In der Tat ist es diejenige, die ich am häufigsten auswählen würde.
Jamie McCrindle
1

Philosophisch gesprochen

Ich stimme der Besorgnis von David Arno zu .

Ich lese das OP als auf der Suche nach Implementierungslösungen. Die Antwort ist jedoch , das Design zu ändern . "Muster"? OO-Design dreht sich, könnte man sagen, alles um den Kontext. Es ist ein riesiges, leeres Blatt Papier mit unzähligen Möglichkeiten.

Der Umgang mit vorhandenem Code ist ein anderer Kontext.



Ich arbeite gerade an genau demselben Problem. Nun, ich korrigiere die Hunderte von Codezeilen beim Kopieren und Einfügen, damit ein Wert eingefügt werden kann.

Modularisieren Sie den Code

Ich habe 600 Zeilen doppelten Codes weggeworfen und dann überarbeitet, sodass anstelle von "A ruft B ruft C ruft D auf ..." ich "Ruf A, Rückgabe, Ruf B, Rückgabe, Ruf C auf ..." habe. Jetzt müssen wir den Wert nur noch in eine dieser Methoden einfügen, sagen wir Methode E.

Fügen Sie dem Konstruktor einen Standardparameter hinzu. Bestehende Anrufer ändern sich nicht - "optional" ist hier das maßgebliche Wort. Wird ein Argument nicht übergeben, wird der Standardwert verwendet. Dann nur 1 Zeile ändern, um die Variable in die überarbeitete, modulare Struktur zu übergeben; und eine kleine Änderung in Methode E, um es zu verwenden.


Verschlüsse

Ein Programmier-Thread - "Warum sollte ein Programm einen Abschluss verwenden?"

Im Wesentlichen injizieren Sie Werte in eine Methode, die eine mit den Werten angepasste Methode zurückgibt. Diese angepasste Methode wird anschließend ausgeführt.

Mit dieser Technik können Sie eine vorhandene Methode ändern, ohne ihre Signatur zu ändern.

Radarbob
quelle
Dieser Ansatz kommt
Roger über das Problem der zeitlichen Kopplung (Ihr Link), @Snowman. Es ist wichtig, dass die erforderliche Ausführungsreihenfolge gekapselt ist.
Radarbob