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 dirty
und 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 LayoutSystem
Gerä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 IMotion
Schnittstelle 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.