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 :)