Versuche ich hier den Unterricht zu sehr zu entkoppeln?


8

Ich arbeite an einer Echtzeit-Terrain-Rendering-Engine. Ich habe eine QuadTree- und eine Node-Klasse. Die QuadTree-Klasse erweitert / reduziert einen Baum je nachdem, wo sich die Kamera befindet. Daher ist es sinnvoll, dass der QuadTree für die Lebensdauer der Knotenobjekte verantwortlich ist. Das Problem ist, dass es viele Daten gibt, die mit dieser Lebensdauer verknüpft werden müssen und nichts mit dem QuadTree zu tun haben. Diese Daten sind möglicherweise nicht einmal miteinander verknüpft. Ich habe nach einem sauberen Weg gesucht, um meine Klassen ohne Erfolg richtig zu entkoppeln. Jede Änderung (manchmal sogar geringfügige) erforderte Änderungen über mehrere Dateien, meistens mit Dateien, die nicht damit zusammenhängen. Ich habe endlich etwas, das zu funktionieren scheint, aber ich glaube, ich habe versucht, es zu stark zu entkoppeln, was die Komplexität für nicht viele Vorteile erhöht hat. So habe ich es gemacht:

Meine QuadTree-Klasse sollte sich nur mit dem Baum befassen. Aber jedes Mal, wenn ich einen Knoten erstelle, muss ich diesen Knoten Daten zuordnen. Da dies die Klasse in der QuadTree-Klasse verschmutzen würde, habe ich eine Schnittstelle für die Kommunikation zwischen dem QuadTree und der Klasse hinzugefügt, deren Aufgabe es ist, diese Daten zu erstellen. An diesem Punkt denke ich, dass ich es richtig gemacht habe. Pseudocode:

class QTInterface
{
    virtual void nodeCreated( Node& node ) = 0;
    virtual void nodeDestroyed( Node& node ) = 0;
};

class QuadTree
{
public:
    QuadTree( ...., QTInterface& i ) : i( i ) {}

    void update( Camera camera )
    {
        // ....
        i.nodeCreated( node );
        // ....
        i.nodeDestroyed( node );
    }

private:
    QTInterface& i;
    Node root;
};

Jetzt muss ich jedem dieser Knoten einige zufällige Daten zuordnen. In meiner Klasse, die QTInterface implementiert, habe ich eine Map, die genau das tut:

class Terrain : public QTInterface
{
    void nodeCreated( Node node )
    {
        Data data;
        // ... create all the data associated to this node
        map[ node ] = data
        // One more thing, The QuadTree actually needs one field of Data to continue, so I fill it there
        node.xxx = data.xxx
    }

    void nodeDestroyed( Node node )
    {
        // ... destroy all the data associated to this node
        map.erase( node );
    }

};

Node und QuadTree sind jetzt unabhängig von anderen Teilen des Codes. Wenn ich dorthin zurückkehren muss, liegt dies nur daran, dass ich etwas am QuadTree-Algorithmus ändern muss.

Aber da habe ich wohl mein erstes Problem. Die meiste Zeit mache ich mir keine Gedanken über die Optimierung, bis ich sie sehe, aber ich denke, wenn ich diese Art von Overhead hinzufügen muss, um meine Klassen richtig zu entkoppeln, liegt das daran, dass das Design Fehler aufweist.

Ein weiteres Problem dabei ist, dass die dem Knoten zugeordneten Daten eine Menge Daten enthalten. Für die gleichen Ergebnisse mit weniger Schmerzen hätte ich die Node-Klasse einfach als Tasche verwenden können.

Also hier einige Fragen:

  • Überkompliziere ich Dinge? Hätte ich die Node-Klasse nur erweitern sollen, damit sie zu einer Tüte mit Daten wird, die von einigen Klassen verwendet werden?

  • Wenn nein, wie gut ist meine Alternative? Gibt es einen besseren Weg?

  • Es fällt mir immer schwer, meine Klassen richtig zu entkoppeln. Irgendwelche Ratschläge, die ich später verwenden könnte? (Wie zum Beispiel, welche Fragen muss ich mir stellen oder wie verarbeitest du? Wenn ich auf dem Papier darüber nachdenke, erscheint mir das sehr abstrakt, und wenn ich sofort etwas codiere, führt dies zu einem späteren Refactoring.)

