Wie kann ich die Codebasis von sehr komplexer Software verwalten?


8

Ich erstelle oft Programme für mich und andere mit verschiedenen objektorientierten Programmiersprachen. Dabei sind sie normalerweise relativ klein (höchstens einige tausend Zeilen). In letzter Zeit habe ich jedoch versucht, größere Projekte wie komplette Spiel-Engines zu realisieren. Dabei stoße ich oft auf eine Straßensperre: Komplexität.

In meinen kleineren Projekten fällt es mir leicht, mich an eine mentale Karte zu erinnern, wie jeder Teil des Programms funktioniert. Auf diese Weise kann ich genau wissen, wie sich Änderungen auf den Rest des Programms auswirken, Fehler sehr effektiv vermeiden und genau sehen, wie eine neue Funktion in die Codebasis passen sollte. Wenn ich jedoch versuche, größere Projekte zu erstellen, finde ich es unmöglich, eine gute mentale Karte zu führen, was zu sehr chaotischem Code und zahlreichen unbeabsichtigten Fehlern führt.

Zusätzlich zu diesem "Mental Map" -Problem fällt es mir schwer, meinen Code von anderen Teilen seiner selbst zu entkoppeln. Wenn es beispielsweise in einem Multiplayer-Spiel eine Klasse gibt, die sich mit der Physik der Spielerbewegung befasst, und eine andere, die sich mit dem Networking befasst, sehe ich keine Möglichkeit, dass sich eine dieser Klassen nicht auf die andere verlässt, um Spielerbewegungsdaten in das Netzwerksystem zu übertragen um es über das Netzwerk zu senden. Diese Kopplung ist eine bedeutende Quelle für die Komplexität, die eine gute mentale Karte beeinträchtigt.

Schließlich finde ich oft eine oder mehrere "Manager" -Klassen, die andere Klassen koordinieren. In einem Spiel würde beispielsweise eine Klasse die Haupt-Tick-Schleife handhaben und Aktualisierungsmethoden in den Netzwerk- und Spielerklassen aufrufen. Dies steht im Widerspruch zu der Philosophie, die ich in meiner Forschung festgestellt habe, dass jede Klasse unabhängig von anderen Einheitenprüfbar und verwendbar sein sollte, da jede solche Managerklasse aufgrund ihres Zwecks von den meisten anderen Klassen im Projekt abhängt. Darüber hinaus ist die Orchestrierung des Restes des Programms durch Manager-Klassen eine bedeutende Quelle für nicht mental abbildbare Komplexität.

Zusammengenommen hindert mich dies daran, qualitativ hochwertige fehlerfreie Software von beträchtlicher Größe zu schreiben. Was tun professionelle Entwickler, um dieses Problem effektiv zu lösen? Ich bin besonders an OOP-Antworten interessiert, die auf Java und C ++ abzielen, aber diese Art von Rat ist wahrscheinlich sehr allgemein.

Anmerkungen:

  • Ich habe versucht, UML-Diagramme zu verwenden, aber das scheint nur beim ersten Problem zu helfen, und selbst dann nur, wenn es um die Klassenstruktur geht und nicht (zum Beispiel) um die Reihenfolge der Methodenaufrufe in Bezug auf das, was zuerst initialisiert wird.

7
"Wie verdiene ich beim Handel mit Aktien so viel Geld wie möglich?" - gleiche Frage, anderer Beruf.
MaxB

1
Wie viel Zeit verbringen Sie mit Design und Modellierung? Viele der Probleme, auf die wir während der Entwicklung stoßen, sind auf einen Perspektivverlust zurückzuführen. Die Art von Perspektive, auf die wir uns bei der Modellierung konzentrieren. Dann später versuchen wir, dieses Leck mit Code zu lösen. Code, der auch Perspektive verliert (die richtige Abstraktion). Großprojekte müssen geplant werden, um ein klares Gesamtbild zu erhalten. Von der Ebene der Codebasis wird es schwierig sein, die Vision zu haben. " Lass dich nicht von den Bäumen davon abhalten, den Wald zu sehen ".
Laiv

4
Modularisierung und Isolation. Aber es scheint, dass Sie das bereits tun, aber daran scheitern. In diesem Fall hängt es wirklich davon ab, um welche Art von Projekt es sich handelt und welche Art von Technologien verwendet werden.
Euphoric

1
Ständiges Refactoring ist ein notwendiger Bestandteil der Softwareentwicklung - betrachten Sie es als Hinweis darauf, dass Sie etwas richtig machen. Jedes nicht triviale System enthält immer "unbekannte Unbekannte" - dh die Dinge, die Sie in Ihrem ursprünglichen Design möglicherweise nicht vorausgesehen oder berücksichtigt haben könnten. Refactoring ist die Realität der iterativen Entwicklung und der schrittweisen Entwicklung zu einer „guten“ Lösung. Dies
Ben Cottrell,

2
UML IMHO fehlt hierfür der nützlichste Diagrammtyp: Datenflussdiagramme. Dies ist jedoch kein Wundermittel. Modularisierung und Isolierung werden nicht automatisch mithilfe von Datenflussdiagrammen durchgeführt.
Doc Brown

Antworten:


8

Ich stoße oft auf eine Straßensperre: Komplexität.

Es gibt ganze Bücher zu diesem Thema. Hier ist ein Zitat aus einem der wichtigsten Bücher, die jemals über Softwareentwicklung geschrieben wurden, Steve McConnells Code Complete :

Das Management von Komplexität ist das wichtigste technische Thema in der Softwareentwicklung. Meiner Ansicht nach ist es so wichtig, dass der primäre technische Imperativ von Software darin besteht, die Komplexität zu verwalten.

Abgesehen davon würde ich das Lesen des Buches sehr empfehlen, wenn Sie überhaupt Interesse an Softwareentwicklung haben (was ich vermute, da Sie diese Frage gestellt haben). Klicken Sie mindestens auf den obigen Link und lesen Sie den Auszug über Designkonzepte.


