Wie vermeide ich, dass Javascript zu Spaghetti-Code wird?

8

Ich habe im Laufe der Jahre ziemlich viel Javascript gemacht und verwende einen objektorientierteren Ansatz, insbesondere mit dem Modulmuster. Welchen Ansatz verwenden Sie, um eine größere Codebasis zu vermeiden, um Spaghetti zu werden? Gibt es einen "besten Ansatz"?

Marko
quelle
1
Wie würden Sie wissen, ob Sie lesbaren und leicht zu wartenden Code geschrieben haben? - Ihr Peer teilt Ihnen dies nach Überprüfung des Codes mit.
Mücke
Aber wenn ein Entwickler denkt, dass nach 3 Codezeilen etwas kompliziert wird? Was ist dann zu tun?
Marko
2
Sie können dies nicht selbst feststellen, da Sie als Autor mehr wissen, als der Code selbst sagt. Ein Computer kann Ihnen aus den gleichen Gründen nicht sagen, ob ein Gemälde Kunst ist oder nicht. Daher benötigen Sie einen anderen Menschen, der in der Lage ist, die Software zu warten, um zu sehen, was Sie geschrieben haben, und um seine Meinung zu äußern. Der formale Name des Prozesses ist "Peer Review" ( Zitat Quelle - Top-Voted-Antwort auf eine Frage, die mit Ihrer identisch ist)
Mücke
Das wäre großartig, wenn wir bei der Arbeit wären.
Marko
Kontinuierliches Refactoring. Es werden immer schlechte Entscheidungen getroffen. Sie werden zu technischen Schulden, sobald Sie aufhören, sich um sie zu kümmern.
Pithikos vor

Antworten:

7

Dan Wahlin gibt spezifische Anleitungen zur Vermeidung von Funktions-Spaghetti-Code in JavaScript.

Die meisten Leute (einschließlich ich) beginnen damit, JavaScript-Code zu schreiben, indem sie Funktion für Funktion in eine .js- oder HTML-Datei einfügen. Während an diesem Ansatz sicherlich nichts auszusetzen ist, da er die Arbeit erledigt, kann er bei der Arbeit mit viel Code schnell außer Kontrolle geraten. Wenn Funktionen in eine Datei zusammengefasst werden, kann es schwierig sein, Code zu finden. Das Refactoring von Code ist eine große Aufgabe (es sei denn, Sie haben ein nettes Tool wie Resharper 6.0), der variable Bereich kann zu einem Problem werden, und die Wartung des Codes kann ein Albtraum sein, insbesondere wenn du hast es ursprünglich nicht geschrieben.

Er beschreibt einige JavaScript-Entwurfsmuster , die dem Code Struktur verleihen.

Ich bevorzuge das aufschlussreiche Prototypmuster . Mein zweiter Favorit ist das aufschlussreiche Modulmuster , das sich geringfügig vom Standardmodulmuster darin unterscheidet, dass Sie den öffentlichen / privaten Bereich deklarieren können.

Jim G.
quelle
Ja, es ist eine Art, was ich mache, wenn ich größere Sachen baue.
Marko
1
Aber wo ich das benannte Muster zum ersten Mal gesehen habe, muss es in Javascript Patterns gewesen sein - ( amazon.com/JavaScript-Patterns-Stoyan-Stefanov/dp/0596806752/… ).
Marko
Ich hasse es, der Typ zu sein, der sagt "benutze einfach ein Framework" ... aber für mich denke ich immer an den Typ, der nach mir kommt, und um ein gut getestetes, dokumentiertes und von der Community unterstütztes Framework zu haben ... Es ist ein Kinderspiel für mich. Mein Favorit ist Javascript MVC (bald CanJS). Es verfügt über Gerüstskripte und einen sehr intuitiven Abhängigkeitsmanager ( steal()), mit dem Sie eine sehr gut strukturierte Anwendung erstellen können, während Sie Skripte bis zu 1 oder 2 minimierten Dateien kompilieren.
Ryan Wheale
4