Hinweis: Ich habe versucht, das Problem so weit wie möglich zu vereinfachen, um eine sehr lange Frage mit unnötigen Details zu vermeiden. Ich hoffe, ich habe wichtige nicht ausgelassen.

Bearbeiten: Einige Details wurden gefragt:

Die Kamera kann nicht nur die sichtbaren Knoten auswählen, da dies bedeuten würde, dass ich alle Knoten im Speicher behalten muss. Dies ist nicht möglich, da die Engine sehr große Terrains mit einer hohen Auflösung rendern soll. Die Tiefe des Baumes würde leicht 25+ betragen. Abgesehen davon ist es auch einfacher zu wissen, wann neue Knoten erstellt / zerstört wurden (im Grunde genommen einfacher: Wenn der Knoten keine untergeordneten Knoten hat und die Tiefe nicht 0 ist, liegt dies daran, dass der Knoten erstellt werden muss, wenn der Knoten dies hat Kinder und der Algorithmus stoppt dort, es bedeutet, dass sie den Frame vorher sichtbar waren, aber nicht jetzt, also muss ich sie löschen und die Daten daran binden).

Ein Beispiel für Daten, die berechnet werden müssen, ist die Höhe und die Normalen dieser Knoten ( https://en.wikipedia.org/wiki/Heightmap und https://en.wikipedia.org/wiki/Normal_mapping ).

Das Erstellen dieser Daten umfasst:

  • Senden von vom QuadTree berechneten Knotendaten an eine Multithread-Workqueue
  • Aktualisieren Sie nach der Erstellung der Höhenkarte das einzige Feld des Knotens, das QuadTree benötigt, um den Algorithmus fortzusetzen: Die minimale / maximale Höhe.
  • Aktualisieren Sie anschließend die GPU-Texturen mithilfe der auf der CPU berechneten Höhen- und Normalkarte.

Dies ist jedoch nur die Art und Weise, die Daten zu berechnen. Ich kann es auch auf der GPU tun, und es werden ganz andere Schritte erforderlich sein. Und das ist ein Grund, warum ich es von der QuadTree-Klasse entkoppeln möchte, weil ich einfach (zu Testzwecken) zwischen den beiden wechseln möchte, ohne meinen gesamten Code umgestalten zu müssen. Designkupplung


1
Warum erstellen und zerstören Sie ständig Knoten? Sollte die Kamera nicht einfach auswählen, welche Knoten des Baums angezeigt werden sollen? Können Sie auch ein konkretes Beispiel für die Daten geben, die Sie hinzufügen möchten?
null

Ich habe meinen Beitrag bearbeitet, um Ihre Fragen zu beantworten.
Aulaulz

Antworten:


2

Das dynamische Zuordnen und Trennen von Daten im laufenden Betrieb unabhängig von der Lebensdauer eines QT-Knotens, während die QT in Kombination mit der Kamera weiß, wann die Daten im laufenden Betrieb zugeordnet / getrennt werden sollten, ist etwas schwierig zu verallgemeinern, und ich denke, Ihre Lösung ist eigentlich nicht schlecht. Das ist eine schwierige Sache, gegen die man auf sehr schöne und allgemeine Weise vorgehen kann. wie "äh ... teste es gut und versende es!" Okay, ein kleiner Witz. Ich werde versuchen, einen Gedankengang zum Erkunden anzubieten. Eines der Dinge, die mich am meisten beeindruckten, war hier:

void nodeCreated(Node& node)
{
    ...
    // One more thing, The QuadTree actually needs one field of 
    // Data to continue, so I fill it there
    node.xxx = data.xxx
}

Dies sagt mir, dass ein Knotenreferenz / Zeiger nicht nur als Schlüssel für einen externen assoziativen Container verwendet wird. Sie greifen tatsächlich auf Interna des Quadtree-Knotens außerhalb des Quadtree selbst zu und ändern diese. Und es sollte einen ziemlich einfachen Weg geben, dies zumindest für den Anfang zu vermeiden. Wenn dies der einzige Ort ist, an dem Sie Knoteninternale außerhalb des Quadtree ändern, können Sie dies möglicherweise tun (sagen wir, es xxxhandelt sich um ein Paar Floats):

std::pair<float, float> nodeCreated(const Node& node)
{
    Data data;
    ...
    map[&node] = data;
    ...
    return data.xxx;
}

An diesem Punkt kann der Quadtree den Rückgabewert dieser Funktion zum Zuweisen verwenden xxx. Dadurch wird die Kopplung bereits erheblich gelockert, wenn Sie nicht mehr auf die Interna eines Baumknotens außerhalb des Baums zugreifen.

Wenn Sie nicht Terrainmehr auf Quadtree-Interna zugreifen müssen, ist der einzige Ort, an dem Sie Dinge sehr unnötig koppeln, überflüssig. Dies ist die einzige echte PITA, wenn Sie beispielsweise Dinge mit einer GPU-Implementierung austauschen, da die GPU-Implementierung möglicherweise einen völlig anderen internen Repräsentanten für Knoten verwendet.

Aber für Ihre Leistungsbedenken, und da habe ich viel mehr Gedanken darüber, wie Sie mit solchen Dingen maximal eine Entkopplung erreichen, würde ich tatsächlich eine ganz andere Darstellung vorschlagen, bei der Sie die Datenzuordnung / -zuordnung in eine billige Operation mit konstanter Zeit verwandeln können. Es ist ein bisschen schwierig, jemandem zu erklären, der nicht daran gewöhnt ist, Standardcontainer zu erstellen, für die eine neue Platzierung erforderlich ist, um Elemente aus dem Poolspeicher zu erstellen. Daher beginne ich mit einigen Daten:

struct Node
{
    ....
    // Stores an index to the data being associated on the fly
    // or -1 if there's no data associated to the node.
    int32_t data;
};

class Quadtree
{
private:
    // Stores all the data being associated on the fly.
    std::vector<char> data;

    // Stores the size of the data being associated on the fly.
    int32_t type_size;

    // Stores an index to the first free index of data
    // to reclaim or -1 if the free list is empty.
    int32_t free_index;

    ...

public:
    // Creates a quadtree with the specified type size for the
    // data associated and disassociated on the fly.
    explicit Quadtree(int32_t itype_size): type_size(itype_size), free_data(-1)
    {
        // Make sure our data type size is at least the size of an integer
        // as required for the free list.
        if (type_size < sizeof(int32_t))
            type_size = sizeof(int32_t);
    }

    // Inserts a buffer to store a data element and returns an index
    // to that.
    int32_t alloc_data()
    {
        int32_t index = free_index;
        if (free_index != -1)
        {
            // If a free index is available, pop it off the
            // free list (stack) and return that.
            void* mem = data.data() + index * type_size;
            free_index = *static_cast<int*>mem;
        }
        else
        {
            // Otherwise insert the buffer for the data
            // and return an index to that.
            index = data.size() / type_size;
            data.resize(data.size() + type_size);
        }
        return index;
    }

    // Frees the memory for the nth data element. 
    void free_data(int32_t n)
    {
        // Push the nth index to the free list to make
        // it available for use in subsequent insertions.
        void* mem = data.data() + n * type_size;
        *static_cast<int*>(mem) = free_index;
        free_index = n;
    }

    ...
};

Das ist im Grunde eine "indizierte freie Liste". Wenn Sie diesen Mitarbeiter für die zugehörigen Daten verwenden, können Sie Folgendes tun:

class QTInterface
{
    virtual std::pair<float, float> createData(void* mem) = 0;
    virtual void destroyData(void* mem) = 0;
};

void Quadtree::update(Camera camera)
{
    ...
    node.data = alloc_data();
    node.xxx = i.createData(data.data() + node.data * type_size);
    ...
    i.destroyData(data.data() + node.data * type_size);
    free_data(node.data);
    node.data = -1;
    ...
}

class Terrain : public QTInterface
{
    // Note that we don't even need access to nodes anymore,
    // not even as keys to use. We've completely decoupled
    // terrains from tree internals.
    std::pair<float, float> createData(void* mem) override
    {
        // Construct the data (placement new) using the memory
        // allocated by the tree.
        Data* data = new(mem) Data(...);

        // Return data to assign to node.xxx.
        return data->xxx;
    }

    void destroyData(void* mem) override
    {
        // Destroy the data.
        static_cast<Data*>(mem)->~Data();
    }
};

Hoffentlich macht dies alles Sinn, und natürlich ist es etwas entkoppelter von Ihrem ursprünglichen Design, da Clients keinen internen Zugriff auf Baumknotenfelder benötigen (es erfordert jetzt nicht einmal mehr Kenntnisse über Knoten, nicht einmal mehr als Schlüssel ), und es ist wesentlich effizienter, da Sie Daten in konstanter Zeit (und ohne Verwendung einer Hash-Tabelle, die eine viel größere Konstante implizieren würde) zu / von Knoten verknüpfen und trennen können. Ich hoffe, dass Ihre Daten mithilfe von max_align_t(z. B. ohne SIMD-Felder) ausgerichtet werden können und trivial kopierbar sind. Andernfalls werden die Dinge erheblich komplexer, da wir einen ausgerichteten Allokator benötigen und möglicherweise unseren eigenen kostenlosen Listencontainer rollen müssen. Nun, wenn Sie nur nicht trivial kopierbare Typen haben und nicht mehr als benötigenmax_align_tkönnen wir eine kostenlose Listenzeigerimplementierung verwenden, die nicht gerollte Knoten, die jeweils KDatenelemente speichern, bündelt und verknüpft , um zu vermeiden, dass vorhandene Speicherblöcke neu zugewiesen werden müssen. Ich kann das zeigen, wenn Sie eine solche Alternative brauchen.

Es ist etwas fortgeschritten und sehr C ++ - spezifisch, da die Idee, Speicher für Elemente zuzuweisen und freizugeben, eine separate Aufgabe ist, die vom Erstellen und Zerstören dieser Elemente getrennt ist. Wenn Sie dies jedoch auf diese Weise tun, übernehmen Sie Terraindie Mindestverantwortung und erfordern keinerlei interne Kenntnisse der Baumdarstellung mehr, auch nicht für undurchsichtige Knoten. Diese Ebene der Speichersteuerung ist jedoch in der Regel das, was Sie benötigen, um die effizientesten Datenstrukturen zu entwerfen.

Die Grundidee dort ist, dass der Client den Tree-Pass in der Typgröße der Daten verwendet, die er im laufenden Betrieb dem Quadtree-Ctor zuordnen / trennen möchte. Dann hat der Quadtree die Verantwortung, Speicher mit dieser Typgröße zuzuweisen und freizugeben. Anschließend wird die Verantwortung für die Erstellung und Zerstörung der Daten mithilfe eines QTInterfacedynamischen Versands an den Client weitergegeben . Die einzige Verantwortung außerhalb des Baums, der noch mit dem Baum zusammenhängt, besteht daher darin, Elemente aus dem Speicher zu konstruieren und zu zerstören, die der Quadtree selbst zuweist und aufhebt. An diesem Punkt werden die Abhängigkeiten wie folgt:

Geben Sie hier die Bildbeschreibung ein

Was angesichts der Schwierigkeit Ihrer Arbeit und der Größe der Eingaben sehr vernünftig ist. Grundsätzlich Terrainhängt Ihr dann nur noch von Quadtreeund ab QTInterfaceund nicht mehr von den Interna des Quadtree oder seiner Knoten. Zuvor hatten Sie Folgendes:

Geben Sie hier die Bildbeschreibung ein

Und natürlich ist ein eklatantes Problem dabei, insbesondere wenn Sie GPU-Implementierungen ausprobieren möchten , diese Abhängigkeit von Terrainbis Node, da eine GPU-Implementierung wahrscheinlich einen ganz anderen Knotenvertreter verwenden möchte. Wenn du Hardcore SOLID machen willst, machst du natürlich so etwas:

Geben Sie hier die Bildbeschreibung ein

... zusammen mit möglicherweise einer Fabrik. Aber IMO ist ein völliger Overkill (zumindest INodeist es ein totaler Overkill IMO) und wäre in einem so granularen Fall wie einer Quadtree-Funktion nicht sehr hilfreich, wenn jeder einen dynamischen Versand erfordern würde.

Es fällt mir immer schwer, meine Klassen richtig zu entkoppeln. Irgendwelche Ratschläge, die ich später verwenden könnte? (Wie zum Beispiel, welche Fragen muss ich mir stellen oder wie verarbeitest du? Wenn ich auf dem Papier darüber nachdenke, erscheint mir das sehr abstrakt, und wenn ich sofort etwas codiere, führt dies zu einem späteren Refactoring.)

Ganz allgemein und grob gesagt läuft die Entkopplung oft darauf hinaus, die Menge an Informationen zu begrenzen, die eine bestimmte Klasse oder Funktion über etwas anderes benötigt, um ihre Sache zu tun.

Ich gehe davon aus, dass Sie C ++ verwenden, da keine andere mir bekannte Sprache genau diese Syntax hat. In C ++ sind Klassenvorlagen mit statischem Polymorphismus ein sehr effektiver Entkopplungsmechanismus für Datenstrukturen, wenn Sie sie verwenden können. Wenn Sie die Standardcontainer wie betrachten std::vector<T, Alloc>, ist der Vektor nicht an das gekoppelt, was Sie für Tirgendetwas angegeben haben. Es ist nur erforderlich, dass Teinige grundlegende Schnittstellenanforderungen erfüllt werden, z. B. dass es kopierkonstruierbar ist und einen Standardkonstruktor für den Füllkonstruktor und die Größenänderung der Füllung hat. Und es werden niemals Änderungen aufgrund von TÄnderungen erforderlich sein .

Wenn Sie es mit dem oben Gesagten verknüpfen, können Sie die Datenstruktur mit dem absolut minimalen Wissen über das, was sie enthält, implementieren, und das entkoppelt sie in dem Maße, in dem sie im Voraus nicht einmal Typinformationen benötigt (hier ist der Fortschritt) Sprechen in Bezug auf Codeabhängigkeiten / Kopplung, nicht Informationen zur Kompilierungszeit) darüber, was Tist.

