Gibt es einen Namen für das (Anti) Muster der Übergabe von Parametern, die nur auf mehreren Ebenen tief in der Aufrufkette verwendet werden?


209

Ich habe versucht, Alternativen zur Verwendung globaler Variablen in einem älteren Code zu finden. Bei dieser Frage geht es jedoch nicht um die technischen Alternativen, sondern hauptsächlich um die Terminologie .

Die naheliegende Lösung besteht darin, einen Parameter an die Funktion zu übergeben, anstatt einen globalen zu verwenden. In dieser alten Codebasis würde dies bedeuten, dass ich alle Funktionen in der langen Aufrufkette zwischen dem Punkt, an dem der Wert letztendlich verwendet wird, und der Funktion, die den Parameter zuerst empfängt, ändern muss.

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

Wo newParamwar früher eine globale Variable in meinem Beispiel, aber es könnte stattdessen ein zuvor fest codierter Wert gewesen sein. Der Punkt ist, dass jetzt der Wert von newParam erhalten wird higherlevel()und den ganzen Weg zu "reisen" muss level3().

Ich habe mich gefragt, ob es einen oder mehrere Namen für diese Art von Situation / Muster gibt, in denen Sie für viele Funktionen, die den Wert unverändert übergeben, einen Parameter hinzufügen müssen.

Wenn ich die richtige Terminologie verwende, finde ich hoffentlich mehr Ressourcen zu Lösungen für die Neugestaltung und beschreibe diese Situation den Kollegen.


94
Dies ist eine Verbesserung gegenüber der Verwendung globaler Variablen. Es macht deutlich, von welchem ​​Zustand jede Funktion abhängt (und ist ein Schritt auf dem Weg zu reinen Funktionen). Ich habe gehört, dass es einen Parameter als "Threading" bezeichnet, aber ich weiß nicht, wie häufig diese Terminologie ist.
Gardenhead

8
Dies ist ein zu breites Spektrum, um eine bestimmte Antwort zu haben. Auf dieser Ebene würde ich dies einfach "Codierung" nennen.
Machado

38
Ich denke, das "Problem" ist nur die Einfachheit. Dies ist im Grunde eine Abhängigkeitsinjektion. Ich vermute, es könnte Mechanismen geben, die die Abhängigkeit automatisch durch die Kette injizieren, wenn es ein tiefer verschachteltes Mitglied gibt, ohne die Parameterlisten der Funktionen aufzublähen. Vielleicht führt die Betrachtung von Abhängigkeitsinjektionsstrategien mit unterschiedlichen Verfeinerungsstufen zu der Terminologie, nach der Sie suchen, falls es eine gibt.
null

7
Obwohl ich die Diskussion darüber, ob es ein gutes Muster / Antimuster / Konzept / Lösung ist, sehr schätze, wollte ich unbedingt wissen, ob es einen Namen dafür gibt.
Ecerulm

3
Ich habe auch davon gehört genannt Einfädeln am häufigsten, aber auch Sanitärbereich , wie es in einer Senkung Senkblei während des Call - Stack.
wchargin

Antworten:


202

Die Daten selbst werden "Tramp-Daten" genannt . Es ist ein "Codegeruch", der anzeigt, dass ein Codeteil über Zwischenhändler mit einem anderen Codeteil auf Distanz kommuniziert.

  • Erhöht die Code-Steifheit, insbesondere in der Aufrufkette. Sie sind viel gezwungener, eine Methode in der Aufrufkette umzugestalten.
  • Verteilt das Wissen über Daten / Methoden / Architektur an Orte, die sich überhaupt nicht darum kümmern. Wenn Sie die gerade durchlaufenden Daten deklarieren müssen und die Deklaration einen neuen Import erfordert, haben Sie den Namensraum verschmutzt.

Das Refactoring zum Entfernen globaler Variablen ist schwierig, und Tramp-Daten sind eine Methode und oft die günstigste. Es hat seine Kosten.