Wenn Sie OOP in einer modernen Sprache verwenden, besteht die größte Gefahr normalerweise nicht in " Spaghetti-Code ", sondern in " Ravioli-Code"". Sie können am Ende Probleme aufteilen und überwinden, bis zu dem Punkt, an dem Ihre Codebasis aus kleinen Teilen, winzigen Funktionen und Objekten besteht, die alle lose miteinander verbunden sind und einzelne, aber winzige Aufgaben erfüllen, die alle gegen Komponententests getestet wurden, mit einem Spinnennetz abstrakter Interaktionen Wenn Sie so weitermachen, ist es so schwierig, darüber nachzudenken, was in Bezug auf Nebenwirkungen vor sich geht. Und es ist leicht zu glauben, dass Sie dies wunderschön konstruiert haben, da die einzelnen Teile in der Tat wunderschön sein könnten und alle an SOLID haften, während Sie immer noch Ihre finden Gehirn kurz vor der Explosion aus der Komplexität aller Interaktionen, wenn versucht wird, das System vollständig zu verstehen.

Und während es sehr einfach ist, darüber nachzudenken, was eines dieser Objekte oder Funktionen einzeln tut, da sie eine so einzigartige und einfache und vielleicht sogar schöne Verantwortung übernehmen, während sie zumindest ihre abstrakten Abhängigkeiten durch DI ausdrücken, besteht das Problem darin, wann Sie wollen Um das Gesamtbild zu analysieren, ist es schwer herauszufinden, was tausend winzige Dinge mit einem Spinnennetz von Interaktionen letztendlich bewirken. Natürlich sagen die Leute, schauen Sie sich nur die großen Objekte und Funktionen an, die dokumentiert sind, und gehen Sie nicht auf die kleinen ein, und das hilft natürlich, zumindest zu verstehen, was auf einer hochrangigen Art und Weise passieren soll. ..

Dies hilft jedoch nicht so viel, wenn Sie den Code tatsächlich ändern oder debuggen müssen. An diesem Punkt müssen Sie in der Lage sein, herauszufinden, was all diese Dinge für Ideen auf niedrigerer Ebene wie Nebenwirkungen und bewirken Anhaltende Statusänderungen und Pflege systemweiter Invarianten. Und es ist ziemlich schwierig, die Nebenwirkungen zusammenzufassen, die zwischen den Interaktionen von Tausenden von kleinen Dingen auftreten, unabhängig davon, ob sie abstrakte Schnittstellen verwenden, um miteinander zu kommunizieren oder nicht.

ECS

Das Letzte, was ich gefunden habe, um dieses Problem zu mindern, sind Entity-Component-Systeme, aber das könnte für viele Projekte übertrieben sein. Ich habe mich bis zu dem Punkt in ECS verliebt, an dem ich jetzt, selbst wenn ich kleine Projekte schreibe, meine ECS-Engine verwende (obwohl dieses kleine Projekt möglicherweise nur ein oder zwei Systeme hat). Für Leute, die nicht an ECS interessiert sind, habe ich versucht herauszufinden, warum ECS die Fähigkeit, das System so gut zu verstehen, so vereinfacht hat, und ich denke, ich bin auf einige Dinge fixiert, die für viele Projekte anwendbar sein sollten, auch wenn sie dies nicht tun Verwenden Sie eine ECS-Architektur.

Homogene Schleifen

Ein grundlegender Anfang besteht darin, homogenere Schleifen zu bevorzugen, was tendenziell mehr Durchgänge über dieselben Daten, aber gleichmäßigere Durchgänge impliziert. Zum Beispiel, anstatt dies zu tun:

for each entity:
    apply physics to entity
    apply AI to entity
    apply animation to entity
    update entity textures
    render entity

... es scheint irgendwie so viel zu helfen, wenn Sie dies stattdessen tun:

for each entity:
    apply physics to entity

for each entity:
    apply AI to entity

etc.

Und das mag verschwenderisch erscheinen, wenn Sie dieselben Daten mehrmals durchlaufen, aber jetzt ist jeder Durchgang sehr homogen. Es erlaubt Ihnen zu denken: "In dieser Phase des Systems ist mit diesen Objekten nichts los außer der Physik. Wenn sich Dinge ändern und Nebenwirkungen auftreten, werden sie alle auf sehr einheitliche Weise geändert. "" Und irgendwie finde ich, dass das hilft, über die Codebasis so viel nachzudenken.