Der zweitpraktischste Weg, um die erforderliche Informationsmenge zu minimieren, ist die Verwendung eines dynamischen Polymorphismus. Wenn Sie beispielsweise eine einigermaßen verallgemeinerte Datenstruktur implementieren möchten, die das Wissen darüber, was gespeichert wird, minimiert, können Sie die Schnittstellenanforderungen für das, was in einer oder mehreren Schnittstellen gespeichert ist, erfassen:

// Contains all the functions (pure virtual) required of the elements 
// stored in the container.
class IElement {...};

In beiden Fällen geht es jedoch darum, die Menge an Informationen, die Sie im Voraus benötigen, zu minimieren, indem Sie eher auf Schnittstellen als auf konkrete Details codieren. Hier ist die einzige große Sache, die Sie tun, die viel mehr Informationen als erforderlich zu erfordern scheint, dass Sie Terrainvollständige Informationen über die Interna eines Quadtree-Knotens haben müssen, z. B. in einem solchen Fall, vorausgesetzt, der einzige Grund, den Sie benötigen, ist Um einem Knoten ein Datenelement zuzuweisen, können wir diese Abhängigkeit von den Interna eines Baumknotens leicht beseitigen, indem wir lediglich die Daten zurückgeben, die dem Knoten in dieser Zusammenfassung zugewiesen werden sollen QTInterface.