73
Durch die Suche nach "tramp data" konnte ich in meinem Safari-Abonnement das Buch "Code Complete" finden. Es gibt einen Abschnitt im Buch mit dem Titel "Gründe für die Verwendung globaler Daten" und einer der Gründe ist "Die Verwendung globaler Daten kann Fremddaten eliminieren". :). Ich glaube, mit "Tramp-Daten" kann ich mehr Literatur zum Umgang mit Globalen finden. Vielen Dank!
Ecerulm

9
@ JimmyJames, diese Funktionen machen natürlich Sachen. Nur nicht mit diesem speziellen neuen Parameter, der zuvor nur ein globaler war.
Ecerulm

174
In 20 Jahren Programmierung habe ich diesen Begriff noch nie zuvor gehört und es wäre nicht sofort klar gewesen, was er bedeutet. Ich beschwere mich nicht über die Antwort und behaupte nur, dass der Begriff nicht so weit verbreitet ist. Vielleicht bin es nur ich.
Derek Elkins

6
Einige globale Daten sind in Ordnung. Anstatt es "globale Daten" zu nennen, können Sie es "Umgebung" nennen - denn das ist es. Die Umgebung kann beispielsweise einen Zeichenfolgenpfad für AppData (unter Windows) oder in meinem aktuellen Projekt den gesamten Satz von GDI + -Pinseln, Stiften, Schriftarten usw. enthalten, der von allen Komponenten verwendet wird.
Robinson

7
@ Robinson Nicht ganz. Möchten Sie beispielsweise wirklich, dass der Code zum Schreiben von Bildern% AppData% berührt, oder möchten Sie, dass er ein Argument für den Schreibort enthält? Das ist der Unterschied zwischen einem globalen Staat und einem Argument. "Umwelt" kann ebenso leicht eine injizierte Abhängigkeit sein, die nur für diejenigen vorhanden ist, die für die Interaktion mit der Umwelt verantwortlich sind. GDI + -Pinsel usw. sind vernünftiger, aber das ist eigentlich eher ein Fall von Ressourcenverwaltung in einer Umgebung, in der dies für Sie nicht möglich ist - so ziemlich ein Mangel der zugrunde liegenden APIs und / oder Ihrer Sprache / Bibliotheken / Laufzeit.
Luaan

102

Ich denke nicht, dass dies an sich ein Anti-Muster ist. Ich denke, das Problem ist, dass Sie die Funktionen als Kette betrachten, wenn Sie wirklich jede als unabhängige Blackbox betrachten sollten. ( HINWEIS : Rekursive Methoden sind eine bemerkenswerte Ausnahme von diesem Rat.)

Angenommen, ich muss die Anzahl der Tage zwischen zwei Kalenderdaten berechnen, damit ich eine Funktion erstelle:

int daysBetween(Day a, Day b)

Dazu erstelle ich dann eine neue Funktion:

int daysSinceEpoch(Day day)

Dann wird meine erste Funktion einfach:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

Das ist nichts gegen Muster. Die Parameter der daysBetween-Methode werden an eine andere Methode übergeben und in der Methode nicht anderweitig referenziert. Sie werden jedoch weiterhin benötigt, damit diese Methode die erforderlichen Aktionen ausführt.

Ich würde empfehlen, jede Funktion zu betrachten und mit ein paar Fragen zu beginnen:

  • Hat diese Funktion ein klares und zielgerichtetes Ziel oder handelt es sich um eine Methode, mit der einige Dinge erledigt werden können? Normalerweise hilft hier der Name der Funktion, und wenn sich etwas darin befindet, das nicht durch den Namen beschrieben wird, ist das eine rote Fahne.
  • Gibt es zu viele Parameter? Manchmal kann eine Methode zu Recht viele Eingaben erfordern, aber so viele Parameter zu haben, erschwert die Verwendung oder das Verständnis.

Wenn Sie ein Durcheinander von Code ohne einen einzigen Zweck betrachten, der in einer Methode gebündelt ist, sollten Sie damit beginnen, dies zu entwirren. Das kann mühsam sein. Beginnen Sie mit den einfachsten Methoden und wiederholen Sie diese, bis Sie etwas Kohärentes gefunden haben.