Wenn es beispielsweise in einem Multiplayer-Spiel eine Klasse gibt, die sich mit der Physik der Spielerbewegung befasst, und eine andere, die sich mit dem Networking befasst, sehe ich keine Möglichkeit, dass sich eine dieser Klassen nicht auf die andere verlässt, um Spielerbewegungsdaten in das Netzwerksystem zu übertragen um es über das Netzwerk zu senden.

In diesem speziellen Fall würde ich Ihre PlayerMovementCalculatorKlasse und Ihre NetworkSystemKlasse als völlig unabhängig voneinander betrachten. Eine Klasse ist für die Berechnung der Spielerbewegung verantwortlich, die andere für die Netzwerk-E / A. Vielleicht sogar in separaten unabhängigen Modulen.

Ich würde jedoch sicherlich erwarten, dass es irgendwo außerhalb dieser Module zumindest ein zusätzliches Stück Verkabelung oder Kleber gibt , das Daten und / oder Ereignisse / Nachrichten zwischen ihnen vermittelt. Beispielsweise können Sie eine PlayerNetworkMediatorKlasse mithilfe des Mediator-Musters schreiben .

Ein anderer möglicher Ansatz könnte darin bestehen, Ihre Module mithilfe eines Ereignisaggregators zu entkoppeln .

Im Fall von asynchroner Programmierung, wie z. B. der Art der Logik, die mit Netzwerksockets verbunden ist, können Sie Expose Observables verwenden , um den Code aufzuräumen, der diese Benachrichtigungen abhört.

Asynchrone Programmierung bedeutet nicht unbedingt auch Multithreading. Es geht mehr um Programmstruktur und Flusskontrolle (obwohl Multithreading der offensichtliche Anwendungsfall für Asynchronität ist). Observables können in einem oder beiden dieser Module nützlich sein, damit nicht verwandte Klassen Änderungsbenachrichtigungen abonnieren können.

Zum Beispiel:

  • NetworkMessageReceivedEvent
  • PlayerPositionChangedEvent
  • PlayerDisconnectedEvent

usw.


Schließlich finde ich oft eine oder mehrere "Manager" -Klassen, die andere Klassen koordinieren. In einem Spiel würde beispielsweise eine Klasse die Haupt-Tick-Schleife handhaben und Aktualisierungsmethoden in den Netzwerk- und Spielerklassen aufrufen. Dies steht im Widerspruch zu der Philosophie, die ich in meiner Forschung festgestellt habe, dass jede Klasse unabhängig von anderen Einheitenprüfbar und verwendbar sein sollte, da jede solche Managerklasse aufgrund ihres Zwecks von den meisten anderen Klassen im Projekt abhängt. Darüber hinaus ist die Orchestrierung des Restes des Programms durch Manager-Klassen eine bedeutende Quelle für nicht mental abbildbare Komplexität.

Während einiges davon sicherlich auf Erfahrung zurückzuführen ist; Der Name Managerin einer Klasse weist häufig auf einen Designgeruch hin .

Wenn Klassen zu benennen, sollten Sie die Funktionalität dieser Klasse für, verantwortlich ist und lassen Sie Ihre Klassennamen reflektieren , was sie tut .

Das Problem mit Managern im Code ist ein bisschen wie das Problem mit Managern am Arbeitsplatz. Ihr Zweck ist in der Regel vage und selbst für sich selbst schlecht verstanden. Die meiste Zeit sind wir ohne sie einfach besser dran.

Bei der objektorientierten Programmierung geht es hauptsächlich um Verhalten . Eine Klasse ist keine Datenentität, sondern eine Darstellung einiger funktionaler Anforderungen in Ihrem Code.

Wenn Sie eine Klasse basierend auf den funktionalen Anforderungen benennen können, die sie erfüllt, verringern Sie die Wahrscheinlichkeit, dass Sie ein aufgeblähtes Gottobjekt erhalten , und es ist wahrscheinlicher, dass Sie eine Klasse haben, deren Identität und Zweck in Ihrem Programm klar ist.

Darüber hinaus sollte es offensichtlicher sein, wenn sich zusätzliche Methoden und Verhaltensweisen einschleichen, wenn sie wirklich nicht dazu gehören, da der Name falsch aussieht - dh Sie haben eine Klasse, die eine ganze Reihe von Dingen erledigt, die nicht funktionieren. t spiegelt sich in seinem Namen wider

Vermeiden Sie schließlich die Versuchung, Klassen zu schreiben, deren Namen so aussehen, als gehörten sie zu einem Entitätsbeziehungsmodell. Das Problem mit dem Klassennamen wie Player, Monster, Car, Dogusw. ist , dass das nichts über ihr Verhalten impliziert, und scheint nur eine Sammlung von logisch verknüpften Daten oder Attributen zu beschreiben. Objektorientiertes Design ist keine Datenmodellierung, sondern eine Verhaltensmodellierung.

Betrachten Sie beispielsweise zwei verschiedene Methoden zur Modellierung eines Schadens Monsterund zur PlayerBerechnung des Schadens:

class Monster : GameEntity {
    dealDamage(...);
}

class Player : GameEntity {
    dealDamage(...);
}

Das Problem hierbei ist, dass Sie vernünftigerweise eine ganze Reihe anderer Methoden erwarten Playerund Monsterhaben können, die wahrscheinlich völlig unabhängig von der Höhe des Schadens sind, den diese Entitäten anrichten könnten (z. B. Bewegung). Du bist auf dem Weg zum oben erwähnten Gottobjekt.

Ein natürlicherer objektorientierter Ansatz besteht darin, den Namen der Klasse anhand ihres Verhaltens zu identifizieren, zum Beispiel:

class MonsterDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

class PlayerDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

Bei dieser Art von Design sind Ihren Playerund MonsterObjekten wahrscheinlich keine Methoden zugeordnet, da diese Objekte die Daten enthalten, die von Ihrer gesamten Anwendung benötigt werden. Es handelt sich wahrscheinlich nur um einfache Datenentitäten, die sich in einem Repository befinden und nur Felder / Eigenschaften enthalten.