Wenn ich also etwas entkoppeln möchte, konzentriere ich mich nur auf das, was es braucht, um es zu tun, und erstelle eine Schnittstelle dafür (entweder explizit unter Verwendung von Vererbung oder implizit unter Verwendung von statischem Polymorphismus und Ententypisierung). Und Sie haben dies bereits zu einem gewissen Grad aus dem Quadtree selbst heraus getan, um QTInterfacees dem Client zu ermöglichen, seine Funktionen mit einem Subtyp zu überschreiben und die konkreten Details bereitzustellen, die erforderlich sind, damit der Quadtree seine Aufgabe erfüllt. Der einzige Ort, an dem Sie meiner Meinung nach zu kurz gekommen sind, ist, dass der Kunde weiterhin Zugriff auf die Interna des Quadtree benötigt. Sie können dies vermeiden, indem Sie erhöhen, was QTInterfacegenau das ist, was ich vorgeschlagen habe, als ich einen Wert zurückgab, dem zugewiesen werden sollnode.xxxin der quadtree Implementierung selbst. Es geht also nur darum, die Dinge abstrakter und die Schnittstellen vollständiger zu gestalten, damit die Dinge keine unnötigen Informationen über einander erfordern.

Und indem Sie diese unnötigen Informationen vermeiden (Sie Terrainmüssen sich mit QuadtreeKnoteninternalen auskennen), können Sie diese jetzt Quadtreebeispielsweise durch eine GPU-Implementierung austauschen, ohne auch die TerrainImplementierung zu ändern . Was Dinge nicht voneinander wissen, kann sich ändern, ohne sich gegenseitig zu beeinflussen. Wenn Sie wirklich GPU-Quadtree-Implementierungen von CPU-Implementierungen austauschen möchten, gehen Sie möglicherweise ein wenig in Richtung der obigen SOLID-Route mitIQuadtree(Den Quadtree selbst abstrakt machen). Dies ist mit einem dynamischen Versandtreffer verbunden, der mit der Baumtiefe und den Eingabegrößen, über die Sie sprechen, etwas teuer werden kann. Wenn nicht, sind zumindest weitaus weniger Änderungen am Code erforderlich, wenn die Dinge, die den Quadtree verwenden, nicht über die interne Knotendarstellung Bescheid wissen müssen, um zu funktionieren. Möglicherweise können Sie eine Datei mit der anderen austauschen, indem Sie nur eine einzelne Codezeile für a aktualisieren typedef, z. B. auch wenn Sie keine abstrakte Schnittstelle verwenden ( IQuadtree).