Wenn Sie nur zu viele Parameter haben, ziehen Sie das Refactoring von Methode zu Objekt in Betracht .


2
Nun, ich wollte nicht kontrovers mit dem (Anti) sein. Aber ich frage mich immer noch, ob es einen Namen für die "Situation" gibt, viele Funktionssignaturen aktualisieren zu müssen. Ich denke, es ist eher ein "Codegeruch" als ein Antimuster. Es sagt mir, dass in diesem Legacy-Code etwas behoben werden muss, wenn ich die Signatur mit 6 Funktionen aktualisieren muss, um die Beseitigung eines globalen Codes zu ermöglichen. Aber ich denke, dass das Übergeben von Parametern normalerweise in Ordnung ist, und ich schätze den Rat, wie das zugrunde liegende Problem angegangen werden kann.
Ecerulm

4
@ecerulm Ich kenne keinen, aber ich werde sagen, dass meine Erfahrung zeigt, dass das Konvertieren der Globalen in Parameter absolut der richtige Weg ist, um sie zu eliminieren. Dadurch wird der gemeinsam genutzte Status aufgehoben, sodass Sie eine weitere Umgestaltung vornehmen können. Ich vermute, es gibt weitere Probleme mit diesem Code, aber Ihre Beschreibung enthält nicht genug Informationen, um zu erkennen, um welche es sich handelt.
JimmyJames

2
Normalerweise folge ich auch diesem Ansatz und werde es wahrscheinlich auch in diesem Fall tun. Ich wollte nur mein Vokabular / meine Terminologie verbessern, damit ich mehr darüber recherchieren und in Zukunft besser fokussierte Fragen stellen kann.
Ecerulm

3
@ecerulm Ich glaube nicht, dass es einen Namen dafür gibt. Es ist wie ein Symptom, das sowohl bei vielen Krankheiten als auch bei Nicht-Krankheitszuständen auftritt, z. B. bei trockenem Mund. Wenn Sie die Beschreibung der Struktur des Codes konkretisieren, kann dies auf etwas Bestimmtes hindeuten.
JimmyJames

@ecerulm Es sagt Ihnen, dass etwas repariert werden muss - jetzt ist es viel offensichtlicher, dass etwas repariert werden muss, als es war, als es stattdessen eine globale Variable war.
immibis

61

BobDalgleish hat bereits bemerkt, dass dieses (Anti) Muster " Tramp-Daten " genannt wird.

Nach meiner Erfahrung besteht die häufigste Ursache für übermäßige Vagabunddaten in einer Reihe verknüpfter Statusvariablen, die wirklich in ein Objekt oder eine Datenstruktur eingekapselt werden sollten. Manchmal kann es sogar erforderlich sein, eine Reihe von Objekten zu verschachteln, um die Daten ordnungsgemäß zu organisieren.

Für ein einfaches Beispiel betrachten wir ein Spiel , das eine anpassbare Spielercharakter, mit Eigenschaften wie hat playerName, playerEyeColorund so weiter. Natürlich hat der Spieler auch eine physische Position auf der Spielkarte und verschiedene andere Eigenschaften wie zum Beispiel die aktuelle und maximale Gesundheitsstufe und so weiter.

In einer ersten Iteration eines solchen Spiels kann es durchaus sinnvoll sein, all diese Eigenschaften in globale Variablen umzuwandeln - schließlich gibt es nur einen Spieler, und fast alles im Spiel bezieht den Spieler irgendwie mit ein. Ihr globaler Status kann also Variablen enthalten wie:

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

Aber irgendwann werden Sie vielleicht feststellen, dass Sie dieses Design ändern müssen, vielleicht weil Sie dem Spiel einen Mehrspielermodus hinzufügen möchten. Als ersten Versuch könnten Sie versuchen, alle diese Variablen lokal zu machen und sie an Funktionen zu übergeben, die sie benötigen. Dann stellen Sie jedoch möglicherweise fest, dass zu einer bestimmten Aktion in Ihrem Spiel eine Funktionsaufrufkette gehört, beispielsweise:

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