Dieser Ansatz wird normalerweise als anämisches Domänenmodell bezeichnet , das als Anti-Pattern für Domain-Driven-Design (DDD) angesehen wird. Die SOLID-Prinzipien führen Sie jedoch natürlich zu einer sauberen Trennung zwischen "gemeinsam genutzten" Datenentitäten (möglicherweise in einem Repository). und modulare (vorzugsweise zustandslose) Verhaltensklassen im Objektdiagramm Ihrer Anwendung.

SOLID und DDD sind zwei verschiedene Ansätze für das OO-Design. Während sie sich in vielerlei Hinsicht überschneiden, tendieren sie dazu, in Bezug auf die Klassenidentität und die Trennung von Daten und Verhalten in entgegengesetzte Richtungen zu ziehen.


Zurück zu McConnells obigem Zitat: Das Management von Komplexität ist der Grund, warum Softwareentwicklung eher ein qualifizierter Beruf als eine alltägliche Büroarbeit ist. Bevor McConnell sein Buch schrieb, schrieb Fred Brooks einen Artikel zu diesem Thema, der die Antwort auf Ihre Frage genau zusammenfasst: Es gibt keine Silberkugel für den Umgang mit Komplexität.

Obwohl es keine einzige Antwort gibt, können Sie sich das Leben leichter oder schwerer machen, je nachdem, wie Sie es angehen:

  • Erinnere dich an KISS , DRY und YAGNI .
  • Verstehen, wie die SOLID-Prinzipien von OO Design / Software Development angewendet werden
  • Verstehen Sie auch domänengesteuertes Design, selbst wenn es Orte gibt, an denen der Ansatz im Widerspruch zu den SOLID-Prinzipien steht. SOLID und DDD stimmen eher überein als nicht.
  • Erwarten Sie, dass sich Ihr Code ändert - schreiben Sie automatisierte Tests, um die Auswirkungen dieser Änderungen zu erfassen (Sie müssen TDD nicht befolgen , um nützliche automatisierte Tests zu schreiben - tatsächlich können einige dieser Tests Integrationstests mit "Wegwerf" -Konsolen-Apps oder sein Kabelbaum-Apps testen)
  • Am wichtigsten - sei pragmatisch. Befolgen Sie keine sklavischen Richtlinien. Das Gegenteil von Komplexität ist Einfachheit, also im Zweifelsfall (wieder) - KISS

Solide Antwort. Wortspiel beabsichtigt.
ipaul

4
Ich bin nicht der Meinung, dass DDD und SOLID nicht miteinander übereinstimmen. Ich denke, dass sie sich tatsächlich ziemlich ergänzen. Ein Grund, warum sie widersprüchlich oder vielleicht orthogonal erscheinen mögen, ist, dass sie verschiedene Dinge auf einer anderen Abstraktionsebene beschreiben.
RibaldEddie

1
@ RibaldEddie Ich stimme zu, dass sie sich größtenteils ergänzen. Das Hauptproblem, das ich sehe, ist, dass SOLID das sogenannte "anämische" Domänenmodell als natürliche Folge der logischen Schlussfolgerung von SRP zu einer echten Einzelverantwortung pro Klasse aktiv zu fördern scheint, während DDD dies als Anti-Verantwortlichkeit bezeichnet. Muster. Oder hat Fowlers Beschreibung des anämischen Domänenmodells etwas, das ich falsch interpretiert habe?
Ben Cottrell

1

Ich denke, es geht um die Verwaltung von Komplexität, die immer dann abnimmt, wenn die Dinge um Sie herum zu komplex werden, um sie noch mit dem zu verwalten, was Sie derzeit haben, und wahrscheinlich ein paar Mal auftritt, wenn Sie von einem 1-Mann-Unternehmen zu einem globalen Unternehmen mit einer halben Million Personen heranwachsen .

Im Allgemeinen wächst die Komplexität, wenn Ihr Projekt wächst. Und im Allgemeinen benötigen Sie in dem Moment, in dem die Komplexität zu groß wird (unabhängig davon, ob es sich um die IT oder die Verwaltung des Programms, der Programme oder des gesamten Unternehmens handelt), mehr Tools oder ersetzen Tools durch Tools, die die Komplexität besser handhaben, als es die Menschheit seit Ewigkeiten getan hat. Und Tools gehen Hand in Hand mit komplexeren Prozessen, da jemand etwas ausgeben muss, an dem jemand anderes arbeiten muss. Und diese gehen Hand in Hand mit "zusätzlichen Personen", die bestimmte Rollen haben, z. B. einem Enterprise Architect, der bei einem 1-Personen-Projekt zu Hause nicht benötigt wird.

Gleichzeitig muss Ihre dahinter stehende Taxonomie hinter Ihrer Komplexität wachsen, bis die Taxonomie zu komplex ist und Sie auf unstrukturierte Daten umsteigen. Sie können beispielsweise eine Liste von Websites erstellen und diese kategorisieren. Wenn Sie jedoch eine Liste mit 1 Milliarde Websites benötigen, können Sie dies nicht innerhalb einer angemessenen Zeit tun, sodass Sie zu intelligenteren Algorithmen wechseln oder einfach nur Daten durchsuchen.

In einem kleinen Projekt können Sie an einem freigegebenen Laufwerk arbeiten. Bei einem etwas großen Projekt installieren Sie .git oder .svn oder was auch immer. In einem komplexen Programm mit mehreren Projekten, die alle unterschiedliche Releases haben, die alle live sind und unterschiedliche Release-Daten haben und alle von anderen Systemen abhängig sind, müssen Sie möglicherweise zu Clearcase wechseln, wenn Sie für das Konfigurationsmanagement verantwortlich sind, anstatt nur die Versionierung einige Zweige eines Projekts.