Aber da habe ich wohl mein erstes Problem. Die meiste Zeit mache ich mir keine Gedanken über die Optimierung, bis ich sie sehe, aber ich denke, wenn ich diese Art von Overhead hinzufügen muss, um meine Klassen richtig zu entkoppeln, liegt das daran, dass das Design Fehler aufweist.

Nicht unbedingt. Entkopplung bedeutet oft, eine Abhängigkeit vom Konkreten zum Abstrakten zu verschieben. Abstraktionen bedeuten in der Regel eine Laufzeitstrafe, es sei denn, der Compiler generiert zur Kompilierungszeit Code, um die Abstraktionskosten zur Laufzeit zu eliminieren. Im Gegenzug erhalten Sie viel mehr Spielraum, um Änderungen vorzunehmen, ohne andere Dinge zu beeinflussen. Dies führt jedoch häufig zu Leistungseinbußen, es sei denn, Sie verwenden die Codegenerierung.

Jetzt können Sie die Notwendigkeit einer nicht trivialen assoziativen Datenstruktur (Karte / Wörterbuch, dh) beseitigen, um Daten im laufenden Betrieb Knoten (oder etwas anderem) zuzuordnen. Im obigen Fall habe ich gerade die Knoten dazu gebracht, direkt einen Index für die Daten zu speichern, die im laufenden Betrieb zugewiesen / freigegeben werden. Diese Art von Dingen zu tun, hängt weniger mit dem Studium zusammen, wie man Dinge effektiv entkoppelt, als vielmehr damit, wie man Speicherlayouts für Datenstrukturen effektiv nutzt (mehr im Bereich der reinen Optimierung).