Es scheint zwar verschwenderisch zu sein, kann Ihnen aber auch dabei helfen, mehr Möglichkeiten zur Parallelisierung des Codes zu finden, wenn einheitliche Aufgaben auf alles in jeder Schleife angewendet werden. Und es neigt auch dazu, zu ermutigenein größerer Grad an Entkopplung. Nur wenn Sie diese geschiedenen Durchgänge haben, die nicht versuchen, alles mit einem Objekt in einem Durchgang zu tun, finden Sie von Natur aus mehr Möglichkeiten, den Code einfach zu entkoppeln und entkoppelt zu halten. In ECS sind Systeme häufig vollständig voneinander entkoppelt, und es gibt keine äußere "Klasse" oder "Funktion", die sie manuell miteinander koordiniert. Das ECS erleidet auch nicht notwendigerweise wiederholte Cache-Fehler, da es nicht notwendigerweise dieselben Daten mehrmals wiederholt (jede Schleife kann auf verschiedene Komponenten zugreifen, die sich vollständig an einer anderen Stelle im Speicher befinden, aber denselben Entitäten zugeordnet sind). Die Systeme müssen nicht manuell koordiniert werden, da sie autonom sind und für die Schleife selbst verantwortlich sind. Sie benötigen lediglich Zugriff auf dieselben zentralen Daten.

Dies ist also eine Möglichkeit, um loszulegen und einen einheitlicheren und einfacheren Kontrollfluss über Ihr System zu erreichen.

Abflachen der Ereignisbehandlung

Eine andere Möglichkeit besteht darin, die Abhängigkeit von der Ereignisbehandlung zu verringern. Die Ereignisbehandlung ist häufig erforderlich, um externe Ereignisse zu ermitteln, die ohne Abfrage aufgetreten sind. Oft gibt es jedoch Möglichkeiten, kaskadierende Push-Ereignisse zu vermeiden, die zu sehr schwer vorhersehbaren Kontrollabläufen und Nebenwirkungen führen. Die Ereignisbehandlung befasst sich von Natur aus mit komplexen Dingen, die jeweils mit einem winzigen Objekt geschehen, wenn wir uns auf einfache und einheitliche Dinge konzentrieren möchten, die mit vielen Objekten gleichzeitig geschehen.

Anstelle eines Ereignisses zur Größenänderung des Betriebssystems, bei dem die Größe eines übergeordneten Steuerelements geändert wird, werden dann die Größenänderungs- und Malereignisse für jedes untergeordnete Element verschoben, wodurch möglicherweise mehr Ereignisse an wer-weiß-wo kaskadiert werden. Sie können nur Größenänderungsereignisse auslösen und das übergeordnete Element und die untergeordneten Elemente markieren als dirtyund müssen neu gestrichen werden. Sie können sogar einfach festlegen, dass alle Steuerelemente in der Größe geändert werden müssen. An diesem Punkt kann ein LayoutSystemGerät diese Größe übernehmen und die Größe ändern und Größenänderungsereignisse für alle relevanten Steuerelemente auslösen.

Dann wird Ihr GUI-Rendering-System möglicherweise mit einer Bedingungsvariablen aufgeweckt und durchläuft die fehlerhaften Steuerelemente und malt sie mit einem breiten Durchlauf (keine Ereigniswarteschlange) neu. Dieser gesamte Durchgang konzentriert sich auf nichts anderes als das Zeichnen einer Benutzeroberfläche. Wenn es eine hierarchische Ordnungsabhängigkeit für das Neulackieren gibt, ermitteln Sie die verschmutzten Regionen oder Rechtecke und zeichnen Sie alles in diesen Regionen in der richtigen Z-Reihenfolge neu, sodass Sie keine Baumdurchquerung durchführen müssen und die Daten einfach in einer sehr durchlaufen können einfache und "flache" Mode, keine rekursive und "tiefe" Mode.