... und die interactWithShopkeeper()Funktion hat den Ladenbesitzer den Spieler mit Namen angesprochen, so dass Sie nun plötzlich alle diese Funktionen playerNameals Fremddaten durchlaufen müssen. Und wenn der Ladenbesitzer blauäugige Spieler für naiv hält und ihnen höhere Preise in Rechnung stellt, müssen Sie natürlich die gesamte Funktionskette durchlaufen und so weiter.playerEyeColor

In diesem Fall besteht die richtige Lösung natürlich darin, ein Spielerobjekt zu definieren, das den Namen, die Augenfarbe, die Position, den Gesundheitszustand und andere Eigenschaften des Spielercharakters enthält. Auf diese Weise müssen Sie nur dieses einzelne Objekt an alle Funktionen weitergeben, an denen der Player beteiligt ist.

Einige der oben genannten Funktionen könnten natürlich auch in Methoden dieses Spielerobjekts umgewandelt werden, die ihnen automatisch Zugriff auf die Eigenschaften des Spielers gewähren würden. In gewisser Weise handelt es sich nur um syntaktischen Zucker, da der Aufruf einer Methode für ein Objekt die Objektinstanz ohnehin als verborgenen Parameter an die Methode übergibt, aber den Code bei richtiger Verwendung klarer und natürlicher erscheinen lässt.

Natürlich hätte ein typisches Spiel viel mehr "globale" Zustände als nur der Spieler; Zum Beispiel hätten Sie mit ziemlicher Sicherheit eine Art Karte, auf der das Spiel stattfindet, und eine Liste von Nicht-Spieler-Charakteren, die sich auf der Karte bewegen, und vielleicht Gegenstände, die darauf platziert sind, und so weiter. Sie könnten all diese Objekte auch als Tramp-Objekte weitergeben, aber das würde Ihre Methodenargumente wieder überladen.

Stattdessen besteht die Lösung darin, die Objekte Verweise auf andere Objekte speichern zu lassen, zu denen sie dauerhafte oder temporäre Beziehungen haben. So sollte beispielsweise das Spielerobjekt (und wahrscheinlich auch alle NPC-Objekte) wahrscheinlich einen Verweis auf das Objekt "Spielwelt" speichern, der einen Verweis auf das aktuelle Level / die aktuelle Karte enthalten würde, damit eine Methode wie player.moveTo(x, y)diese nicht erforderlich ist Die Map muss explizit als Parameter angegeben werden.

In ähnlicher Weise würden wir, wenn unsere Spielerfigur beispielsweise einen Haustierhund hätte, der ihnen folgte, natürlich alle Zustandsvariablen, die den Hund beschreiben, zu einem einzigen Objekt zusammenfassen und dem Spielerobjekt einen Verweis auf den Hund geben (damit der Spieler dies kann) Sagen wir, nennen Sie den Hund beim Namen) und umgekehrt (damit der Hund weiß, wo der Spieler ist). Und natürlich möchten wir den Spieler und den Hund wahrscheinlich zu Unterklassen eines allgemeineren "Schauspieler" -Objekts machen, damit wir denselben Code wiederverwenden können, um beispielsweise beide auf der Karte zu bewegen.

Ps. Obwohl ich ein Spiel als Beispiel genommen habe, gibt es andere Arten von Programmen, bei denen solche Probleme ebenfalls auftauchen. Meiner Erfahrung nach ist das zugrunde liegende Problem jedoch immer dasselbe: Es gibt eine Reihe separater Variablen (lokal oder global), die wirklich zu einem oder mehreren miteinander verknüpften Objekten zusammengefasst werden sollen. Unabhängig davon, ob die in Ihre Funktionen eingebrachten "Tramp-Daten" aus "globalen" Optionseinstellungen oder zwischengespeicherten Datenbankabfragen oder Zustandsvektoren in einer numerischen Simulation bestehen, besteht die Lösung ausnahmslos darin, den natürlichen Kontext zu identifizieren, zu dem die Daten gehören, und diesen zu einem Objekt zu machen (oder was auch immer das nächste Äquivalent in Ihrer gewählten Sprache ist).


