"Alles ist eine Karte", mache ich das richtig?


69

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:

  1. 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:
    1. Sie müssen sich keine Gedanken über die Reihenfolge der Argumente machen
    2. 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.
    3. Sie müssen kein Schema definieren
  2. 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:
    1. Performance
    2. Leichtigkeit der Durchsetzung
    3. 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.
  3. 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:

    1. Einfach zu testen
    2. Einfach, diese Funktionen isoliert zu betrachten
    3. Einfach eine Zeile davon auskommentieren und sehen, was das Ergebnis ist, indem ein einzelner Schritt entfernt wird
    4. 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.
    5. 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 gameStateObjekt 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:

  1. Sie kennen nie die Struktur der Karte, die die Funktion benötigt
  2. 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 :)


2
Coole Frage (+1)! Ich finde es eine sehr nützliche Übung, funktionale Redewendungen in einer nicht funktionalen (oder nicht sehr stark funktionalen) Sprache umzusetzen.
Giorgio

15
Jeder, der Ihnen mitteilt, dass das Verbergen von Informationen im OO-Stil (mit Eigenschaften und Accessorfunktionen) aufgrund der (in der Regel vernachlässigbaren) Leistung eine schlechte Sache ist, und Sie dann auffordert, alle Parameter in eine Karte umzuwandeln Der (viel größere) Overhead einer Hash-Suche bei jedem Versuch, einen Wert abzurufen, kann ignoriert werden.
Mason Wheeler

4
Mit @MasonWheeler haben Sie Recht. Wirst du jeden anderen Punkt, den er macht, für ungültig erklären, weil eine Sache falsch ist?
Daniel Kaplan

9
In Python (und ich glaube, die meisten dynamischen Sprachen, einschließlich Javascript) ist object sowieso nur Syntaxzucker für ein Diktat / eine Karte.
Lie Ryan

6
@EvanPlaice: Die Big-O-Notation kann täuschen. Die einfache Tatsache ist, dass alles im Vergleich zum direkten Zugriff mit zwei oder drei einzelnen Maschinencodeanweisungen langsam ist und sich bei etwas, das so oft wie ein Funktionsaufruf geschieht, der Overhead sehr schnell summiert.
Mason Wheeler

Antworten:


42

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.


2
Dein zweiter Absatz macht für mich Sinn und das klingt wirklich so, als wäre es beschissen. Ich verstehe aus Ihrem dritten Absatz, dass es sich nicht wirklich um dasselbe Design handelt. "Wiederverwendung" ist der Punkt. Es wäre falsch, das zu vermeiden. Und ich kann mich wirklich nicht auf deinen letzten Absatz beziehen. Ich muss jede Funktion übernehmen, gameStateohne 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?
Daniel Kaplan

2
Ich habe ein Beispiel hinzugefügt, um es ein wenig klarer zu machen. Ich hoffe es hilft. Außerdem gibt es einen Unterschied zwischen dem Umgehen eines gut definierten
Statusobjekts

28

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:

  • Welche Felder statebraucht es?
  • Welche Felder werden geändert?
  • Welche Felder sind unverändert?
  • Können Sie die Reihenfolge der Funktionen sicher ändern?

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.


2
"Die Vorteile der Unveränderlichkeit gehen weit zurück, je größer Ihre unveränderlichen Objekte werden." Warum? Ist das ein Kommentar zur Leistung oder Wartbarkeit? Bitte erläutern Sie diesen Satz.
Daniel Kaplan

8
@tieTYT Immutables funktionieren gut, wenn es etwas Kleines gibt (zum Beispiel einen numerischen Typ). Sie können sie kopieren, erstellen, verwerfen und mit relativ geringen Kosten fliegengewichtig machen. Wenn Sie anfangen, sich mit ganzen Spielzuständen zu befassen, die aus tiefen und großen Karten, Bäumen, Listen und Dutzenden, wenn nicht Hunderten von Variablen bestehen, steigen die Kosten für das Kopieren oder Löschen (und Fliegengewichte werden unpraktisch).

