Ich habe Stuart Sierras Vortrag " Thinking In Data " gesehen und eine der Ideen daraus als Designprinzip in diesem Spiel übernommen, das ich mache. Der Unterschied ist, dass er in Clojure arbeitet und ich in JavaScript arbeite. Ich sehe einige wesentliche Unterschiede zwischen unseren Sprachen darin:
- Clojure ist eine idiomatisch funktionierende Programmierung
- Der meiste Staat ist unveränderlich
Ich habe die Idee von der Folie "Alles ist eine Karte" übernommen (von 11 Minuten, 6 Sekunden bis> 29 Minuten). Einige Dinge, die er sagt, sind:
- Wann immer Sie eine Funktion sehen, die 2-3 Argumente akzeptiert, können Sie ein Argument dafür vorgeben, sie in eine Karte umzuwandeln und nur eine Karte hineinzugeben. Das hat viele Vorteile:
- Sie müssen sich keine Gedanken über die Reihenfolge der Argumente machen
- Sie müssen sich nicht um zusätzliche Informationen kümmern. Wenn es zusätzliche Schlüssel gibt, ist das nicht wirklich unser Anliegen. Sie fließen einfach durch, sie stören nicht.
- Sie müssen kein Schema definieren
- Im Gegensatz zur Übergabe eines Objekts werden keine Daten ausgeblendet. Er macht jedoch den Fall, dass das Verbergen von Daten Probleme verursachen kann und überbewertet wird:
- Performance
- Leichtigkeit der Durchsetzung
- Sobald Sie über das Netzwerk oder prozessübergreifend kommunizieren, müssen sich beide Seiten auf die Datendarstellung einigen. Das ist zusätzliche Arbeit, die Sie überspringen können, wenn Sie nur an Daten arbeiten.
Am relevantesten für meine Frage. Dies ist 29 Minuten in: "Machen Sie Ihre Funktionen zusammensetzbar". Hier ist das Codebeispiel, anhand dessen er das Konzept erklärt:
;; Bad (defn complex-process [] (let [a (get-component @global-state) b (subprocess-one a) c (subprocess-two a b) d (subprocess-three a b c)] (reset! global-state d))) ;; Good (defn complex-process [state] (-> state subprocess-one subprocess-two subprocess-three))
Ich verstehe, dass die Mehrheit der Programmierer mit Clojure nicht vertraut ist, daher schreibe ich dies im imperativen Stil um:
;; Good def complex-process(State state) state = subprocess-one(state) state = subprocess-two(state) state = subprocess-three(state) return state
Hier sind die Vorteile:
- Einfach zu testen
- Einfach, diese Funktionen isoliert zu betrachten
- Einfach eine Zeile davon auskommentieren und sehen, was das Ergebnis ist, indem ein einzelner Schritt entfernt wird
- Jeder Unterprozess kann dem Status weitere Informationen hinzufügen. Wenn ein Unterprozess etwas an den Unterprozess drei kommunizieren muss, ist es so einfach wie das Hinzufügen eines Schlüssels / Werts.
- Kein Boilerplate, um die benötigten Daten aus dem Status zu extrahieren, nur damit Sie sie wieder speichern können. Geben Sie einfach den gesamten Status ein und lassen Sie den Subprozess zuweisen, was er benötigt.
Nun zurück zu meiner Situation: Ich nahm diese Lektion und wandte sie auf mein Spiel an. Das heißt, fast alle meine übergeordneten Funktionen nehmen ein gameState
Objekt und geben es zurück . Dieses Objekt enthält alle Daten des Spiels. EG: Eine Liste von BadGuys, eine Liste von Menüs, die Beute auf dem Boden usw. Hier ist ein Beispiel für meine Update-Funktion:
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
Was ich hier fragen möchte, ist, ob ich einen Gräuel erschaffen habe, der eine Idee verkehrt macht, die nur in einer funktionalen Programmiersprache praktikabel ist? JavaScript ist nicht idiomatisch (obwohl es so geschrieben werden kann) und es ist wirklich eine Herausforderung, unveränderliche Datenstrukturen zu schreiben. Eine Sache, die mich betrifft, ist , dass er annimmt, dass jeder dieser Teilprozesse rein ist. Warum muss diese Annahme gemacht werden? Es ist selten, dass eine meiner Funktionen rein ist (damit meine ich, dass sie häufig die Funktionen modifizieren gameState
. Ich habe keine anderen komplizierten Nebenwirkungen als diese). Fallen diese Ideen auseinander, wenn Sie keine unveränderlichen Daten haben?
Ich mache mir Sorgen, dass ich eines Tages aufwache und merke, dass dieses ganze Design eine Täuschung ist, und ich habe gerade das Anti-Pattern Big Ball Of Mud implementiert .
Ehrlich gesagt arbeite ich seit Monaten an diesem Code und es war großartig. Ich habe das Gefühl, dass ich alle Vorteile erhalte, die er behauptet. Mein Code ist super einfach für mich zu überlegen . Aber ich bin ein Ein-Mann-Team, also habe ich den Fluch des Wissens.
Aktualisieren
Ich habe 6+ Monate mit diesem Muster programmiert. Normalerweise vergesse ich zu diesem Zeitpunkt, was ich getan habe und dort "habe ich das sauber geschrieben?" kommt ins Spiel. Wenn ich nicht hätte, würde ich wirklich kämpfen. Bisher habe ich überhaupt keine Probleme.
Ich verstehe, wie ein anderer Satz Augen notwendig wäre, um seine Wartbarkeit zu überprüfen. Ich kann nur sagen, dass mir in erster Linie die Wartbarkeit am Herzen liegt. Ich bin immer der lauteste Evangelist für sauberen Code, egal wo ich arbeite.
Ich möchte direkt auf diejenigen antworten, die bereits schlechte persönliche Erfahrungen mit dieser Art der Codierung gemacht haben. Ich wusste es damals noch nicht, aber ich denke, wir sprechen wirklich über zwei verschiedene Arten, Code zu schreiben. Die Art und Weise, wie ich es gemacht habe, scheint strukturierter zu sein als das, was andere erlebt haben. Wenn jemand eine schlechte persönliche Erfahrung mit "Everything is a map" hat, spricht er darüber, wie schwierig es ist, diese zu pflegen, weil:
- Sie kennen nie die Struktur der Karte, die die Funktion benötigt
- Jede Funktion kann die Eingabe auf eine Weise verändern, die Sie nicht erwarten würden. Sie müssen die gesamte Codebasis durchsuchen, um herauszufinden, wie ein bestimmter Schlüssel in die Karte gelangt ist oder warum er verschwunden ist.
Für diejenigen mit einer solchen Erfahrung war die Codebasis vielleicht: "Alles braucht 1 von N Kartentypen." Meins ist: "Alles nimmt 1 von 1 Kartentyp". Wenn Sie die Struktur dieses 1-Typs kennen, kennen Sie die Struktur von allem. Natürlich wächst diese Struktur normalerweise mit der Zeit. Deshalb...
Es gibt einen Ort, an dem Sie nach der Referenzimplementierung suchen können (z. B. das Schema). Diese Referenzimplementierung ist Code, den das Spiel verwendet, damit er nicht veraltet ist.
Was den zweiten Punkt betrifft, füge ich der Karte keine Schlüssel außerhalb der Referenzimplementierung hinzu / entferne sie nicht, sondern mutiere nur das, was bereits vorhanden ist. Ich habe auch eine große Reihe von automatisierten Tests.
Wenn diese Architektur unter ihrem eigenen Gewicht zusammenbricht, füge ich ein zweites Update hinzu. Ansonsten gehe ich davon aus, dass alles gut läuft :)
quelle
Antworten:
Ich habe zuvor eine Anwendung unterstützt, bei der "alles eine Karte ist". Es ist eine schreckliche Idee. BITTE nicht machen!
Wenn Sie die Argumente angeben, die an die Funktion übergeben werden, können Sie auf einfache Weise ermitteln, welche Werte die Funktion benötigt. Es wird vermieden, dass fremde Daten an die Funktion übergeben werden, die den Programmierer nur ablenkt. Jeder übergebene Wert impliziert, dass er benötigt wird, und dass der Programmierer, der Ihren Code unterstützt, herausfinden muss, warum die Daten benötigt werden.
Wenn Sie dagegen alles als Karte übergeben, muss der Programmierer, der Ihre App unterstützt, die aufgerufene Funktion in jeder Hinsicht vollständig verstehen, um zu wissen, welche Werte die Karte enthalten muss. Schlimmer noch, es ist sehr verlockend, die Map, die an die aktuelle Funktion übergeben wurde, erneut zu verwenden, um Daten an die nächsten Funktionen weiterzuleiten. Dies bedeutet, dass der Programmierer, der Ihre App unterstützt, alle von der aktuellen Funktion aufgerufenen Funktionen kennen muss, um zu verstehen, was die aktuelle Funktion tut. Das ist genau das Gegenteil von dem Zweck, Funktionen zu schreiben - Probleme zu abstrahieren, damit Sie nicht über sie nachdenken müssen! Stellen Sie sich nun 5 Aufrufe tief und 5 Aufrufe breit vor. Das ist verdammt viel im Kopf zu behalten und verdammt viele Fehler zu machen.
"Alles ist eine Karte" scheint auch dazu zu führen, dass die Karte als Rückgabewert verwendet wird. Ich habe es gesehen. Und wieder ist es ein Schmerz. Die aufgerufenen Funktionen dürfen niemals den Rückgabewert des anderen überschreiben - es sei denn, Sie kennen die Funktionalität von allem und wissen, dass der eingegebene Zuordnungswert X für den nächsten Funktionsaufruf ersetzt werden muss. Und die aktuelle Funktion muss die Zuordnung ändern, um ihren Wert zurückzugeben, der manchmal den vorherigen Wert überschreiben muss und manchmal nicht.
edit - beispiel
Hier ist ein Beispiel, wo dies problematisch war. Dies war eine Webanwendung. Benutzereingaben wurden vom Benutzeroberflächen-Layer akzeptiert und in einer Karte platziert. Dann wurden Funktionen aufgerufen, um die Anforderung zu verarbeiten. Der erste Funktionssatz würde nach fehlerhaften Eingaben suchen. Wenn ein Fehler auftritt, wird die Fehlermeldung in die Karte eingefügt. Die aufrufende Funktion prüft die Map auf diesen Eintrag und schreibt den Wert in die Benutzeroberfläche, falls vorhanden.
Der nächste Funktionssatz würde die Geschäftslogik starten. Jede Funktion würde die Karte nehmen, einige Daten entfernen, einige Daten ändern, die Daten in der Karte bearbeiten und das Ergebnis in die Karte einfügen usw. Nachfolgende Funktionen würden Ergebnisse von früheren Funktionen in der Karte erwarten. Um einen Fehler in einer nachfolgenden Funktion zu beheben, mussten Sie alle vorherigen Funktionen sowie den Aufrufer untersuchen, um festzustellen, wo der erwartete Wert möglicherweise festgelegt wurde.
Die nächsten Funktionen würden Daten aus der Datenbank ziehen. Oder sie leiten die Karte an die Datenzugriffsebene weiter. Die DAL prüft, ob die Zuordnung bestimmte Werte enthält, um die Ausführung der Abfrage zu steuern. Wenn 'justcount' ein Schlüssel wäre, wäre die Abfrage 'count select foo from bar'. Jede der zuvor aufgerufenen Funktionen könnte diejenige sein, die der Karte 'justcount' hinzugefügt hat. Die Abfrageergebnisse werden derselben Karte hinzugefügt.
Die Ergebnisse würden dem Aufrufer (Geschäftslogik) in die Luft sprudeln, der auf der Karte nachschauen würde, was zu tun ist. Ein Teil davon stammte von Dingen, die durch die anfängliche Geschäftslogik zur Karte hinzugefügt wurden. Einige würden von den Daten aus der Datenbank stammen. Der einzige Weg, um herauszufinden, woher es kam, bestand darin, den Code zu finden, der es hinzugefügt hat. Und der andere Ort, der es auch hinzufügen kann.
Der Code war praktisch ein monolithisches Durcheinander, das man in seiner Gesamtheit verstehen musste, um zu wissen, woher ein einzelner Eintrag in der Karte kam.
quelle
gameState
ohne etwas darüber zu wissen, was davor oder danach passiert ist. Es reagiert einfach auf die angegebenen Daten. Wie sind Sie in eine Situation gekommen, in der die Funktionen sich gegenseitig auf die Zehen treten? Kannst du ein Beispiel geben?Persönlich würde ich dieses Muster in keinem der beiden Paradigmen empfehlen. Es macht es einfacher, anfangs zu schreiben, was das spätere Nachdenken erschwert.
Versuchen Sie beispielsweise, die folgenden Fragen zu jeder Unterprozessfunktion zu beantworten:
state
braucht es?Mit diesem Muster können Sie diese Fragen nicht beantworten, ohne die gesamte Funktion gelesen zu haben.
In einer objektorientierten Sprache ist das Muster noch weniger sinnvoll, da der Verfolgungsstatus das ist, was Objekte tun.
quelle
Was Sie anscheinend tun, ist im Grunde genommen eine manuelle Staatsmonade. Was ich tun würde, ist, einen (vereinfachten) Bindungskombinator zu erstellen und die Verbindungen zwischen Ihren logischen Schritten damit erneut auszudrücken:
Sie können sogar
stateBind
die verschiedenen Unterprozesse aus Unterprozessen erstellen und einen Baum von Bindungskombinatoren abarbeiten, um Ihre Berechnung entsprechend zu strukturieren.Eine Erklärung der vollständigen, nicht vereinfachten Staatsmonade und eine hervorragende Einführung in die Monaden im Allgemeinen in JavaScript finden Sie in diesem Blogbeitrag .
quelle
stateBind
mir angegebene Kombinator ist ein sehr einfaches Beispiel dafür.Es scheint also eine große Diskussion zwischen der Wirksamkeit dieses Ansatzes in Clojure zu geben. Ich denke, es könnte nützlich sein, sich Rich Hickeys Philosophie anzuschauen, warum er Clojure erstellt hat, um Datenabstraktionen auf diese Weise zu unterstützen :
Fogus wiederholt diese Punkte in seinem Buch Functional Javascript :
quelle
Der Anwalt des Teufels
Ich denke, diese Frage verdient den Anwalt eines Teufels (aber natürlich bin ich voreingenommen). Ich denke, @KarlBielefeldt macht sehr gute Punkte und ich möchte sie ansprechen. Zunächst möchte ich sagen, dass seine Punkte großartig sind.
Da er erwähnte, dass dies selbst in der funktionalen Programmierung kein gutes Muster ist, werde ich JavaScript und / oder Clojure in meinen Antworten berücksichtigen. Eine äußerst wichtige Ähnlichkeit zwischen diesen beiden Sprachen besteht darin, dass sie dynamisch typisiert werden. Ich wäre mit seinen Punkten einverstandener, wenn ich dies in einer statisch typisierten Sprache wie Java oder Haskell implementieren würde. Aber ich werde die Alternative zum "Everything is a Map" -Muster als ein traditionelles OOP-Design in JavaScript und nicht in einer statisch typisierten Sprache betrachten (ich hoffe, dass ich auf diese Weise kein Strawman-Argument einrichte, Lass es mich wissen, bitte).
Wie würden Sie diese Fragen in einer dynamisch getippten Sprache normalerweise beantworten? Der erste Parameter einer Funktion kann benannt werden
foo
, aber was ist das? Eine Anordnung? Ein Objekt? Ein Objekt von Arrays von Objekten? Wie findest du es heraus? Der einzige Weg, den ich kenne, ist zuIch denke nicht, dass das Muster "Alles ist eine Karte" hier einen Unterschied macht. Dies sind immer noch die einzigen Möglichkeiten, die mir bekannt sind, um diese Fragen zu beantworten.
Denken Sie auch daran, dass in JavaScript und den meisten wichtigen Programmiersprachen
function
jeder Status, auf den er zugreifen kann, erforderlich ist, geändert und ignoriert werden kann und die Signatur keinen Unterschied macht: Die Funktion / Methode kann etwas mit dem globalen Status oder mit einem Singleton tun. Unterschriften lügen oft .Ich versuche nicht, eine falsche Zweiteilung zwischen "Everything is a Map" und schlecht gestaltetem OO-Code herzustellen. Ich möchte nur darauf hinweisen, dass Signaturen mit weniger / feineren / grobkörnigeren Parametern nicht garantieren, dass Sie wissen, wie eine Funktion zu isolieren, einzurichten und aufzurufen ist.
Aber wenn Sie mir erlauben würden, diese falsche Zweiteilung zu verwenden: Verglichen mit dem Schreiben von JavaScript in der traditionellen OOP-Art, scheint "Everything is a Map" besser zu sein. In der traditionellen OOP-Art kann es sein, dass die Funktion den Status erfordert, ändert oder ignoriert, den Sie übergeben haben, oder den Status, den Sie nicht übergeben haben im.
In meinem Code ja. Siehe meinen zweiten Kommentar zur Antwort von @ Evicatos. Vielleicht liegt das nur daran, dass ich ein Spiel mache, kann ich nicht sagen. In einem Spiel, das 60x pro Sekunde aktualisiert, spielt es keine Rolle, ob
dead guys drop loot
danngood guys pick up loot
oder umgekehrt. Jede Funktion tut genau das, was sie tun soll, unabhängig von der Reihenfolge, in der sie ausgeführt wird. Die gleichen Daten werden nur bei unterschiedlichenupdate
Aufrufen eingespeist, wenn Sie den Auftrag tauschen. Wenn Siegood guys pick up loot
dann habendead guys drop loot
, werden die Guten die Beute im nächsten abholenupdate
und es ist keine große Sache. Ein Mensch wird den Unterschied nicht bemerken können.Zumindest war dies meine allgemeine Erfahrung. Ich fühle mich wirklich verwundbar, wenn ich das öffentlich zugebe. Vielleicht ist es eine sehr , sehr schlechte Sache, wenn man bedenkt, dass dies in Ordnung ist . Lassen Sie mich wissen, wenn ich hier einen schrecklichen Fehler gemacht habe. Aber wenn ja, ist es extrem einfach, die Funktionen neu zu ordnen, sodass die Reihenfolge
dead guys drop loot
danngood guys pick up loot
wieder stimmt. Es wird weniger Zeit als die Zeit dauern, die benötigt wird, um diesen Absatz zu schreiben: PVielleicht denkst du "Tote sollten zuerst Beute fallen lassen. Es wäre besser, wenn dein Code diese Reihenfolge durchsetzen würde". Aber warum sollten Feinde Beute fallen lassen müssen, bevor Sie Beute aufheben können? Für mich ergibt das keinen Sinn. Vielleicht wurde die Beute vor 100
updates
Jahren fallen gelassen . Es muss nicht überprüft werden, ob ein willkürlicher Bösewicht Beute einsammeln muss, die sich bereits auf dem Boden befindet. Deshalb denke ich, dass die Reihenfolge dieser Operationen völlig willkürlich ist.Es ist natürlich, entkoppelte Schritte mit diesem Muster zu schreiben, aber es ist schwierig, Ihre gekoppelten Schritte im traditionellen OOP zu bemerken. Wenn ich traditionelles OOP schreibe, besteht die natürliche, naive Art des Denkens darin, die
dead guys drop loot
Rückkehr zu einemLoot
Objekt zu machen, das ich in das OOP überführen mussgood guys pick up loot
. Ich wäre nicht in der Lage, diese Operationen neu zu ordnen, da die erste die Eingabe der zweiten zurückgibt.Objekte haben einen Status und es ist idiomatisch, den Status zu ändern, wodurch die Historie einfach verschwindet ... es sei denn, Sie schreiben manuell Code, um den Überblick zu behalten. In welcher Weise wird der Status "was sie tun" verfolgt?
Richtig, wie gesagt: "Es kommt selten vor, dass eine meiner Funktionen rein ist." Sie bearbeiten immer nur ihre Parameter, aber sie verändern ihre Parameter. Dies ist ein Kompromiss, den ich eingehen musste, als ich dieses Muster auf JavaScript anwendete.
quelle
parent
? Istrepetitions
und Array von Zahlen oder Zeichenfolgen oder spielt es keine Rolle? Oder sind Wiederholungen nur eine Zahl, um die Anzahl der Wiederholungen darzustellen, die ich möchte? Es gibt viele Apis, die nur ein Optionsobjekt nehmen . Die Welt ist ein besserer Ort, wenn Sie die Dinge richtig benennen, aber es garantiert nicht, dass Sie wissen, wie man die API verwendet, ohne dass Fragen gestellt werden.Ich habe festgestellt, dass mein Code in der Regel so aufgebaut ist:
Ich habe nicht versucht, diese Unterscheidung zu treffen, aber so endet sie oft in meinem Code. Ich denke nicht, dass die Verwendung eines Stils notwendigerweise den anderen negiert.
Die reinen Funktionen sind einfach zu Unit-Test. Die größeren mit Karten befassen sich mehr mit dem "Integrationstest", da sie dazu neigen, mehr bewegliche Teile einzubeziehen.
In Javascript ist es eine wichtige Aufgabe, die Meteor Match-Bibliothek für die Parametervalidierung zu verwenden. Es macht sehr deutlich, was die Funktion erwartet und kann mit Karten sehr sauber umgehen.
Zum Beispiel,
Weitere Informationen finden Sie unter http://docs.meteor.com/#match .
:: UPDATE ::
Stuart Sierras Videoaufnahme von Clojure / Wests "Clojure in the Large" geht ebenfalls auf dieses Thema ein. Wie das OP kontrolliert er Nebenwirkungen als Teil der Karte, so dass das Testen viel einfacher wird. Er hat auch einen Blog-Post , der seinen aktuellen Clojure-Workflow beschreibt, der relevant zu sein scheint.
quelle
Das Hauptargument, das mir gegen diese Praxis einfällt, ist, dass es sehr schwierig ist zu sagen, welche Daten eine Funktion tatsächlich benötigt.
Das bedeutet, dass zukünftige Programmierer in der Codebasis wissen müssen, wie die aufgerufene Funktion intern funktioniert - und alle verschachtelten Funktionsaufrufe -, um sie aufzurufen.
Je mehr ich darüber nachdenke, desto mehr riecht dein gameState-Objekt nach einem globalen. Wenn es so verwendet wird, warum sollte man es weitergeben?
quelle
gameState = f(gameState)
,f()
ist es viel schwieriger zu testenf
.f()
kann jedes Mal eine andere Sache zurückgeben, wenn ich es anrufe. Es ist jedoch einfach, dief(gameState)
Rückgabe jedes Mal zu ändern, wenn dieselbe Eingabe erfolgt.Es gibt passendere Namen für das, was Sie tun, als Big Ball of Mud . Was Sie tun, nennt man das Gott- Objektmuster. Auf den ersten Blick sieht es nicht so aus, aber in Javascript gibt es kaum einen Unterschied zwischen
und
Ob es eine gute Idee ist oder nicht, hängt wahrscheinlich von den Umständen ab. Aber es steht mit Sicherheit im Einklang mit dem Clojure-Weg. Eines der Ziele von Clojure ist es, das zu beseitigen, was Rich Hickey "zufällige Komplexität" nennt. Mehrere miteinander kommunizierende Objekte sind sicherlich komplexer als ein einzelnes Objekt. Wenn Sie die Funktionalität in mehrere Objekte aufteilen, müssen Sie sich plötzlich Gedanken über Kommunikation und Koordination machen und die Verantwortlichkeiten aufteilen. Dies sind Komplikationen, die nur im Zusammenhang mit Ihrem ursprünglichen Ziel stehen, ein Programm zu schreiben. Sie sollten das Gespräch von Rich Hickey sehen. Einfach leicht gemacht . Ich halte das für eine sehr gute Idee.
quelle
Ich habe mich heute mit diesem Thema beschäftigt, als ich mit einem neuen Projekt gespielt habe. Ich arbeite in Clojure, um ein Pokerspiel zu machen. Ich stellte Nennwerte und Farben als Schlüsselwörter dar und entschied mich, eine Karte wie eine Karte darzustellen
Ich hätte sie genauso gut zu Listen oder Vektoren der beiden Schlüsselwortelemente machen können. Ich weiß nicht, ob es einen Speicher- / Leistungsunterschied gibt, deshalb beschäftige ich mich vorerst nur mit Karten.
Für den Fall, dass ich es mir später anders überlege, habe ich beschlossen, dass die meisten Teile meines Programms eine "Schnittstelle" durchlaufen, um auf die Teile einer Karte zuzugreifen, damit die Implementierungsdetails kontrolliert und verborgen werden. Ich habe Funktionen
dass der Rest des Programms verwendet. Karten werden an Funktionen als Karten weitergegeben, aber die Funktionen verwenden eine vereinbarte Schnittstelle, um auf die Karten zuzugreifen, und sollten daher nicht in der Lage sein, Fehler zu machen.
In meinem Programm wird eine Karte wahrscheinlich immer nur eine Karte mit zwei Werten sein. In der Frage wird der gesamte Spielstatus als Karte herumgereicht. Der Spielstatus wird viel komplizierter sein als eine einzelne Karte, aber ich glaube nicht, dass es einen Fehler gibt, eine Karte zu verwenden. In einer objektimperativen Sprache könnte ich genauso gut ein einzelnes großes GameState-Objekt haben und dessen Methoden aufrufen und das gleiche Problem haben:
Jetzt ist es objektorientiert. Ist etwas besonders falsch daran? Ich glaube nicht, Sie delegieren nur Arbeit an Funktionen, die wissen, wie ein Statusobjekt behandelt wird. Und egal, ob Sie mit Karten oder Objekten arbeiten, Sie sollten vorsichtig sein, wenn Sie sie in kleinere Teile aufteilen. Ich sage also, dass die Verwendung von Karten vollkommen in Ordnung ist, solange Sie die gleiche Sorgfalt verwenden, die Sie für Objekte verwenden würden.
quelle
Nach allem, was ich (wenig) gesehen habe, ist die Verwendung von Maps oder anderen verschachtelten Strukturen, um ein einziges globales unveränderliches Statusobjekt wie dieses zu erstellen, in funktionalen Sprachen ziemlich verbreitet, zumindest in reinen, insbesondere wenn die Statusmonade als @ Ptharien'sFlame verwendet wird mentioend .
Zwei Hindernisse, um dies effektiv zu nutzen, die ich gesehen / gelesen habe (und andere hier erwähnte Antworten), sind:
Es gibt verschiedene Techniken / gängige Muster, mit denen diese Probleme behoben werden können:
Das erste ist Reißverschlüsse : Diese lassen einen Zustand tief in einer unveränderlichen verschachtelten Hierarchie überqueren und mutieren.
Eine andere Möglichkeit ist Objektive : Mit diesen können Sie sich auf eine bestimmte Stelle in der Struktur konzentrieren und den Wert dort lesen / ändern. Sie können verschiedene Objektive miteinander kombinieren, um sich auf verschiedene Dinge zu konzentrieren, ähnlich einer einstellbaren Eigenschaftskette in OOP (wo Sie die tatsächlichen Eigenschaftsnamen durch Variablen ersetzen können!)
Prismatic hat kürzlich einen Blogbeitrag über die Verwendung dieser Technik verfasst, unter anderem in JavaScript / ClojureScript, das Sie sich ansehen sollten. Sie verwenden Cursors (die sie mit Reißverschlüssen vergleichen), um den Fensterstatus für Funktionen zu bestimmen :
IIRC, sie berühren auch die Unveränderlichkeit in JavaScript in diesem Beitrag.
quelle
assoc-in
et al.). Ich schätze, ich hatte gerade Haskell im Gehirn. Ich frage mich, ob jemand einen JavaScript-Port davon gemacht hat? Die Leute haben es wahrscheinlich nicht erwähnt, weil sie (wie ich) das Gespräch nicht gesehen haben :)Ob dies eine gute Idee ist oder nicht, hängt davon ab, was Sie mit dem Zustand in diesen Unterprozessen tun. Wenn ich das Clojure-Beispiel richtig verstehe, handelt es sich bei den zurückgegebenen Statuswörterbüchern nicht um dieselben Statuswörterbücher, die übergeben werden. Es handelt sich um Kopien, möglicherweise mit Ergänzungen und Änderungen, die von Clojure (ich nehme an) effizient erstellt werden können, weil das Die funktionale Natur der Sprache hängt davon ab. Die ursprünglichen Statuswörterbücher für jede Funktion werden in keiner Weise geändert.
Wenn ich das richtig verstehe, ändern Sie die Statusobjekte, die Sie in Ihre Javascript-Funktionen übergeben, anstatt eine Kopie zurückzugeben, was bedeutet, dass Sie etwas sehr, sehr anderes tun als das, was dieser Clojure-Code tut. Wie Mike Partridge hervorhob, handelt es sich im Grunde genommen nur um eine globale Funktion, die Sie explizit ohne echten Grund übergeben und von Funktionen zurückgeben. An diesem Punkt denke ich, dass es Sie einfach denken lässt , dass Sie etwas tun, was Sie eigentlich nicht tun.
Wenn Sie explizit Kopien des Status erstellen, ihn ändern und dann diese geänderte Kopie zurückgeben, fahren Sie fort. Ich bin mir nicht sicher, ob dies der beste Weg ist, um das zu erreichen, was Sie in Javascript versuchen, aber es ist wahrscheinlich "eng" mit dem, was dieses Clojure-Beispiel tut.
quelle
update()
Funktion verschoben . Ich habe einen nach oben und einen nach unten bewegt. Alle meine Tests sind noch bestanden und als ich mein Spiel spielte, bemerkte ich keine negativen Auswirkungen. Ich habe das Gefühl, dass meine Funktionen genauso komponierbar sind wie das Clojure-Beispiel. Wir beide werfen unsere alten Daten nach jedem Schritt weg.(-> 10 (- 5) (/ 2))
gibt 2.5 zurück.(-> 10 (/ 2) (- 5))
gibt 0 zurück.Wenn Sie ein globales Zustandsobjekt haben, das manchmal als "Gottobjekt" bezeichnet wird und an jeden Prozess übergeben wird, können Sie eine Reihe von Faktoren verwechseln, die die Kopplung erhöhen und gleichzeitig den Zusammenhalt verringern. Diese Faktoren wirken sich alle negativ auf die langfristige Wartbarkeit aus.
Tramp Coupling Dies ergibt sich aus der Weitergabe von Daten über verschiedene Methoden, bei denen fast alle Daten nicht benötigt werden, um sie an den Ort zu bringen, an dem sie tatsächlich verarbeitet werden können. Diese Art der Kopplung ähnelt der Verwendung globaler Daten, kann jedoch enthaltener sein. Die Tramp-Kopplung ist das Gegenteil von "Need to Know", das verwendet wird, um Effekte zu lokalisieren und den Schaden einzudämmen, den ein fehlerhafter Code für das gesamte System haben kann.
Datennavigation Jeder Unterprozess in Ihrem Beispiel muss wissen, wie er genau zu den Daten kommt, die er benötigt, und er muss in der Lage sein, sie zu verarbeiten und möglicherweise ein neues globales Statusobjekt zu erstellen. Das ist die logische Konsequenz der Fremdkopplung; Der gesamte Kontext eines Datums ist erforderlich, um mit dem Datum arbeiten zu können. Auch hier ist nicht lokales Wissen eine schlechte Sache.
Wenn Sie einen "Reißverschluss", "Objektiv" oder "Cursor" eingegeben haben, wie in dem Beitrag von @paul beschrieben, ist das eine Sache. Sie würden den Zugriff einschränken und dem Reißverschluss usw. erlauben, das Lesen und Schreiben der Daten zu steuern.
Verletzung der Einzelverantwortung Die Behauptung, dass jeder von "Teilprozess eins", "Teilprozess zwei" und "Teilprozess drei" nur eine einzige Verantwortung trägt, nämlich ein neues globales Staatsobjekt mit den richtigen Werten zu produzieren, ist ein ungeheurer Reduktionismus. Es ist alles Bits am Ende, nicht wahr?
Mein Punkt hier ist, dass alle Hauptkomponenten Ihres Spiels die gleichen Verantwortlichkeiten haben, wie Ihr Spiel den Zweck der Delegierung und des Factorings besiegt.
Auswirkungen auf Systeme
Die Hauptauswirkung Ihres Designs liegt in der geringen Wartbarkeit. Die Tatsache, dass Sie das gesamte Spiel im Kopf behalten können, zeigt, dass Sie sehr wahrscheinlich ein ausgezeichneter Programmierer sind. Ich habe viele Dinge entworfen, die ich während des gesamten Projekts im Kopf behalten könnte. Das ist jedoch nicht der Punkt der Systemtechnik. Der Punkt ist , ein System zu machen , die für etwas mehr als eine Person arbeiten kann , kann in seinem Kopf halten zugleich .
Wenn Sie einen weiteren oder zwei oder acht Programmierer hinzufügen, wird Ihr System fast sofort auseinanderfallen.
Endlich
quelle