1
Diese Antwort enthält einige Lösungen für eine Reihe von Problemen, die möglicherweise vorliegen. Es kann Situationen geben, in denen Globale verwendet werden, die auf eine andere Lösung hinweisen. Ich habe ein Problem mit der Idee, dass das Einfügen der Methoden in die Player-Klasse dem Übergeben von Objekten an Methoden entspricht. Dies ignoriert den Polymorphismus, der auf diese Weise nicht leicht zu replizieren ist. Wenn ich zum Beispiel verschiedene Arten von Spielern erstellen möchte, die unterschiedliche Regeln für Bewegungen und unterschiedliche Arten von Eigenschaften haben, erfordert die einfache Übergabe dieser Objekte an eine Methodenimplementierung eine Menge Bedingungslogik.
JimmyJames

6
@JimmyJames: Ihr Punkt zum Polymorphismus ist gut, und ich habe darüber nachgedacht, ihn selbst zu machen, ihn aber weggelassen, damit die Antwort nicht noch länger wird. Der Punkt , den ich versuche , (vielleicht schlecht), war, dass, während lediglich in Bezug auf den Daten dort fließen ist wenig Unterschied zwischen foo.method(bar, baz)und method(foo, bar, baz)gibt es andere Gründe (einschließlich Polymorphismus, Kapselung, Ort usw.) , um die ersteren zu bevorzugen.
Ilmari Karonen

@IlmariKaronen: auch der sehr offensichtliche Vorteil, dass die Funktionsprototypen von zukünftigen Änderungen / Hinzufügungen / Löschungen / Umgestaltungen in den Objekten (z. B. playerAge) zukunftssicher sind. Das allein ist von unschätzbarem Wert.
smci

34

Ich kenne keinen bestimmten Namen dafür, aber ich denke, es ist erwähnenswert, dass das von Ihnen beschriebene Problem nur das Problem ist, den besten Kompromiss für den Umfang eines solchen Parameters zu finden:

  • Als globale Variable ist der Gültigkeitsbereich zu groß, wenn das Programm eine bestimmte Größe erreicht

  • Als rein lokaler Parameter ist der Gültigkeitsbereich möglicherweise zu klein, wenn es zu vielen sich wiederholenden Parameterlisten in Aufrufketten kommt

  • Als Kompromiss kann man einen solchen Parameter oft zu einer Mitgliedsvariablen in einer oder mehreren Klassen machen, und das ist das, was ich einfach als richtiges Klassendesign bezeichnen würde .


10
+1 für die richtige Klassengestaltung. Das klingt nach einem klassischen Problem, das auf eine OO-Lösung wartet.
l0b0

21

Ich glaube, das Muster, das Sie beschreiben, ist genau die Abhängigkeitsinjektion . Mehrere Kommentatoren haben argumentiert, dass dies ein Muster ist , kein Anti-Muster , und ich würde eher zustimmen.

Ich stimme auch der Antwort von @ JimmyJames zu, in der er behauptet, es sei eine gute Programmierpraxis, jede Funktion als Black Box zu behandeln, die alle ihre Eingaben als explizite Parameter verwendet. Das heißt, wenn Sie eine Funktion schreiben, die ein Erdnussbutter-Gelee-Sandwich macht, können Sie es so schreiben

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

Es ist jedoch besser , die Abhängigkeitsinjektion anzuwenden und sie stattdessen wie folgt zu schreiben:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

Jetzt haben Sie eine Funktion, die alle Abhängigkeiten in ihrer Funktionssignatur klar dokumentiert, was für die Lesbarkeit von Vorteil ist. Immerhin ist es wahr, dass, um make_sandwichSie Zugriff auf ein benötigen Refrigerator; Daher war die alte Funktionssignatur im Grunde genommen unaufrichtig, da der Kühlschrank nicht als Teil seiner Eingaben verwendet wurde.