3
Aha. Handelt es sich um ein Problem mit "unveränderlichen Daten in imperitiven Sprachen" oder um ein Problem mit "unveränderlichen Daten"? IE: Vielleicht ist dies kein Problem in Clojure-Code. Aber ich kann sehen, wie es in JS ist. Es ist auch mühsam, den gesamten Code für das Boilerplate zu schreiben, um dies zu tun.
Daniel Kaplan

3
@MichaelT und Karl: Um fair zu sein, sollten Sie wirklich die andere Seite der Unveränderlichkeits- / Effizienzgeschichte erwähnen. Ja, naiver Gebrauch kann schrecklich ineffizient sein, deshalb haben sich die Leute bessere Ansätze ausgedacht. Weitere Informationen finden Sie in Chris Okasakis Arbeit.

3
@MattFenwick Ich persönlich mag immutables. Wenn ich mit Threading zu tun habe, weiß ich etwas über das Unveränderliche und kann es sicher bearbeiten und kopieren. Ich setze es in einen Parameteraufruf und gebe es an einen anderen weiter, ohne mir Sorgen zu machen, dass jemand es ändern wird, wenn es zu mir zurückkommt. Wenn man von einem komplexen Spielzustand spricht (die Frage verwendete dies als Beispiel - ich wäre entsetzt, wenn ich mir etwas so einfaches wie einen unveränderlichen Spielzustand vorstellen würde), ist Unveränderlichkeit wahrscheinlich der falsche Ansatz.

12

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:

function stateBind() {
    var computation = function (state) { return state; };
    for ( var i = 0 ; i < arguments.length ; i++ ) {
        var oldComp = computation;
        var newComp = arguments[i];
        computation = function (state) { return newComp(oldComp(state)); };
    }
    return computation;
}

...

stateBind(
  subprocessOne,
  subprocessTwo,
  subprocessThree,
);

Sie können sogar stateBinddie 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 .


1
OK, ich werde das untersuchen (und später kommentieren). Aber was halten Sie von der Idee, das Muster zu verwenden?
Daniel Kaplan

1
@tieTYT Ich denke, das Muster selbst ist eine sehr gute Idee. Die Staatsmonade ist im Allgemeinen ein nützliches Werkzeug zur Codestrukturierung für pseudoveränderbare Algorithmen (Algorithmen, die unveränderlich sind, aber die Veränderbarkeit emulieren).
Pthariens Flamme

2
+1 für die Feststellung, dass dieses Muster im Wesentlichen eine Monade ist. Ich bin jedoch anderer Meinung, dass es eine gute Idee in einer Sprache ist, die tatsächlich veränderlich ist. Monade ist eine Möglichkeit, die Fähigkeit von globalen / veränderlichen Zuständen in einer Sprache bereitzustellen, die keine Mutation zulässt. IMO, in einer Sprache, die keine Unveränderlichkeit erzwingt, ist das Monadenmuster nur mentale Masturbation.
Lie Ryan

6
@LieRyan Monaden haben im Allgemeinen überhaupt nichts mit Veränderlichkeit oder Globalität zu tun; nur die staatliche Monade tut dies ausdrücklich (denn genau dafür ist sie ausgelegt). Ich bin auch anderer Meinung, dass die Staatsmonade in einer Sprache mit Veränderlichkeit nicht nützlich ist, obwohl eine Implementierung, die sich auf die darunter liegende Veränderlichkeit stützt, effizienter sein könnte als die unveränderliche, die ich gegeben habe (obwohl ich mir da überhaupt nicht sicher bin). Die monadische Schnittstelle bietet Funktionen auf hohem Niveau, die ansonsten nicht leicht zugänglich sind. Der von stateBindmir angegebene Kombinator ist ein sehr einfaches Beispiel dafür.
Pthariens Flamme

1
@LieRyan I, zweiter Kommentar von Ptharien: Bei den meisten Monaden geht es nicht um Staat oder Veränderlichkeit, und selbst bei der einen geht es nicht speziell um den globalen Staat. Monaden funktionieren eigentlich ganz gut in OO / imperativen / veränderlichen Sprachen.