Daher denke ich, dass die beste Anstrengung des Menschen bis jetzt eine Kombination von Werkzeugen, spezifischen Rollen und Prozessen ist, um die zunehmende Komplexität von fast allem zu bewältigen.

Glücklicherweise gibt es Unternehmen, die Geschäftsrahmen, Prozessrahmen, Taxonomien, Datenmodelle usw. verkaufen. Je nach Branche können Sie also kaufen, wenn die Komplexität so groß geworden ist, dass jeder anerkennt, dass es keinen anderen Weg gibt, Daten. Modell und Prozess sofort einsatzbereit, z. B. für ein globales Versicherungsunternehmen. Von dort aus arbeiten Sie sich nach unten, um Ihre vorhandenen Anwendungen zu überarbeiten und alles wieder mit dem allgemein bewährten Framework in Einklang zu bringen, das bereits ein bewährtes Datenmodell und Prozesse enthält.

Wenn es für Ihre Branche keine auf dem Markt gibt, gibt es vielleicht eine Chance :)


Es scheint, als würden Sie im Grunde genommen eine Kombination aus "Leute einstellen" oder "etwas kaufen, das Ihre Probleme verschwinden lässt" vorschlagen. Ich bin an einer Verwaltungskomplexität als einzelner Entwickler interessiert, der (vorerst) kein Budget für Hobbyprojekte hat.
John01dav

Ich denke auf dieser Ebene ist Ihre Frage wahrscheinlich so etwas wie "100 Best Practices nennen, die ein guter Anwendungsarchitekt anwendet"? Weil ich denke, dass er im Wesentlichen die Komplexität hinter allem verwaltet, was er tut. Er setzt Maßstäbe, um die Komplexität zu bekämpfen.
Edelwasser

Das ist eine Art, was ich frage, aber ich interessiere mich mehr für eine allgemeine Philosophie als für eine Liste.
John01dav

Es ist eine breite Frage, da es mehr als 21.000 Bücher über Praktiken / Muster / Design der Anwendungsarchitektur usw. gibt: safaribooksonline.com/search/?query=application%20architecture
edelwater

Das Buch konnte nur die Popularität einer Frage zeigen, nicht die Breite guter Antworten. Wenn Sie der Meinung sind, dass zur Beantwortung dieser Frage ein Buch erforderlich ist, geben Sie auf jeden Fall eine Empfehlung ab.
John01dav

0

Hören Sie auf, alles OOP zu schreiben, und fügen Sie stattdessen einige Dienste hinzu.

Zum Beispiel habe ich kürzlich eine GOAP für ein Spiel geschrieben. Sie werden die Beispiele im Internet sehen, die ihre Aktionen mit der Spiel-Engine im OOP-Stil verbinden:

Action.Process();

In meinem Fall musste ich Aktionen sowohl in der Engine als auch außerhalb der Engine verarbeiten und Aktionen auf weniger detaillierte Weise simulieren, wenn der Spieler nicht anwesend ist. Also habe ich stattdessen verwendet:

Interface IActionProcessor<TAction>
{
    void ProcessAction(TAction action);
}

Dadurch kann ich die Logik entkoppeln und zwei Klassen haben.

ActionProcesser_InEngine.ProcessAction()


ActionProcesser_BackEnd.ProcessAction()

Es ist nicht OOP, aber es bedeutet, dass die Aktionen jetzt keine Verbindung zur Engine haben. Somit ist mein GOAP-Modul völlig vom Rest des Spiels getrennt.

Sobald es fertig ist, benutze ich einfach die DLL und denke nicht an die interne Implementierung.


1
Wie ist Ihr Aktionsprozessor nicht objektorientiert?
Greg Burghardt

2
Ich denke, Ihr Punkt ist: „Versuchen Sie nicht, ein OOP-Design zu erstellen, da OOP nur eine von vielen Techniken zur Bewältigung der Komplexität ist - etwas anderes (z. B. ein prozeduraler Ansatz) könnte in Ihrem Fall besser funktionieren. Es spielt keine Rolle, wie Sie dorthin gelangen, solange Sie klare Modulgrenzen haben, die über Implementierungsdetails abstrahieren. “
Amon

1
Ich denke, das Problem liegt nicht auf Code-Ebene. Es ist auf höheren Ebenen. Die Probleme mit der Codebasis sind Symptome, keine Krankheiten. Die Lösung kommt nicht aus dem Code. Ich denke, es wird vom Design und der Modellierung kommen.
Laiv

2
Wenn ich dies lese und mich als OP vorstelle, finde ich, dass diese Antwort nicht detailliert genug ist und keine nützlichen Informationen liefert, da sie nicht beschreibt, wie der IActionProcessor tatsächlich Wert liefert, anstatt nur eine weitere Indirektionsebene hinzuzufügen.
Whatsisname

Die Nutzung solcher Dienste ist eher prozedural als OO. Dies bietet eine klarere Trennung zwischen den Verantwortlichkeiten als OO, die, wie das OP festgestellt hat, schnell übermäßig gekoppelt werden kann
Ewan

0

Was für eine Mutter von einer Frage! Es könnte mir peinlich sein, dies mit meinen skurrilen Gedanken zu versuchen (und ich würde gerne Vorschläge hören, wenn ich wirklich abwesend bin). Für mich war es jedoch das Nützlichste, was ich in letzter Zeit in meiner Domäne gelernt habe (einschließlich Spielen in der Vergangenheit, jetzt VFX), Interaktionen zwischen abstrakten Schnittstellen mit Daten als Entkopplungsmechanismus zu ersetzen (und letztendlich die zwischen ihnen erforderliche Informationsmenge zu reduzieren Dinge und über einander bis auf das kleinste Minimum). Das klingt vielleicht völlig verrückt (und ich verwende möglicherweise alle möglichen schlechten Begriffe).