Effektive SE-Prinzipien und -Leistungen stehen auf ausreichend niedrigen Niveaus im Widerspruch zueinander. Häufig werden durch die Entkopplung die Speicherlayouts für Felder, auf die häufig gemeinsam zugegriffen wird, aufgeteilt, es können mehr Heap-Zuweisungen erforderlich sein, es kann sich um einen dynamischeren Versand handeln usw. Sie werden schnell trivialisiert, wenn Sie auf Code höherer Ebene hinarbeiten (z. B. Operationen, die auf ganze Bilder angewendet werden, nicht per -Pixeloperationen beim Durchlaufen einzelner Pixel), aber die Kosten reichen von trivial bis schwerwiegend, je nachdem, wie viel diese Kosten in Ihrem kritischsten, schleifenförmigen Code anfallen, der die leichteste Arbeit in jeder Iteration ausführt.

Überkompliziere ich Dinge? Hätte ich die Node-Klasse nur erweitern sollen, damit sie zu einer Tüte mit Daten wird, die von einigen Klassen verwendet werden?

Ich persönlich finde das nicht so schlimm, wenn Sie nicht versuchen, Ihre Datenstruktur zu stark zu verallgemeinern, sondern sie nur in einem sehr begrenzten Kontext verwenden und sich mit einem äußerst leistungskritischen Kontext für eine Art von Problem befassen, das Sie haben noch nicht in Angriff genommen. In diesem Fall würden Sie Ihren Quadtree in ein Implementierungsdetail Ihres Terrains verwandeln, z. B. in etwas, das weit verbreitet und öffentlich verwendet werden soll. Auf ähnliche Weise könnte jemand ein Octree in ein Implementierungsdetail seiner Physik-Engine verwandeln, indem er das nicht mehr unterscheidet Idee der "öffentlichen Schnittstelle" von "Interna". Das Verwalten von Invarianten in Bezug auf den räumlichen Index wird dann zur Verantwortung der Klasse, die ihn als privates Implementierungsdetail verwendet.