11

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: Wie kann Clojure bei der Lösung des vorliegenden Problems helfen, nachdem die Komplexität von Nebeneffekten verringert wurde? Zum Beispiel soll das idealisierte objektorientierte Paradigma die Wiederverwendung fördern, aber Clojure ist nicht klassisch objektorientiert - wie können wir unseren Code für die Wiederverwendung strukturieren?

Hickey: Ich würde über OO und Wiederverwendung streiten, aber die Möglichkeit, Dinge wiederzuverwenden, macht das Problem einfacher, da Sie Räder nicht neu erfinden, anstatt Autos zu bauen. Und Clojure, der Mitglied der JVM ist, stellt eine Vielzahl von Rädern - Bibliotheken - zur Verfügung. Was macht eine Bibliothek wiederverwendbar? Es sollte ein oder mehrere Dinge gut machen, relativ autark sein und wenig Anforderungen an den Client-Code stellen. Nichts davon fällt aus OO heraus, und nicht alle Java-Bibliotheken erfüllen diese Kriterien, aber viele tun dies.

Wenn wir zur Algorithmenebene zurückkehren, kann OO meiner Meinung nach die Wiederverwendung ernsthaft vereiteln. Insbesondere die Verwendung von Objekten zur Darstellung einfacher Informationsdaten ist bei der Erzeugung von Mikrosprachen pro Information, dh der Klassenmethode, beinahe strafbar, im Vergleich zu weitaus leistungsstärkeren deklarativen und generischen Methoden wie der relationalen Algebra. Das Erfinden einer Klasse mit einer eigenen Schnittstelle zum Speichern von Informationen ist wie das Erfinden einer neuen Sprache zum Schreiben jeder Kurzgeschichte. Dies ist eine Anti-Wiederverwendung und führt meiner Meinung nach in typischen OO-Anwendungen zu einer Explosion von Code. Clojure verzichtet darauf und plädiert stattdessen für ein einfaches assoziatives Informationsmodell. Mit ihm kann man Algorithmen schreiben, die über verschiedene Informationstypen hinweg wiederverwendet werden können.

Dieses assoziative Modell ist nur eine von mehreren mit Clojure gelieferten Abstraktionen, und dies sind die wahren Grundlagen seines Ansatzes zur Wiederverwendung: Funktionen auf Abstraktionen. Ein offener und großer Satz von Funktionen kann auf einem offenen und kleinen Satz erweiterbarer Abstraktionen ausgeführt werden. Dies ist der Schlüssel zur algorithmischen Wiederverwendung und zur Interoperabilität der Bibliothek. Die überwiegende Mehrheit der Clojure-Funktionen wird in Bezug auf diese Abstraktionen definiert, und Bibliotheksautoren entwerfen ihre Eingabe- und Ausgabeformate auch in Bezug auf diese, um eine enorme Interoperabilität zwischen unabhängig entwickelten Bibliotheken zu realisieren. Dies steht in krassem Gegensatz zu den DOMs und anderen solchen Dingen, die Sie in OO sehen. Natürlich können Sie in OO eine ähnliche Abstraktion mit Interfaces durchführen, beispielsweise mit den java.util-Auflistungen. Dies ist jedoch genauso einfach wie in java.io nicht möglich.

Fogus wiederholt diese Punkte in seinem Buch Functional Javascript :

In diesem Buch werde ich den Ansatz verfolgen, minimale Datentypen für die Darstellung von Abstraktionen zu verwenden, von Mengen über Bäume bis hin zu Tabellen. In JavaScript sind die Objekttypen zwar äußerst leistungsfähig, die dafür bereitgestellten Tools sind jedoch nicht vollständig funktionsfähig. Stattdessen besteht das größere Verwendungsmuster, das JavaScript-Objekten zugeordnet ist, darin, Methoden zum Zwecke des polymorphen Versands anzuhängen. Zum Glück können Sie auch ein unbenanntes (nicht über eine Konstruktorfunktion erstelltes) JavaScript-Objekt als einfachen assoziativen Datenspeicher anzeigen.