Als Bonus können Sie, wenn Sie Ihre Klassenhierarchie richtig machen, das Schneiden vermeiden und so weiter, die make_sandwichFunktion sogar Unit-testen, indem Sie ein MockRefrigerator! (Möglicherweise müssen Sie es auf diese Weise Unit-testen, da Ihre Unit-Test-Umgebung möglicherweise keinen Zugriff auf PhysicalRefrigerators hat.)

Ich verstehe, dass nicht alle Verwendungen der Abhängigkeitsinjektion die Installation eines ähnlich benannten Parameters auf mehreren Ebenen des Aufrufstapels erfordern. Daher beantworte ich nicht genau die Frage, die Sie gestellt haben. "Dependency Injection" ist definitiv ein relevantes Schlüsselwort für Sie.


10
Dies ist sehr deutlich ein Anti- Muster. Es gibt absolut keine Aufforderung, einen Kühlschrank weiterzugeben. Nun, die Übergabe einer generischen IngredientSource könnte funktionieren, aber was ist, wenn Sie das Brot aus dem Brotkasten, den Thunfisch aus der Speisekammer, den Käse aus dem Kühlschrank ... erhalten, indem Sie eine Abhängigkeit von der Quelle der Zutaten in die unabhängige Operation der Bildung dieser Zutaten einbringen Zutaten in ein Sandwich, haben Sie die Trennung von Bedenken verletzt und einen Code-Geruch gemacht.
Dewi Morgan

8
@DewiMorgan: Offensichtlich könnten Sie die Verallgemeinerung Refrigeratorin eine weiter umgestalten IngredientSourceoder sogar den Begriff "Sandwich" in verallgemeinern template<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&); Das nennt sich "generische Programmierung" und ist einigermaßen leistungsfähig, aber es ist sicherlich viel geheimnisvoller, als das OP im Moment wirklich möchte. Sie können gerne eine neue Frage zum Abstraktionsgrad von Sandwich-Programmen stellen. ;)
Quuxplusone

11
Seien Sie da, kein Fehler, der nicht privilegierte Benutzer sollte keinen Zugriff auf haben make_sandwich().
Dotancohen

2
@Dewi - XKCD Link
Gavin Lock

19
Der schwerwiegendste Fehler in Ihrem Code ist, dass Sie Erdnussbutter im Kühlschrank aufbewahren.
Malvolio

15

Dies ist so ziemlich die Lehrbuchdefinition der Kopplung , bei der ein Modul eine Abhängigkeit aufweist, die sich stark auf ein anderes auswirkt und die bei Änderungen einen Welligkeitseffekt erzeugt. Die anderen Kommentare und Antworten stimmen, dass dies eine Verbesserung gegenüber dem globalen ist, da die Kopplung jetzt expliziter und für den Programmierer leichter zu sehen ist als subversiv. Das heißt nicht, dass es nicht behoben werden sollte. Sie sollten in der Lage sein, die Kupplung zu refaktorieren, um sie zu entfernen oder zu reduzieren, obwohl es schmerzhaft sein kann, wenn sie eine Weile darin steckt.


3
Wenn level3()nötig newParam, ist das sicher eine Kopplung, aber irgendwie müssen verschiedene Teile des Codes miteinander kommunizieren. Ich würde einen Funktionsparameter nicht unbedingt als schlechte Kopplung bezeichnen, wenn diese Funktion den Parameter verwendet. Ich denke, der problematische Aspekt der Kette ist die zusätzliche Kupplung, die eingeführt wurde level1()und level2()die newParamnur zum Weitergeben verwendet werden kann. Gute Antwort, +1 für die Kopplung.
null

6
@null Wenn sie es wirklich nicht gebraucht hätten, könnten sie sich einen Wert ausdenken, anstatt ihn von ihrem Anrufer zu erhalten.
Random832

3