Nehmen wir jedoch an, ich gebe Ihnen einen einigermaßen überschaubaren Job. Sie haben diese Datei mit Szenen- und Animationsdaten zum Rendern. Es gibt eine Dokumentation zum Dateiformat. Ihre einzige Aufgabe besteht darin, die Datei zu laden, hübsche Bilder für die Animation mithilfe der Pfadverfolgung zu rendern und die Ergebnisse in Bilddateien auszugeben. Das ist eine ziemlich kleine Anwendung, die wahrscheinlich nicht mehr als Zehntausende von LOC umfassen wird, selbst für einen ziemlich hoch entwickelten Renderer (definitiv nicht Millionen).

Geben Sie hier die Bildbeschreibung ein

Sie haben Ihre eigene kleine isolierte Welt für diesen Renderer. Es wird nicht von der Außenwelt beeinflusst. Es isoliert seine eigene Komplexität. Abgesehen von den Bedenken, diese Szenendatei zu lesen und Ihre Ergebnisse in Bilddateien auszugeben, können Sie sich ganz auf das reine Rendern konzentrieren. Wenn dabei etwas schief geht, wissen Sie, dass es sich im Renderer befindet und nichts anderes, da dieses Bild nichts anderes enthält.

Nehmen wir stattdessen an, Sie müssen Ihren Renderer im Kontext einer großen Animationssoftware arbeiten lassen, die tatsächlich Millionen von LOC enthält. Anstatt nur ein optimiertes, dokumentiertes Dateiformat zu lesen, um an die für das Rendern erforderlichen Daten zu gelangen, müssen Sie alle Arten von abstrakten Schnittstellen durchlaufen, um alle Daten abzurufen, die Sie für Ihre Arbeit benötigen:

Geben Sie hier die Bildbeschreibung ein

Plötzlich befindet sich Ihr Renderer nicht mehr in seiner eigenen kleinen isolierten Welt. Das fühlt sich so viel komplexer an. Sie müssen das Gesamtdesign einer ganzen Menge der Software als ein organisches Ganzes mit möglicherweise vielen beweglichen Teilen verstehen und vielleicht sogar manchmal über Implementierungen von Dingen wie Netzen oder Kameras nachdenken, wenn Sie auf einen Engpass oder einen Fehler in einem von ihnen stoßen die Funktionen.

Funktionalität vs. optimierte Daten

Einer der Gründe ist, dass die Funktionalität viel komplexer ist als statische Daten. Es gibt auch so viele Möglichkeiten, wie ein Funktionsaufruf auf eine Weise schief gehen kann, die das Lesen statischer Daten nicht kann. Es gibt so viele versteckte Nebenwirkungen, die beim Aufrufen dieser Funktionen auftreten können, obwohl konzeptionell nur schreibgeschützte Daten zum Rendern abgerufen werden. Es kann auch so viele weitere Gründe geben, sich zu ändern. In einigen Monaten kann es vorkommen, dass die Netz- oder Texturschnittstelle Teile so ändert oder ablehnt, dass Sie wichtige Abschnitte Ihres Renderers neu schreiben und mit diesen Änderungen Schritt halten müssen, obwohl Sie genau dieselben Daten abrufen Die Dateneingabe in Ihren Renderer hat sich nicht geändert (nur die Funktionalität, die erforderlich ist, um letztendlich auf alles zuzugreifen).

Wenn möglich, habe ich festgestellt, dass optimierte Daten ein sehr guter Entkopplungsmechanismus sind, mit dem Sie wirklich vermeiden müssen, über das gesamte System als Ganzes nachzudenken, und sich nur auf einen bestimmten Teil des Systems konzentrieren können Verbesserungen, Hinzufügen neuer Funktionen, Beheben von Problemen usw. Es folgt einer sehr E / A-Denkweise für die sperrigen Teile, aus denen Ihre Software besteht. Geben Sie dies ein, machen Sie Ihr Ding, geben Sie das aus, und ohne Dutzende abstrakter Schnittstellen zu durchlaufen, beenden Sie endlose Funktionsaufrufe auf dem Weg. Und es fängt an, bis zu einem gewissen Grad einer funktionalen Programmierung zu ähneln.

Dies ist also nur eine Strategie und möglicherweise nicht für alle Menschen anwendbar. Und wenn Sie alleine fliegen, müssen Sie natürlich immer noch alles beibehalten (einschließlich des Formats der Daten selbst), aber der Unterschied besteht darin, dass Sie sich wirklich auf den Renderer konzentrieren können, wenn Sie sich hinsetzen, um Verbesserungen an diesem Renderer vorzunehmen zum größten Teil und sonst nichts. Es wird in seiner eigenen kleinen Welt so isoliert - ungefähr so ​​isoliert wie es sein könnte, wenn die Daten, die es für die Eingabe benötigt, so rationalisiert werden.

Ich habe das Beispiel eines Dateiformats verwendet, aber es muss keine Datei sein, die die optimierten Daten von Interesse für die Eingabe bereitstellt. Es könnte sich um eine In-Memory-Datenbank handeln. In meinem Fall handelt es sich um ein Entity-Component-System, bei dem die Komponenten die interessierenden Daten speichern. Ich habe jedoch festgestellt, dass dieses Grundprinzip der Entkopplung zu optimierten Daten (wie auch immer Sie es tun) meine geistige Leistungsfähigkeit so viel weniger belastet als frühere Systeme, an denen ich gearbeitet habe und die sich um Abstraktionen und viele, viele, viele Interaktionen zwischen all diesen drehten abstrakte Schnittstellen, die es unmöglich machten, sich nur mit einer Sache hinzusetzen und nur darüber und über wenig anderes nachzudenken. Mein Gehirn füllte sich bis an den Rand mit diesen früheren Systemtypen und wollte explodieren, weil so viele Interaktionen zwischen so vielen Dingen stattfanden.

Entkopplung