Es scheint ein so subtiler Unterschied zu sein, aber ich finde dies aus irgendeinem Grund vom Standpunkt des Kontrollflusses aus recht hilfreich. Es geht wirklich darum, die Anzahl der Dinge zu reduzieren, die einzelnen Objekten gleichzeitig passieren, und zu versuchen, etwas Ähnliches wie SRP anzustreben, das jedoch in Bezug auf Schleifen und Nebenwirkungen angewendet wird: das " Single-Task-Loop-Prinzip ", " The Single Type of Side" Effekt pro Schleife Prinzip ".

Mit dieser Art von Kontrollfluss können Sie das System mehr in Bezug auf große, kräftige, aber äußerst einheitliche Aufgaben betrachten, die in Schleifen ausgeführt werden, und nicht in Bezug auf alle Funktionen und Nebenwirkungen, die mit einem einzelnen Objekt gleichzeitig auftreten können. So sehr dies auch nicht so aussehen mag, als würde es einen großen Unterschied machen, ich stellte fest, dass es den Unterschied in der Welt ausmacht, zumindest was die Fähigkeit meines eigenen Verstandes betrifft, das Verhalten der Codebasis in allen Bereichen zu verstehen, die bei Änderungen von Bedeutung sind oder Debugging (was ich auch mit diesem Ansatz viel weniger zu tun hatte).

Abhängigkeiten fließen in Richtung Daten

Dies ist wahrscheinlich der umstrittenste Teil von ECS und kann für einige Bereiche sogar katastrophal sein. Es verstößt direkt gegen das Prinzip der Abhängigkeitsinversion von SOLID, das besagt, dass Abhängigkeiten auch für Module auf niedriger Ebene in Richtung Abstraktionen fließen sollten. Es verstößt auch gegen das Verstecken von Informationen, aber zumindest für ECS nicht annähernd so viel, wie es scheint, da normalerweise nur ein oder zwei Systeme auf die Daten einer bestimmten Komponente zugreifen.

Und ich denke, die Idee von Abhängigkeiten, die in Richtung Abstraktionen fließen, funktioniert wunderbar, wenn Ihre Abstraktionen stabil sind (wie in, unveränderlich). Abhängigkeiten sollten in Richtung Stabilität fließen . Zumindest meiner Erfahrung nach waren Abstraktionen jedoch oft nicht stabil. Entwickler würden sie nie ganz richtig verstehen und müssten Funktionen ändern oder entfernen (das Hinzufügen war nicht schlecht) und einige Schnittstellen ein oder zwei Jahre später verwerfen. Kunden würden ihre Meinung so ändern, dass sie die sorgfältigen Konzepte der Entwickler brechen und die abstrakte Fabrik für das abstrakte Haus der abstrakten Karten zum Erliegen bringen.

Inzwischen finde ich Daten weitaus stabiler. Welche Daten benötigt beispielsweise eine Bewegungskomponente in einem Spiel? Die Antwort ist ganz einfach. Es benötigt eine Art 4x4-Transformationsmatrix und einen Verweis / Zeiger auf ein übergeordnetes Element, damit Bewegungshierarchien erstellt werden können. Das ist es. Diese Entwurfsentscheidung könnte die Lebensdauer der gesamten Software dauern.

Es mag einige Feinheiten geben, z. B. ob wir Gleitkommazahlen mit einfacher oder doppelter Genauigkeit für die Matrix verwenden sollen, aber beide sind anständige Entscheidungen. Wenn SPFP verwendet wird, ist Präzision eine Herausforderung. Wenn DPFP verwendet wird, ist Geschwindigkeit eine Herausforderung, aber beide sind gute Entscheidungen, die nicht geändert oder notwendigerweise hinter einer Schnittstelle versteckt werden müssen. Jede Repräsentation ist eine, zu der wir uns verpflichten und die wir stabil halten können.