Diese Antwort beantwortet Ihre Frage zwar nicht direkt , aber ich bin der Meinung, dass ich es versäumen würde, sie zu übergehen, ohne zu erwähnen, wie man sie verbessern kann (da es, wie Sie sagen, ein Anti-Pattern sein kann). Ich hoffe, dass Sie und andere Leser von diesem zusätzlichen Kommentar profitieren können, wie man "Tramp-Daten" vermeidet (wie Bob Dalgleish es so hilfreich für uns nannte).

Ich stimme Antworten zu, die darauf hindeuten, etwas mehr OO zu tun, um dieses Problem zu vermeiden. Eine andere Möglichkeit, diese Übergabe von Argumenten zu reduzieren, ohne nur zu "Übergeben Sie eine Klasse, in der Sie früher viele Argumente übergeben haben! " Zu wechseln, besteht darin, einige Schritte Ihres Prozesses auf der höheren Ebene statt auf der niedrigeren Ebene auszuführen einer. Hier ist zum Beispiel ein Vorher- Code:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

Beachten Sie, dass dies umso schlimmer wird, je mehr Dinge erledigt werden müssen ReportStuff. Möglicherweise müssen Sie die Instanz des Reporters übergeben, den Sie verwenden möchten. Und alle möglichen Abhängigkeiten, die weitergegeben werden müssen, funktionieren in verschachtelten Funktionen.

Mein Vorschlag ist, all das auf eine höhere Ebene zu heben, wo das Wissen über die Schritte Leben in einer einzelnen Methode erfordert, anstatt über eine Kette von Methodenaufrufen verteilt zu sein. Natürlich wäre es in echtem Code komplizierter, aber das gibt Ihnen eine Idee:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

Beachten Sie, dass der große Unterschied darin besteht, dass Sie die Abhängigkeiten nicht durch eine lange Kette führen müssen. Selbst wenn Sie nicht nur auf eine Ebene, sondern auf einige Ebenen abflachen, haben Sie eine Verbesserung erzielt, wenn diese Ebenen auch eine gewisse "Abflachung" erzielen, sodass der Prozess als eine Reihe von Schritten auf dieser Ebene angesehen wird.

Während dies noch prozedural ist und noch nichts in ein Objekt umgewandelt wurde, ist es ein guter Schritt, um zu entscheiden, welche Art von Kapselung Sie erreichen können, indem Sie etwas in eine Klasse umwandeln. Die tief verketteten Methodenaufrufe im vorherigen Szenario verbergen die Details dessen, was tatsächlich passiert, und können dazu führen, dass der Code sehr schwer zu verstehen ist. Obwohl Sie dies übertreiben und den Code auf höherer Ebene über die Dinge informieren können, die er nicht wissen sollte, oder eine Methode entwickeln können, die zu viele Dinge tut und so gegen das Prinzip der einmaligen Verantwortung verstößt, habe ich im Allgemeinen festgestellt, dass das Reduzieren von Dingen ein wenig hilfreich ist in der Klarheit und bei der schrittweisen Änderung in Richtung eines besseren Codes.

Beachten Sie, dass Sie dabei die Testbarkeit berücksichtigen sollten. Die verketteten Methodenaufrufe erschweren das Testen von Units tatsächlich, da Sie für das zu testende Segment keinen guten Einstiegs- und Ausstiegspunkt in der Assembly haben. Beachten Sie, dass Ihre Methoden mit dieser Reduzierung, da sie nicht mehr so ​​viele Abhängigkeiten aufweisen, einfacher zu testen sind und nicht so viele Mocks erfordern!

Ich habe kürzlich versucht, Komponententests zu einer Klasse hinzuzufügen (die ich nicht geschrieben habe), die ungefähr 17 Abhängigkeiten hat, die alle verspottet werden mussten! Ich habe noch nicht alles geklappt, aber ich habe die Klasse in drei Klassen aufgeteilt, die sich jeweils mit einem der einzelnen Nomen befassten, und die Abhängigkeitsliste auf 12 für das schlechteste und ungefähr 8 für das schlechteste Beste.