Wenn Sie minimieren möchten, wie viel größere Codebasen Ihr Gehirn belasten, machen Sie es so, dass jeder wichtige Teil der Software (ein ganzes Rendering-System, ein ganzes Physik-System usw.) in einer möglichst isolierten Welt lebt. Minimieren Sie den Umfang der Kommunikation und Interaktion, der durch die optimiertesten Daten auf ein Minimum reduziert wird. Sie können sogar Redundanz akzeptieren (redundante Arbeit für den Prozessor oder sogar für sich selbst), wenn der Austausch ein weitaus isolierteres System ist, das nicht mit Dutzenden anderer Dinge sprechen muss, bevor es seine Arbeit erledigen kann.

Und wenn Sie damit beginnen, haben Sie das Gefühl, Sie verwalten ein Dutzend kleiner Anwendungen anstelle einer gigantischen. Und ich finde das auch viel lustiger. Sie können sich hinsetzen und nach Herzenslust an einem System arbeiten, ohne sich mit der Außenwelt zu befassen. Es wird nur die Eingabe der richtigen Daten und die Ausgabe der richtigen Daten am Ende an einen Ort, an dem andere Systeme darauf zugreifen können (an diesem Punkt könnte ein anderes System dies eingeben und seine Sache tun, aber das muss Sie nicht interessieren bei der Arbeit an Ihrem System). Natürlich müssen Sie zum Beispiel immer noch darüber nachdenken, wie alles in die Benutzeroberfläche integriert ist (ich muss immer noch über das gesamte Design für GUIs nachdenken), aber zumindest nicht, wenn Sie sich hinsetzen und an diesem vorhandenen System arbeiten oder beschließen Sie, eine neue hinzuzufügen.

Vielleicht beschreibe ich Menschen, die mit den neuesten technischen Methoden auf dem neuesten Stand sind, etwas Offensichtliches. Ich weiß es nicht. Aber es war mir nicht klar. Ich wollte mich dem Design von Software um Objekte nähern, die miteinander interagieren, und Funktionen, die für umfangreiche Software benötigt werden. Und die Bücher, die ich ursprünglich über umfangreiches Software-Design gelesen habe, konzentrierten sich auf Schnittstellendesigns über Dinge wie Implementierungen und Daten (das Mantra war damals, dass Implementierungen nicht so wichtig sind, sondern nur Schnittstellen, da erstere leicht ausgetauscht oder ersetzt werden können ). Anfangs war es für mich nicht intuitiv, die Interaktionen einer Software als bloße Eingabe und Ausgabe von Daten zwischen riesigen Subsystemen zu betrachten, die nur über diese optimierten Daten miteinander kommunizieren. Als ich mich jedoch darauf konzentrierte, dieses Konzept zu entwerfen, machte es die Dinge so viel einfacher. Ich könnte so viel mehr Code hinzufügen, ohne dass mein Gehirn explodiert. Es fühlte sich an, als würde ich ein Einkaufszentrum anstelle eines Turms bauen, der einstürzen könnte, wenn ich zu viel hinzufüge oder wenn in einem Teil ein Bruch vorliegt.

Komplexe Implementierungen vs. komplexe Interaktionen

Dies ist eine weitere, die ich erwähnen sollte, da ich einen Großteil meines frühen Teils meiner Karriere damit verbracht habe, nach den einfachsten Implementierungen zu suchen. Also zerlegte ich die Dinge in die kleinsten und einfachsten Teile und dachte, ich würde die Wartbarkeit verbessern.

Im Nachhinein merkte ich nicht, dass ich eine Art von Komplexität gegen eine andere austauschte. Indem alles auf das Einfachste reduziert wurde, wurden die Interaktionen zwischen diesen kleinen Teilen zu einem komplexesten Netz von Interaktionen mit Funktionsaufrufen, die manchmal 30 Ebenen tief in den Callstack gingen. Und wenn Sie sich eine Funktion ansehen, ist es natürlich so einfach und leicht zu wissen, was sie tut. Zu diesem Zeitpunkt erhalten Sie jedoch nicht viele nützliche Informationen, da jede Funktion so wenig leistet. Sie müssen dann alle Arten von Funktionen nachverfolgen und durch alle Arten von Reifen springen, um tatsächlich herauszufinden, was sie alle auf eine Weise bewirken, die Ihr Gehirn dazu bringen kann, mehr als eine größere zu explodieren.

Das soll nicht bedeuten, dass Gott Objekte oder ähnliches sind. Aber vielleicht müssen wir unsere Netzobjekte nicht in kleinste Dinge wie ein Scheitelpunktobjekt, ein Kantenobjekt oder ein Gesichtsobjekt zerlegen. Vielleicht könnten wir es einfach mit einer moderat komplexeren Implementierung im Netz halten, um radikal weniger Code-Interaktionen zu erhalten. Ich kann hier und da eine mäßig komplexe Implementierung durchführen. Ich kann nicht mit einer Unmenge von Wechselwirkungen mit Nebenwirkungen umgehen, die wer-weiß-wo und in welcher Reihenfolge auftreten.

Zumindest finde ich das Gehirn sehr viel weniger belastend, weil es die Interaktionen sind, die mein Gehirn in einer großen Codebasis verletzen. Keine bestimmte Sache.

Allgemeinheit vs. Spezifität

Vielleicht mit dem oben Gesagten verbunden, liebte ich die Allgemeinheit und die Wiederverwendung von Code und dachte immer, dass die größte Herausforderung beim Entwerfen einer guten Schnittstelle darin bestand, die unterschiedlichsten Anforderungen zu erfüllen, da die Schnittstelle von allen möglichen Dingen mit unterschiedlichen Anforderungen verwendet werden würde. Und wenn Sie das tun, müssen Sie unweigerlich über hundert Dinge gleichzeitig nachdenken, weil Sie versuchen, die Bedürfnisse von hundert Dingen gleichzeitig auszugleichen.

Das Verallgemeinern von Dingen braucht so viel Zeit. Schauen Sie sich einfach die Standardbibliotheken an, die unsere Sprachen begleiten. Die C ++ - Standardbibliothek enthält so wenig Funktionen, dass jedoch Teams von Personen erforderlich sind, um ganze Komitees von Personen zu pflegen und mit ihnen abzustimmen, die über ihr Design diskutieren und Vorschläge machen. Das liegt daran, dass diese kleine Funktionalität versucht, die Bedürfnisse der ganzen Welt zu erfüllen.