Wenn die einzigen Operationen, die wir für ein Book-Objekt oder eine Instanz eines Employee-Typs ausführen können, setTitle oder getSSN sind, haben wir unsere Daten in einzelne Informationsmikrosprachen gesperrt (Hickey 2011). Ein flexiblerer Ansatz zum Modellieren von Daten ist eine assoziative Datentechnik. JavaScript-Objekte sind auch ohne die Prototypen-Maschinerie ideale Vehikel für die assoziative Datenmodellierung, bei der benannte Werte strukturiert werden können, um übergeordnete Datenmodelle zu bilden, auf die auf einheitliche Weise zugegriffen werden kann.

Obwohl die Tools zum Bearbeiten von und Zugreifen auf JavaScript-Objekte als Datenzuordnungen in JavaScript selbst nur spärlich vorhanden sind, bietet Underscore zum Glück eine Reihe nützlicher Operationen. Zu den am einfachsten zu erfassenden Funktionen gehören _.keys, _.values ​​und _.pluck. Sowohl _.keys als auch _.values ​​werden entsprechend ihrer Funktionalität benannt. Dabei wird ein Objekt genommen und ein Array seiner Schlüssel oder Werte zurückgegeben ...


2
Ich habe dieses Fogus / Hickey-Interview schon einmal gelesen, aber ich konnte nicht verstehen, wovon er bis jetzt sprach. Danke für deine Antwort. Ich bin mir immer noch nicht sicher, ob Hickey / Fogus meinem Design den Segen geben würden. Ich bin besorgt, dass ich den Geist ihrer Ratschläge auf das Äußerste übertroffen habe.
Daniel Kaplan

9

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

Versuchen Sie beispielsweise, die folgenden Fragen zu jeder Unterprozessfunktion zu beantworten:

  • Welche Staatsgebiete braucht es?

  • Welche Felder werden geändert?

  • Welche Felder sind unverändert?

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 zu

  1. Lesen Sie die Dokumentation
  2. Schauen Sie sich den Funktionskörper an
  3. schau dir die tests an
  4. Rate und starte das Programm, um zu sehen, ob es funktioniert.

Ich 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 functionjeder 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.

  • Können Sie die Reihenfolge der Funktionen sicher ändern?

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 lootdann good guys pick up lootoder 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 unterschiedlichen updateAufrufen eingespeist, wenn Sie den Auftrag tauschen. Wenn Sie good guys pick up lootdann haben dead guys drop loot, werden die Guten die Beute im nächsten abholen updateund 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 lootdann good guys pick up lootwieder stimmt. Es wird weniger Zeit als die Zeit dauern, die benötigt wird, um diesen Absatz zu schreiben: P

Vielleicht 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 updatesJahren 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 lootRückkehr zu einem LootObjekt zu machen, das ich in das OOP überführen muss good 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.

In einer objektorientierten Sprache ist das Muster noch weniger sinnvoll, da der Verfolgungsstatus das ist, was Objekte tun.

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?

Auch die Vorteile der Unveränderlichkeit gehen weit zurück, je größer Ihre unveränderlichen Objekte werden.

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.


4
"Der erste Parameter einer Funktion kann foo heißen, aber was ist das?" Deshalb benennen Sie Ihre Parameter nicht "foo", sondern "repetitions", "parent" und andere Namen, die verdeutlichen, was in Kombination mit dem Funktionsnamen erwartet wird.
Sebastian Redl

1
Ich muss Ihnen in allen Punkten zustimmen. Das einzige Problem, das Javascript bei diesem Muster wirklich aufwirft, ist, dass Sie an veränderlichen Daten arbeiten und als solches sehr wahrscheinlich den Status ändern. Es gibt jedoch eine Bibliothek, die Ihnen Zugriff auf Clojure-Datenstrukturen in einfachem Javascript gibt, obwohl ich vergesse, wie es heißt. Das Übergeben von Argumenten als Objekt ist ebenfalls keine Seltenheit. Jquery führt dies an mehreren Stellen aus, dokumentiert jedoch, welche Teile des Objekts sie verwenden. Persönlich würde ich UI-Felder und GameLogic-Felder trennen, aber was auch immer für Sie funktioniert :)
Robin Heggelund Hansen