Durch die Testbarkeit werden Sie gezwungen , besseren Code zu schreiben. Sie sollten Komponententests schreiben, da Sie dadurch Ihren Code anders überdenken und von Anfang an besseren Code schreiben, unabhängig davon, wie wenige Fehler Sie möglicherweise hatten, bevor Sie die Komponententests geschrieben haben.


2

Sie verletzen nicht wörtlich das Gesetz von Demeter, aber Ihr Problem ähnelt dem in gewisser Weise. Da es bei Ihrer Frage darum geht, Ressourcen zu finden, empfehle ich Ihnen, das Demeter-Gesetz zu lesen und zu prüfen, inwieweit dieser Rat auf Ihre Situation zutrifft.


1
Ein bisschen schwach in Details, was wahrscheinlich die Ablehnungen erklärt. Im Geiste ist diese Antwort jedoch genau richtig: OP sollte sich über das Gesetz von Demeter informieren - dies ist der relevante Begriff.
Konrad Rudolph

4
FWIW, ich denke nicht, dass das Gesetz von Demeter (alias "geringstes Privileg") überhaupt relevant ist. In dem Fall des OP wäre seine Funktion nicht in der Lage, ihre Arbeit zu erledigen, wenn sie nicht die Tramp-Daten hätte (weil der nächste Mann auf dem Call-Stack sie braucht, weil der nächste Mann sie braucht, und so weiter). Das geringste Privileg / Gesetz von Demeter ist nur relevant, wenn der Parameter wirklich nicht verwendet wird. In diesem Fall ist die Korrektur offensichtlich: Entfernen Sie den nicht verwendeten Parameter!
Quuxplusone

2
Die Situation dieser Frage hat genau nichts mit dem Gesetz von Demeter zu tun ... Es gibt eine oberflächliche Ähnlichkeit in Bezug auf die Kette von Methodenaufrufen, aber ansonsten ist es sehr unterschiedlich.
Eric King

@Quuxplusone Möglich, obwohl die Beschreibung in diesem Fall ziemlich verwirrend ist, da verkettete Aufrufe in diesem Szenario keinen Sinn ergeben: Sie sollten stattdessen verschachtelt sein .
Konrad Rudolph

1
Das Problem ist LoD-Verstößen sehr ähnlich, da das übliche Umgestalten, das vorgeschlagen wird, um LoD-Verstöße zu behandeln, darin besteht, Tramp-Daten einzuführen. IMHO, dies ist ein guter Ausgangspunkt für die Reduzierung der Kopplung, aber es ist nicht ausreichend.
Jørgen Fogh

1

Es gibt Fälle, in denen es am besten ist (in Bezug auf Effizienz, Wartbarkeit und einfache Implementierung), bestimmte Variablen als global zu definieren, und nicht den Aufwand, immer alles zu übergeben (etwa 15 Variablen müssen bestehen bleiben). Daher ist es sinnvoll, eine Programmiersprache zu finden, die das Scoping besser unterstützt (als private statische Variablen in C ++), um das potenzielle Durcheinander (von Namespace und Manipulationen) zu verringern. Natürlich ist das nur allgemein bekannt.

Der vom OP vorgegebene Ansatz ist jedoch sehr nützlich, wenn funktionale Programmierung ausgeführt wird.


0

Es gibt hier überhaupt kein Antimuster, da der Anrufer nicht über alle diese Ebenen unter Bescheid weiß und sich nicht darum kümmert.

Jemand ruft higherLevel (params) auf und erwartet, dass higherLevel seine Arbeit erledigt. Was higherLevel mit params macht, geht den Anrufer nichts an. higherLevel behandelt das Problem so gut wie möglich. In diesem Fall übergeben Sie params an level1 (params). Das ist absolut in Ordnung.

Sie sehen eine Anrufkette - aber es gibt keine Anrufkette. Es gibt eine Funktion an der Spitze, die ihren Job so gut wie möglich macht. Und es gibt noch andere Funktionen. Jede Funktion kann jederzeit ersetzt werden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.