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 newParam
war 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.
Antworten:
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.
Das Refactoring zum Entfernen globaler Variablen ist schwierig, und Tramp-Daten sind eine Methode und oft die günstigste. Es hat seine Kosten.
quelle
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:
Dazu erstelle ich dann eine neue Funktion:
Dann wird meine erste Funktion einfach:
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:
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 .
quelle
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
,playerEyeColor
und 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:
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:
... und die
interactWithShopkeeper()
Funktion hat den Ladenbesitzer den Spieler mit Namen angesprochen, so dass Sie nun plötzlich alle diese FunktionenplayerName
als 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).
quelle
foo.method(bar, baz)
undmethod(foo, bar, baz)
gibt es andere Gründe (einschließlich Polymorphismus, Kapselung, Ort usw.) , um die ersteren zu bevorzugen.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 .
quelle
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
Es ist jedoch besser , die Abhängigkeitsinjektion anzuwenden und sie stattdessen wie folgt zu schreiben:
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_sandwich
Sie Zugriff auf ein benötigenRefrigerator
; 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_sandwich
Funktion sogar Unit-testen, indem Sie einMockRefrigerator
! (Möglicherweise müssen Sie es auf diese Weise Unit-testen, da Ihre Unit-Test-Umgebung möglicherweise keinen Zugriff aufPhysicalRefrigerator
s 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.
quelle
Refrigerator
in eine weiter umgestaltenIngredientSource
oder sogar den Begriff "Sandwich" in verallgemeinerntemplate<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. ;)make_sandwich()
.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.
quelle
level3()
nötignewParam
, 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 wurdelevel1()
undlevel2()
dienewParam
nur zum Weitergeben verwendet werden kann. Gute Antwort, +1 für die Kopplung.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:
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:
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.
quelle
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.
quelle
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.
quelle
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.
quelle