Was sind jedoch alle Funktionen, die für eine abstrakte IMotionSchnittstelle erforderlich sind , und was noch wichtiger ist, welche idealen Mindestfunktionen sollten bereitgestellt werden, um die Anforderungen aller Subsysteme, die sich jemals mit Bewegung befassen, effektiv zu erfüllen? Das ist so viel schwieriger zu beantworten, ohne vorher so viel mehr über die gesamten Designanforderungen der Anwendung zu verstehen. Wenn also so viele Teile der Codebasis davon abhängen IMotion, müssen wir möglicherweise bei jeder Entwurfsiteration so viel neu schreiben, es sei denn, wir können dies beim ersten Mal richtig machen.

In einigen Fällen kann die Datendarstellung natürlich sehr instabil sein. Möglicherweise hängt etwas von einer komplexen Datenstruktur ab, die aufgrund von Unzulänglichkeiten in der Datenstruktur möglicherweise in Zukunft ersetzt werden muss, während die funktionalen Anforderungen des mit der Datenstruktur verbundenen Systems im Voraus leicht vorausgesehen werden können. Es lohnt sich also, pragmatisch zu sein und von Fall zu Fall zu entscheiden, ob Abhängigkeiten in Richtung Abstraktionen oder Daten fließen, aber manchmal sind die Daten zumindest leichter zu stabilisieren als Abstraktionen, und ich habe ECS erst angenommen Es wurde sogar erwogen, Abhängigkeiten vorwiegend in Richtung Daten zu fließen (mit erstaunlich vereinfachenden und stabilisierenden Effekten).

Obwohl dies seltsam erscheinen mag, schlage ich in solchen Fällen, in denen es viel einfacher ist, ein stabiles Design für Daten über eine abstrakte Schnittstelle zu erstellen, vor, die Abhängigkeiten auf einfache alte Daten zu lenken. Dies erspart Ihnen möglicherweise viele wiederholte Iterationen von Umschreibungen. In Bezug auf Kontrollflüsse und Spaghetti und Ravioli-Code vereinfacht dies jedoch tendenziell auch Ihre Kontrollflüsse, wenn Sie keine so komplexen Interaktionen haben müssen, bevor Sie schließlich zu den relevanten Daten gelangen.


quelle
1
Ich habe diese Antwort wirklich genossen, obwohl sie die Frage möglicherweise nicht direkt beantwortet, war sie sehr aufschlussreich. Vielen Dank für Ihren Beitrag. Vielleicht könnten Sie eine Blogpost-Serie zu diesem Thema schreiben? Ich würde es lesen!
Ben
Prost! Was den Blog betrifft, nutze ich diesen Ort gerne, um meine Gedanken loszuwerden! :-D
2

Codestandards im Allgemeinen sind nützlich.

Das bedeutet:

Module sind definitiv notwendig. Sie benötigen auch Konsistenz in der Art und Weise, wie "Klassen" implementiert werden, dh "Methoden auf dem Prototyp" gegenüber "Methoden auf der Instanz". Sie sollten auch entscheiden, auf welche Version von ECMAScript Sie abzielen möchten, und wenn Sie auf ECMAScript 5 abzielen, verwenden Sie die bereitgestellten Sprachfunktionen (z. B. Getter und Setter).

Siehe auch: TypeScript, mit dessen Hilfe Sie beispielsweise Klassen standardisieren können. Ein bisschen neu im Moment, aber ich sehe keine Nachteile bei der Verwendung, da es kaum eine Sperre gibt (weil es zu JavaScript kompiliert wird).

Janus Troelsen
quelle
Ich denke, Typescript ist unnötig.
Marko
1
@marko: Für welche Anwendung nicht erforderlich? Würden Sie einen 25-Kloc-Compiler ohne statische Typisierung implementieren?
Janus Troelsen
2

Eine Trennung zwischen Arbeitscode und bereitgestelltem Code ist hilfreich. Ich verwende ein Tool zum Kombinieren und Komprimieren meiner Javascript-Dateien. Ich kann also eine beliebige Anzahl von Modulen in einem Ordner haben, alle als separate Dateien, wenn ich an diesem bestimmten Modul arbeite. Für die Bereitstellungszeit werden diese Dateien jedoch zu einer komprimierten Datei zusammengefasst.

Ich verwende Chirpy http://chirpy.codeplex.com/ , das auch SASS und CoffeeScript unterstützt.

user69841
quelle