Um eine effektive Abstraktion (dh Schnittstelle) in einem leistungskritischen Kontext zu entwerfen, müssen Sie häufig den Großteil des Problems gründlich verstehen und eine sehr effektive Lösung dafür im Voraus finden. Es kann sich tatsächlich in eine kontraproduktive Maßnahme verwandeln, um zu versuchen, die Lösung zu verallgemeinern und zu abstrahieren, während gleichzeitig versucht wird, das effektive Design über mehrere Iterationen herauszufinden. Einer der Gründe ist, dass leistungskritische Kontexte sehr effiziente Datendarstellungen und Zugriffsmuster erfordern. Abstraktionen stellen eine Barriere zwischen dem Code dar, der auf die Daten zugreifen möchte: Eine Barriere, die nützlich ist, wenn Sie möchten, dass sich die Daten frei ändern können, ohne diesen Code zu beeinflussen, aber ein Hindernis, wenn Sie gleichzeitig versuchen, die effektivste Art der Darstellung herauszufinden und in erster Linie auf solche Daten zugreifen.

Aber wenn Sie es so machen, würde ich mich wieder irren, wenn ich den Quadtree in ein privates Implementierungsdetail Ihres Terrains verwandeln würde, das nicht außerhalb ihrer Implementierungen verallgemeinert und verwendet werden sollte. Und Sie müssten auf die Idee verzichten, GPU-Implementierungen so einfach von CPU-Implementierungen austauschen zu können, da dies normalerweise eine Abstraktion erfordern würde, die für beide funktioniert und nicht direkt von den konkreten Details abhängt (wie Knotenwiederholungen). von beiden.

Der Punkt der Entkopplung