@SebastianRedl Wofür soll ich gehen parent? Ist repetitionsund 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.
Daniel Kaplan

8

Ich habe festgestellt, dass mein Code in der Regel so aufgebaut ist:

  • Funktionen, die Karten aufnehmen, sind in der Regel größer und haben Nebenwirkungen.
  • Funktionen, die Argumente annehmen, sind in der Regel kleiner und rein.

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,

function foo (post) {
  check(post, {
    text: String,
    timestamp: Date,
    // Optional, but if present must be an array of strings
    tags: Match.Optional([String])
    });

  // do stuff
}

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.


1
Ich denke, meine Kommentare zu @Evicatos werden meine Position hier näher erläutern. Ja, ich mutiere und die Funktionen sind nicht rein. Aber meine Funktionen sind wirklich einfach zu testen, insbesondere im Nachhinein auf Regressionsfehler, für die ich keine Tests geplant habe. Die Hälfte des Kredits geht an JS: Es ist sehr einfach, eine "Karte" / ein Objekt nur mit den Daten zu erstellen, die ich für meinen Test benötige. Dann ist es so einfach wie das Weitergeben und Überprüfen der Mutationen. Nebenwirkungen werden immer in der Karte dargestellt, sodass sie leicht zu testen sind.
Daniel Kaplan

1
Ich glaube, dass die pragmatische Anwendung beider Methoden der "richtige" Weg ist. Wenn das Testen für Sie einfach ist und Sie das Metaproblem der Weitergabe erforderlicher Felder an andere Entwickler verringern können, klingt dies nach einem Gewinn. Danke, für ihre Frage; Es hat mir Spaß gemacht, die interessante Diskussion zu lesen, die Sie begonnen haben.
Alaning

5

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?


1
Ja, da ich es normalerweise mutiere, IST es ein globales. Warum sich die Mühe machen, es herumzugeben? Ich weiß nicht, das ist eine berechtigte Frage. Aber mein Bauch sagt mir, wenn ich aufhören würde, mein Programm zu bestehen, würde es augenblicklich schwieriger werden, darüber nachzudenken. Jede Funktion könnte dem globalen Staat entweder alles oder nichts antun . So wie es jetzt ist, sehen Sie dieses Potenzial in der Funktionssignatur. Wenn Sie nicht sagen können, ich bin nicht zuversichtlich, etwas davon :)
Daniel Kaplan

1
BTW: re: das Hauptargument dagegen: Das scheint zu stimmen, ob dies in Clojure oder Javascript war. Aber es ist ein wertvoller Punkt. Vielleicht überwiegen die aufgeführten Vorteile bei weitem die negativen.
Daniel Kaplan

2
Ich weiß jetzt, warum ich es herumgereicht habe, obwohl es eine globale Variable ist: Es erlaubt mir, reine Funktionen zu schreiben. Wenn ich auf ändere gameState = f(gameState), f()ist es viel schwieriger zu testen f. f()kann jedes Mal eine andere Sache zurückgeben, wenn ich es anrufe. Es ist jedoch einfach, die f(gameState)Rückgabe jedes Mal zu ändern, wenn dieselbe Eingabe erfolgt.
Daniel Kaplan

3

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

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

und

{
  ...
  handleUnitCollision: function() {
    ...
  },
  ...
  handleLoot: function() {
    ...
  },
  ...
  update: function() {
    ...
    this.handleUnitCollision()
    ...
    this.handleLoot()
    ...
  },
  ...
};

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.