Vielleicht müssen wir die Dinge nicht so weit bringen. Vielleicht ist es in Ordnung, nur einen räumlichen Index zu haben, der nur zur Kollisionserkennung zwischen indizierten Netzen und sonst nichts verwendet wird. Vielleicht können wir eine andere für andere Arten von Oberflächen und eine andere zum Rendern verwenden. Früher habe ich mich so darauf konzentriert, diese Art von Redundanzen zu beseitigen, aber ein Grund dafür war, dass ich es mit sehr ineffizienten Datenstrukturen zu tun hatte, die von einer Vielzahl von Menschen implementiert wurden. Wenn Sie einen Octree haben, der 1 Gigabyte für ein 300k-Dreiecksnetz benötigt, möchten Sie natürlich keinen weiteren im Speicher haben.

Aber warum sind die Oktrees überhaupt so ineffizient? Ich kann Oktrees erstellen, die nur 4 Bytes pro Knoten und weniger als ein Megabyte benötigen, um dasselbe wie diese Gigabyte-Version zu tun, während ich in einem Bruchteil der Zeit baue und schnellere Suchabfragen durchführe. Zu diesem Zeitpunkt ist eine gewisse Redundanz völlig akzeptabel.

Effizienz

Dies ist also nur für leistungskritische Bereiche relevant. Je besser Sie jedoch mit der Speichereffizienz umgehen, desto mehr können Sie es sich leisten, ein bisschen mehr (möglicherweise etwas mehr Redundanz im Austausch für eine geringere Allgemeinheit oder Entkopplung) zugunsten der Produktivität zu verschwenden . Und dort ist es hilfreich, sich mit Ihren Profilern vertraut zu machen und sich mit der Computerarchitektur und der Speicherhierarchie vertraut zu machen, denn dann können Sie es sich leisten, im Austausch für Produktivität mehr Einbußen bei der Effizienz zu machen, weil Ihr Code bereits so effizient ist und es sich leisten kann selbst in den kritischen Bereichen etwas weniger effizient und dennoch besser als die Konkurrenz. Ich habe festgestellt, dass die Verbesserung in diesem Bereich es mir auch ermöglicht hat, mit immer einfacheren Implementierungen davonzukommen.

Verlässlichkeit

Dies ist offensichtlich, könnte es aber genauso gut erwähnen. Ihre zuverlässigsten Dinge erfordern den minimalen intellektuellen Aufwand. Sie müssen nicht viel über sie nachdenken. Sie arbeiten einfach. Je größer Ihre Liste an äußerst zuverlässigen Teilen ist, die durch gründliche Tests auch "stabil" sind (nicht geändert werden müssen), desto weniger müssen Sie darüber nachdenken.

Besonderheiten

All das oben Genannte behandelt einige allgemeine Dinge, die mir geholfen haben, aber lassen Sie uns zu spezifischeren Aspekten für Ihre Region übergehen:

In meinen kleineren Projekten fällt es mir leicht, mich an eine mentale Karte zu erinnern, wie jeder Teil des Programms funktioniert. Auf diese Weise kann ich genau wissen, wie sich Änderungen auf den Rest des Programms auswirken, Fehler sehr effektiv vermeiden und genau sehen, wie eine neue Funktion in die Codebasis passen sollte. Wenn ich jedoch versuche, größere Projekte zu erstellen, finde ich es unmöglich, eine gute mentale Karte zu führen, was zu sehr chaotischem Code und zahlreichen unbeabsichtigten Fehlern führt.

Für mich hängt dies tendenziell mit komplexen Nebenwirkungen und komplexen Kontrollabläufen zusammen. Das ist eine eher einfache Sicht der Dinge, aber all die am schönsten aussehenden Schnittstellen und die Entkopplung vom Konkreten zum Abstrakten können es nicht einfacher machen, über komplexe Nebenwirkungen in komplexen Kontrollabläufen nachzudenken.

Vereinfachen / reduzieren Sie die Nebenwirkungen und / oder vereinfachen Sie die Kontrollflüsse, idealerweise beides. und Sie werden es im Allgemeinen viel einfacher finden, darüber nachzudenken, was viel größere Systeme tun und was als Reaktion auf Ihre Änderungen passieren wird.

Zusätzlich zu diesem "Mental Map" -Problem fällt es mir schwer, meinen Code von anderen Teilen seiner selbst zu entkoppeln. Wenn es beispielsweise in einem Multiplayer-Spiel eine Klasse gibt, die sich mit der Physik der Spielerbewegung befasst, und eine andere, die sich mit dem Networking befasst, sehe ich keine Möglichkeit, dass sich eine dieser Klassen nicht auf die andere verlässt, um Spielerbewegungsdaten in das Netzwerksystem zu übertragen um es über das Netzwerk zu senden. Diese Kopplung ist eine wesentliche Quelle für die Komplexität, die eine gute mentale Karte beeinträchtigt.

Konzeptionell muss man eine Kopplung haben. Wenn Menschen über Entkopplung sprechen, meinen sie normalerweise, eine Art durch eine andere, wünschenswertere Art zu ersetzen (typischerweise in Richtung Abstraktionen). Angesichts meiner Domäne, der Funktionsweise meines Gehirns usw. sind für mich die oben diskutierten optimierten Daten die wünschenswerteste Art, die Anforderungen an die "mentale Karte" auf ein Minimum zu reduzieren. Eine Black Box spuckt Daten aus, die in eine andere Black Box eingespeist werden, und beide wissen nichts über die Existenz des anderen. Sie kennen nur einen zentralen Ort, an dem Daten gespeichert werden (z. B. ein zentrales Dateisystem oder eine zentrale Datenbank), über den sie ihre Eingaben abrufen, etwas tun und eine neue Ausgabe ausspucken, die dann möglicherweise von einer anderen Black Box eingegeben wird.