Aber vielleicht ist dies in einigen Fällen sogar für öffentlich genutzte Dinge akzeptabel. Bevor die Leute denken, dass ich verrückten Unsinn ausspucke, sollten Sie Bildschnittstellen in Betracht ziehen. Wie viele davon würden für einen Videoprozessor ausreichen, der Bildfilter in Echtzeit auf das Video anwenden muss, wenn das Bild seine Interna nicht verfügbar macht (direkter Zugriff auf das zugrunde liegende Pixelarray in einem bestimmten Pixelformat)? Es gibt keine, von denen ich weiß, dass sie getPixelhier und da so etwas wie abstrakt / virtuell verwendensetPixelDort werden Pixelformatkonvertierungen pro Pixel durchgeführt. In ausreichend leistungskritischen Kontexten, in denen Sie auf sehr detaillierte Ebenen zugreifen müssen (pro Pixel, pro Knoten usw.), müssen Sie möglicherweise manchmal die Interna der zugrunde liegenden Struktur verfügbar machen. Aber zwangsläufig müssen Sie die Dinge zwangsläufig eng miteinander verbinden, und es wird dann nicht einfach sein, die zugrunde liegende Darstellung von Bildern (z. B. Änderung des Bildformats) sozusagen zu ändern, ohne dass sich dies auf alles auswirkt, was auf die zugrunde liegenden Pixel zugreift. In diesem Fall kann es jedoch weniger Gründe für Änderungen geben, da es möglicherweise einfacher ist, die Datendarstellung zu stabilisieren als die abstrakte Schnittstelle. Ein Videoprozessor könnte sich möglicherweise auf die Idee einigen, 32-Bit-RGBA-Pixelformate zu verwenden, und diese Entwurfsentscheidung könnte sich über Jahre hinweg nicht ändern.

Idealerweise möchten Sie, dass die Abhängigkeiten in Richtung Stabilität fließen (unveränderliche Dinge), da das Ändern von Elementen mit vielen Abhängigkeiten die Kosten mit der Anzahl der Abhängigkeiten vervielfacht. Das können in allen Fällen Abstraktionen sein oder auch nicht. Das ignoriert natürlich die Vorteile von Informationen, die sich bei der Aufrechterhaltung von Invarianten verstecken, aber unter dem Gesichtspunkt der Kopplung besteht der Hauptpunkt der Entkopplung darin, die Änderungen kostengünstiger zu gestalten. Das bedeutet, Abhängigkeiten von Dingen, die sich ändern könnten, auf Dinge umzuleiten, die sich nicht ändern, und das hilft nicht im geringsten, wenn Ihre abstrakten Schnittstellen die sich am schnellsten ändernden Teile Ihrer Datenstruktur sind.

Wenn Sie dies aus Kopplungssicht zumindest geringfügig verbessern möchten, trennen Sie Ihre Knotenteile, auf die Clients zugreifen müssen, von Teilen, die dies nicht tun. Ich gehe davon aus, dass Clients zumindest die Links des Knotens nicht aktualisieren müssen, sodass die Links nicht verfügbar gemacht werden müssen. Sie sollten zumindest in der Lage sein, ein Wertaggregat zu erstellen, das von der Gesamtheit der Knoten, auf die Clients zugreifen / die sie ändern können, getrennt ist NodeValue.


3

Wenn Sie zwischen den Zeilen lesen, scheinen Sie sich zu sehr auf Ihre Baumansicht zu konzentrieren. Ihre Idee scheint zu sein: "Ich habe diesen Baum mit Knoten, an die ich Objekte anhänge, und der Baum muss den Objekten sagen, was zu tun ist." Es sollte jedoch umgekehrt sein. Schließlich ist der Baum nur eine Ansicht, die Ihren (Problemdomänen-) Objekten folgen sollte. Die Objekte sollten keine Kenntnisse / Spuren des Baums (Knoten) haben. Es ist die Ansicht, die die Objekte liest und sich entsprechend präsentiert.

Möglicherweise möchten Sie einige Ereignisse für Ihre Objekte implementieren, die der Baum abonnieren kann, damit er weiß, wann ein Knoten reduziert, erweitert, erstellt oder gelöscht werden muss.

Lassen Sie den Baum also Ihrem Modell folgen.

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.