Verwandte, allgemeinere Frage [ programmers.stackexchange.com/questions/260309/… Datenmodellierung im
Vergleich zu

"In der objektorientierten Programmierung ist ein Gottobjekt ein Objekt, das zu viel weiß oder zu viel tut. Das Gottobjekt ist ein Beispiel für ein Anti-Muster." Ein Gott-Objekt ist also keine gute Sache, aber Ihre Botschaft scheint das Gegenteil zu sagen. Das ist ein bisschen verwirrend für mich.
Daniel Kaplan

@ tieTYT Sie tun nicht objektorientierte Programmierung, so ist es in
Ordnung

Wie sind Sie zu diesem Schluss gekommen (das "es ist in Ordnung")?
Daniel Kaplan

Das Problem mit dem Objekt "Gott" in OO ist, dass "das Objekt sich so bewusst wird, dass alle Objekte so abhängig vom Objekt" Gott "werden, dass es zu einem echten Albtraum wird, wenn eine Änderung oder ein Fehler behoben werden muss." source In Ihrem Code gibt es neben dem Gott-Objekt noch ein anderes Objekt, sodass der zweite Teil kein Problem darstellt. In Bezug auf den ersten Teil, euer Gott , Objekt ist das Programm und das Programm sollte von allem bewusst sein. Das ist also auch in Ordnung.
User7610

2

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

{ :face :queen :suit :hearts }

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

(defn face [card] (card :face))
(defn suit [card] (card :suit))

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:

class State
  def complex-process()
    state = clone(this) ; or just use 'this' below if mutation is fine
    state.subprocess-one()
    state.subprocess-two()
    state.subprocess-three()
    return state

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.


2

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:

  • Mutieren eines (tief) verschachtelten Wertes im (unveränderlichen) Zustand
  • Versteckt einen Großteil des Staates vor Funktionen, die es nicht brauchen, und gibt ihnen nur das bisschen, an dem sie arbeiten müssen / mutieren

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 :

Om stellt die Kapselung und Modularität mithilfe von Cursorn wieder her. Cursor bieten aktualisierbare Fenster für bestimmte Teile des Anwendungsstatus (ähnlich wie Reißverschlüsse), sodass Komponenten nur auf die relevanten Teile des globalen Status verweisen und diese kontextfrei aktualisieren können.

IIRC, sie berühren auch die Unveränderlichkeit in JavaScript in diesem Beitrag.


Das erwähnte Talk-OP beschreibt auch die Verwendung der Update-In- Funktion, um den Bereich zu begrenzen, in dem eine Funktion auf einen Teilbaum der Zustandskarte aktualisieren kann. Ich glaube, das hat noch niemand angesprochen.
User7610

@ user7610 Guter Fang, ich kann nicht glauben, dass ich vergessen habe, dieses zu erwähnen - ich mag diese Funktion (und assoc-inet 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 :)
paul

@paul in gewisser Weise, weil es in ClojureScript verfügbar ist, aber ich bin mir nicht sicher, ob das in Ihrem Kopf "zählt". Es könnte in PureScript existieren und ich glaube, es gibt mindestens eine Bibliothek, die unveränderliche Datenstrukturen in JavaScript bereitstellt. Ich hoffe, mindestens einer von ihnen hat diese, sonst wären sie unpraktisch in der Anwendung.
Daniel Kaplan

@tieTYT Als ich diesen Kommentar machte, dachte ich an eine native JS-Implementierung, aber Sie machen einen guten Eindruck über ClojureScript / PureScript. Ich sollte in unveränderliche JS schauen und sehen, was da draußen ist. Ich habe vorher noch nie daran gearbeitet.
Paul

1

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.


1
Ist es am Ende wirklich "sehr, sehr anders"? Im Clojure-Beispiel überschreibt er seinen alten Zustand mit dem neuen Zustand. Ja, es gibt keine wirkliche Mutation, die Identität ändert sich nur. Aber in seinem "guten" Beispiel, wie geschrieben, hat er keine Möglichkeit, die Kopie zu erhalten, die in den zweiten Teilprozess übergeben wurde. Die Identifikation dieses Wertes wurde überschrieben. Daher denke ich, dass die Sache, die "sehr, sehr anders" ist, wirklich nur ein Detail der Sprachimplementierung ist. Zumindest im Kontext dessen, was Sie ansprechen.
Daniel Kaplan

2
Mit dem Clojure-Beispiel gehen zwei Dinge vor sich: 1) Das erste Beispiel hängt davon ab, welche Funktionen in einer bestimmten Reihenfolge aufgerufen werden, und 2) die Funktionen sind rein, sodass sie keine Nebenwirkungen haben. Da die Funktionen im zweiten Beispiel rein sind und dieselbe Signatur haben, können Sie sie neu anordnen, ohne sich um versteckte Abhängigkeiten von der Reihenfolge kümmern zu müssen, in der sie aufgerufen werden. Wenn Sie den Status in Ihren Funktionen ändern, haben Sie nicht die gleiche Garantie. Eine Statusmutation bedeutet, dass Ihre Version nicht so zusammensetzbar ist, wie es der ursprüngliche Grund für das Wörterbuch war.
Evicatos