Wenn Sie dies auf diese Weise tun, würde das Physiksystem von der zentralen Datenbank und das Netzwerksystem von der zentralen Datenbank abhängen, aber sie würden nichts voneinander wissen. Sie müssten nicht einmal wissen, dass es sie gibt. Sie müssten nicht einmal wissen, dass abstrakte Schnittstellen für einander existieren.

Schließlich finde ich oft eine oder mehrere "Manager" -Klassen, die andere Klassen koordinieren. In einem Spiel würde beispielsweise eine Klasse die Haupt-Tick-Schleife handhaben und Aktualisierungsmethoden in den Netzwerk- und Spielerklassen aufrufen. Dies steht im Widerspruch zu der Philosophie, die ich in meiner Forschung festgestellt habe, dass jede Klasse unabhängig von anderen Einheitenprüfbar und verwendbar sein sollte, da jede solche Managerklasse aufgrund ihres Zwecks von den meisten anderen Klassen im Projekt abhängt. Darüber hinaus ist die Orchestrierung des Restes des Programms durch Manager-Klassen eine bedeutende Quelle für nicht mental abbildbare Komplexität.

Sie benötigen in der Regel etwas, um alle Systeme in Ihrem Spiel zu orchestrieren. Central ist vielleicht zumindest weniger komplex und überschaubarer als ein Physiksystem, das ein Rendering-System aufruft, nachdem es fertig ist. Aber hier müssen zwangsläufig einige Funktionen aufgerufen werden, und vorzugsweise sind sie abstrakt.

Sie können also eine abstrakte Schnittstelle für ein System mit einer abstrakten updateFunktion erstellen . Es kann sich dann bei der zentralen Engine registrieren und Ihr Netzwerksystem kann sagen: "Hey, ich bin ein System und hier ist meine Update-Funktion. Bitte rufen Sie mich von Zeit zu Zeit an." Und dann kann Ihre Engine alle diese Systeme durchlaufen und sie aktualisieren, ohne Funktionsaufrufe an bestimmte Systeme fest zu codieren.

Dadurch können Ihre Systeme mehr wie in ihrer eigenen isolierten Welt leben. Die Spiel-Engine muss sie nicht mehr speziell (auf konkrete Weise) kennen. Und dann wird möglicherweise die Aktualisierungsfunktion Ihres Physiksystems aufgerufen. Zu diesem Zeitpunkt gibt es die Daten ein, die es für die Bewegung aller Elemente aus der zentralen Datenbank benötigt, wendet die Physik an und gibt die resultierende Bewegung zurück.

Danach wird möglicherweise die Aktualisierungsfunktion Ihres Netzwerksystems aufgerufen. Zu diesem Zeitpunkt gibt es die benötigten Daten aus der zentralen Datenbank ein und gibt beispielsweise Socket-Daten an Clients aus. Aus meiner Sicht ist es wieder das Ziel, jedes System so weit wie möglich zu isolieren, damit es in seiner eigenen kleinen Welt mit minimalem Wissen über die Außenwelt leben kann. Dies ist im Grunde der in ECS verfolgte Ansatz, der bei Game-Engines beliebt ist.

ECS

Ich denke, ich sollte ECS ein wenig behandeln, da sich viele meiner obigen Gedanken um ECS drehen und versuchen zu rationalisieren, warum dieser datenorientierte Ansatz zur Entkopplung die Wartung so viel einfacher gemacht hat als die objektorientierten und COM-basierten Systeme, die ich gewartet habe in der Vergangenheit trotz der Verletzung von fast allem, was ich ursprünglich für heilig hielt, was ich über SE gelernt habe. Es kann auch sehr sinnvoll für Sie sein, wenn Sie versuchen, größere Spiele zu entwickeln. ECS funktioniert also folgendermaßen:

Geben Sie hier die Bildbeschreibung ein

Und wie im obigen Diagramm MovementSystemkönnte die updateFunktion des aufgerufen werden. Zu diesem Zeitpunkt wird möglicherweise die zentrale Datenbank nach PosAndVelocityKomponenten als einzugebende Daten abgefragt (Komponenten sind nur Daten, keine Funktionalität). Dann kann es diese durchlaufen, die Positionen / Geschwindigkeiten ändern und die neuen Ergebnisse effektiv ausgeben. Dann wird RenderingSystemmöglicherweise die Aktualisierungsfunktion aufgerufen. Zu diesem Zeitpunkt fragt sie die Datenbank nach PosAndVelocityund nach SpriteKomponenten ab und gibt basierend auf diesen Daten Bilder auf dem Bildschirm aus.

Alle Systeme sind sich der Existenz des anderen überhaupt nicht bewusst und müssen nicht einmal verstehen, was a Carist. Sie müssen nur bestimmte Komponenten des Interesses jedes Systems kennen, aus denen die Daten bestehen, die für die Darstellung einer Komponente erforderlich sind. Jedes System ist wie eine Black Box. Es gibt Daten ein und gibt Daten mit minimalem Wissen über die Außenwelt aus, und die Außenwelt hat auch nur minimales Wissen darüber. Es kann vorkommen, dass ein Ereignis von einem System aus verschoben und von einem anderen entfernt wird, sodass beispielsweise bei der Kollision zweier Entitäten im Physiksystem im Audio ein Kollisionsereignis angezeigt wird, bei dem Ton abgespielt wird, die Systeme jedoch möglicherweise nicht wahrgenommen werden über einander. Und ich fand solche Systeme so viel einfacher zu überlegen. Sie lassen mein Gehirn nicht explodieren, selbst wenn Sie Dutzende von Systemen haben, weil jedes so isoliert ist. Du ziehst nicht Sie müssen nicht über die Komplexität von allem als Ganzes nachdenken, wenn Sie hineinzoomen und an einem bestimmten arbeiten. Aus diesem Grund ist es auch sehr einfach, die Ergebnisse Ihrer Änderungen vorherzusagen.

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.