1
Sie werden mir ein Beispiel zeigen müssen, weil ich nach meiner Erfahrung die Dinge nach Belieben bewegen kann und es sehr wenig Wirkung hat. Um es mir selbst zu beweisen, habe ich zwei zufällige Unterprozessaufrufe in der Mitte meiner 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.
Daniel Kaplan

1
Wenn Sie Ihre Tests bestehen und keine negativen Auswirkungen bemerken, mutieren Sie derzeit keinen Zustand, der an anderen Stellen unerwartete Nebenwirkungen hat. Da Ihre Funktionen nicht rein sind, können Sie nicht garantieren, dass dies immer der Fall ist. Ich denke, ich muss etwas an Ihrer Implementierung grundlegend missverstehen, wenn Sie sagen, dass Ihre Funktionen nicht rein sind, sondern Sie Ihre alten Daten nach jedem Schritt wegwerfen.
Evicatos

1
@Evicatos - Rein zu sein und die gleiche Signatur zu haben, bedeutet nicht, dass die Reihenfolge der Funktionen keine Rolle spielt. Stellen Sie sich vor, Sie berechnen einen Preis mit festen und prozentualen Rabatten. (-> 10 (- 5) (/ 2))gibt 2.5 zurück. (-> 10 (/ 2) (- 5))gibt 0 zurück.
Zak

1

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.

  1. Die Lernkurve für Gottobjekte ist flach (dh es dauert sehr lange, bis man in ihnen kompetent ist). Jeder zusätzliche Programmierer muss alles lernen, was Sie wissen, und es im Kopf behalten. Sie werden immer nur Programmierer einstellen können, die besser sind als Sie, vorausgesetzt, Sie können sie genug bezahlen, um durch die Aufrechterhaltung eines massiven Gottesobjekts zu leiden.
  2. Die Testbereitstellung ist in Ihrer Beschreibung nur eine weiße Box. Sie müssen jedes Detail des Gott-Objekts sowie des zu testenden Moduls kennen, um einen Test einzurichten, auszuführen und festzustellen, dass a) es das Richtige getan hat und b) es keines getan hat von 10.000 falschen Dingen. Die Chancen sind stark gegen Sie gestapelt.
  3. Das Hinzufügen eines neuen Features erfordert, dass Sie a) jeden Unterprozess durchlaufen und feststellen, ob sich das Feature auf einen Code in ihm auswirkt und umgekehrt, b) Ihren globalen Status durchlaufen und die Ergänzungen entwerfen und c) jeden Komponententest durchlaufen und ihn ändern um zu überprüfen, ob sich kein Prüfling nachteilig auf die neue Funktion auswirkt .

Endlich

  1. Veränderbare Gottobjekte waren der Fluch meiner Programmier-Existenz, einige von mir selbst und andere, in denen ich gefangen war.
  2. Die Staatsmonade skaliert nicht. Der Zustand wächst exponentiell, mit allen Auswirkungen auf Tests und Operationen, die dies impliziert. Wir kontrollieren den Staat in modernen Systemen durch Delegation (Aufteilung der Zuständigkeiten) und Scoping (Beschränkung des Zugriffs auf nur eine Teilmenge des Staates). Der Ansatz "Alles ist eine Landkarte" ist das genaue Gegenteil des kontrollierenden Staates.
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.