Wie vermeide ich "Manager" in meinem Code?

26

Ich entwerfe gerade mein Entity System für C ++ neu und habe viele Manager. In meinem Design habe ich diese Klassen, um meine Bibliothek zusammenzubinden. Ich habe viele schlechte Dinge gehört, wenn es um "Manager" -Klassen geht. Vielleicht benenne ich meine Klassen nicht richtig. Ich habe jedoch keine Ahnung, wie ich sie sonst benennen soll.

Die meisten Manager in meiner Bibliothek setzen sich aus den folgenden Klassen zusammen (die allerdings ein wenig variieren):

  • Container - Ein Container für Objekte im Manager
  • Attribute - Attribute für die Objekte im Manager

In meinem neuen Design für meine Bibliothek habe ich diese spezifischen Klassen, um meine Bibliothek zusammenzubinden.

  • ComponentManager - verwaltet Komponenten im Entity System

    • ComponentContainer
    • ComponentAttributes
    • Szene * - ein Verweis auf eine Szene (siehe unten)
  • SystemManager - verwaltet Systeme im Entitätssystem

    • SystemContainer
    • Szene * - ein Verweis auf eine Szene (siehe unten)
  • EntityManager - verwaltet Entitäten im Entitätssystem

    • EntityPool - ein Pool von Entitäten
    • EntityAttributes - Attribute einer Entität (nur für ComponentContainer- und System-Klassen verfügbar)
    • Szene * - ein Verweis auf eine Szene (siehe unten)
  • Szene - bindet alle Manager zusammen

    • ComponentManager
    • Systemmanager
    • EntityManager

Ich dachte nur daran, alle Container / Pools in die Scene-Klasse selbst zu setzen.

dh

An Stelle von:

Scene scene; // create a Scene

// NOTE:
// I technically could wrap this line in a createEntity() call in the Scene class
Entity entity = scene.getEntityManager().getPool().create();

Es wäre dies:

Scene scene; // create a Scene

Entity entity = scene.getEntityPool().create();

Aber ich bin mir nicht sicher. Wenn ich Letzteres machen würde, würde das bedeuten, dass ich viele Objekte und Methoden in meiner Scene-Klasse deklariert hätte.

ANMERKUNGEN:

  1. Ein Entitätssystem ist einfach ein Design, das für Spiele verwendet wird. Es besteht aus drei Hauptteilen: Komponenten, Entitäten und Systemen. Die Komponenten sind einfach Daten, die den Entitäten "hinzugefügt" werden können, damit die Entitäten unterscheidbar sind. Eine Entität wird durch eine ganze Zahl dargestellt. Systeme enthalten die Logik für eine Entität mit bestimmten Komponenten.
  2. Der Grund, warum ich mein Design für meine Bibliothek ändere, ist, dass ich denke, dass es ziemlich viel geändert werden kann. Ich mag das Gefühl / Fließen momentan nicht.
miguel.martin
quelle
Können Sie bitte die schlechten Dinge erläutern, die Sie über die Manager gehört haben, und wie sie Sie betreffen?
MrFox
@ MrFox Grundsätzlich habe ich gehört, was Dunk erwähnt hat, und ich habe viele * Manager-Klassen in meiner Bibliothek, die ich loswerden möchte.
Miguel.Martin
Dies könnte auch hilfreich sein: medium.com/@wrong.about/…
Zapadlo
Sie faktorisieren ein nützliches Framework aus einer funktionierenden Anwendung. Es ist fast unmöglich, ein nützliches Framework für imaginäre Anwendungen zu schreiben.
Kevin Cline

Antworten:

19

DeadMG ist genau auf die Besonderheiten Ihres Codes abgestimmt, aber ich habe das Gefühl, es fehlt eine Klärung. Ich bin auch nicht mit einigen seiner Empfehlungen einverstanden, die in bestimmten Kontexten, wie zum Beispiel der Entwicklung der meisten Hochleistungs-Videospiele, nicht zutreffen. Aber er hat global Recht, dass der Großteil Ihres Codes momentan nicht nützlich ist.

Wie Dunk sagt, werden Manager-Klassen so genannt, weil sie Dinge "verwalten". "manage" ist wie "data" oder "do", es ist ein abstraktes Wort, das fast alles enthalten kann.

Ich befand mich vor ungefähr 7 Jahren größtenteils am selben Ort wie Sie, und ich begann zu glauben, dass etwas in meiner Denkweise nicht stimmte, weil es so viel Mühe gab, Code zu schreiben, um noch nichts zu tun.

Was ich geändert habe, um dies zu beheben, ist das Ändern des Vokabulars, das ich in Code verwende. Ich vermeide generische Wörter vollständig (es sei denn, es handelt sich um generischen Code, aber es ist selten, wenn Sie keine generischen Bibliotheken erstellen). Ich vermeide es, einen Typ "Manager" oder "Objekt" zu nennen.

Die Auswirkung ist direkt im Code: Sie werden gezwungen, das richtige Wort zu finden, das der tatsächlichen Verantwortung Ihres Typs entspricht. Wenn Sie der Meinung sind, dass der Typ mehrere Aufgaben erfüllt (einen Index der Bücher führen, sie am Leben erhalten, Bücher erstellen / zerstören), benötigen Sie verschiedene Typen, für die jeweils eine Verantwortung besteht, und kombinieren Sie sie in dem Code, der sie verwendet. Manchmal brauche ich eine Fabrik, manchmal nicht. Manchmal brauche ich eine Registrierung, also richte ich eine ein (unter Verwendung von Standardcontainern und intelligenten Zeigern). Manchmal brauche ich ein System, das aus mehreren unterschiedlichen Untersystemen besteht, damit ich alles so weit wie möglich voneinander trenne, damit jeder Teil etwas Nützliches bewirkt.

Nennen Sie niemals einen Typ "Manager" und stellen Sie sicher, dass alle Ihre Typen eine eindeutige Rolle haben. Dies ist meine Empfehlung. Es mag schwierig sein, irgendwann Namen zu finden, aber es ist eine der schwierigsten Aufgaben bei der Programmierung im Allgemeinen

Klaim
quelle
Wenn ein paar dynamic_castIhre Leistung beeinträchtigen, verwenden Sie das statische Typsystem - dafür ist es gedacht. Es ist wirklich ein spezieller Kontext, kein allgemeiner, um eine eigene RTTI zu erstellen, wie es LLVM getan hat. Sogar dann tun sie das nicht.
DeadMG
@DeadMG Ich habe nicht über diesen Teil im Besonderen gesprochen, aber die meisten Hochleistungsspiele können es sich nicht leisten, beispielsweise dynamische Zuweisungen durch neue oder sogar andere Dinge vorzunehmen, die für die Programmierung im Allgemeinen in Ordnung sind. Es handelt sich dabei um bestimmte Bestien für bestimmte Hardware, die Sie nicht verallgemeinern können, weshalb "es kommt darauf an" eine allgemeinere Antwort ist, insbesondere mit C ++. (Keiner in der Spielentwicklung verwendet übrigens Dynamic Cast, es ist nie nützlich, bis Sie Plugins haben, und selbst dann ist es selten).
Klaim
@DeadMG Ich verwende weder dynamic_cast noch eine Alternative zu dynamic_cast. Ich habe es in der Bibliothek implementiert, aber es hat mich nicht gestört, es herauszunehmen. template <typename T> class Class { ... };dient eigentlich zum Zuweisen von IDs für verschiedene Klassen (nämlich benutzerdefinierte Komponenten und benutzerdefinierte Systeme). Die ID-Zuweisung wird zum Speichern von Komponenten / Systemen in einem Vektor verwendet, da eine Karte möglicherweise nicht die Leistung erbringt, die ich für ein Spiel benötige.
Miguel.Martin
Nun, ich habe auch keine Alternative zu dynamic_cast implementiert, nur eine gefälschte type_id. Trotzdem wollte ich nicht, was type_id erreicht.
Miguel.Martin
"Finde das richtige Wort, das der tatsächlichen Verantwortung deines Typs entspricht" - obwohl ich dem vollkommen zustimme, gibt es oft nicht ein einziges Wort (wenn überhaupt), das die Entwicklergemeinschaft für eine bestimmte Art von Verantwortung vereinbart hat.
Bytedev
23

Nun, ich habe einige der Codes, mit denen Sie verlinkt sind, und Ihren Beitrag durchgelesen, und meine ehrliche Zusammenfassung lautet, dass der Großteil davon im Grunde genommen völlig wertlos ist. Es tut uns leid. Ich meine, Sie haben all diesen Code, aber Sie haben nichts erreicht . Überhaupt. Ich werde hier etwas tiefer gehen müssen, also nimm mich in Acht.

Beginnen wir mit ObjectFactory . Erstens Funktionszeiger. Diese sind zustandslos. Der einzige nützliche zustandslose Weg, ein Objekt zu erstellen, ist, newund Sie brauchen dafür keine Factory. Das zustandsbehaftete Erstellen eines Objekts ist nützlich, und Funktionszeiger sind für diese Aufgabe nicht nützlich. Und zweitens muss jedes Objekt wissen, wie es sich selbst zerstört , ohne dass es genau die Instanz finden muss, die es erschafft, um es zu zerstören. Außerdem müssen Sie einen intelligenten Zeiger und keinen unformatierten Zeiger zurückgeben, um die Ausnahmebedingung und die Ressourcensicherheit Ihres Benutzers zu gewährleisten. Ihre gesamte ClassRegistryData-Klasse ist nur std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, aber mit den oben genannten Löchern. Es muss überhaupt nicht existieren.

Dann haben wir AllocatorFunctor- gibt es bereits Standard - Allocator Schnittstellen provided- nicht zu erwähnen , dass sie nur Wrapper auf delete obj;und return new T;- was wiederum, ist bereits vorgesehen, und sie werden nicht interact mit irgendwelchen Stateful oder benutzerdefinierte Speicherverteilern , die vorhanden sind .

Und ClassRegistry ist einfach, std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>aber Sie haben eine Klasse dafür geschrieben, anstatt die vorhandene Funktionalität zu verwenden. Es ist der gleiche, aber völlig unnötig globale, wandelbare Zustand mit einer Menge beschissener Makros.

Was ich hier sage, ist, dass Sie einige Klassen erstellt haben, in denen Sie das gesamte Ding durch Funktionen ersetzen konnten, die aus Standardklassen erstellt wurden, und diese dann noch schlimmer machten, indem Sie sie zu einem global wandelbaren Zustand machten und eine Reihe von Makros einschlossen, was das ist das Schlimmste, was du tun könntest.

Dann haben wir Identifizierbar . Es ist hier eine Menge der gleichen Geschichte - es übernimmt std::type_infobereits die Aufgabe, jeden Typ als einen Laufzeitwert zu identifizieren, und es erfordert keine übermäßige Heizplatte seitens des Benutzers.

    template<typename T, typename Y>
    bool isSameType(const X& x, const Y& y) { return typeid(x) == typeid(y); }
    template <class Type, class T>
    bool isType(const T& obj)
    {
            return dynamic_cast<const Type*>(std::addressof(obj));
    }

Problem gelöst, aber ohne die vorhergehenden zweihundert Zeilen von Makros und so weiter. Dies kennzeichnet auch Ihr Identifizierbares Array als nichts anderes als, std::vectoraber Sie müssen die Referenzzählung verwenden, auch wenn eindeutiges Eigentum oder Nicht-Eigentum besser wäre.

Oh, und habe ich erwähnt, dass Types eine Menge absolut nutzloser Typedefs enthält? Sie gewinnen buchstäblich keinen Vorteil gegenüber der direkten Verwendung der Rohtypen und verlieren einen guten Teil der Lesbarkeit.

Als nächstes ist ComponentContainer . Sie können den Eigentümer nicht auswählen, Sie können keine separaten Container für abgeleitete Klassen verschiedener Komponenten haben, Sie können keine eigene Lebensdauersemantik verwenden. Löschen Sie ohne Reue.

Jetzt ist der ComponentFilter möglicherweise ein wenig wert, wenn Sie ihn häufig überarbeitet haben. So etwas wie

class ComponentFilter {
    std::unordered_map<std::type_info, unsigned> types;
public:
    template<typename T> void SetTypeRequirement(unsigned count = 1) {
        types[typeid(T)] = count;
    }
    void clear() { types.clear(); }
    template<typename Iterator> bool matches(Iterator begin, Iterator end) {
        std::for_each(begin, end, [this](const std::iterator_traits<Iterator>::value_type& t) {
            types[typeid(t)]--;
        });
        return types.empty();
    }
};

Dies bezieht sich nicht auf Klassen, die von denjenigen abgeleitet sind, mit denen Sie sich befassen möchten, aber auch nicht auf Ihre.

Der Rest ist so ziemlich die gleiche Geschichte. Es gibt hier nichts, was ohne die Nutzung Ihrer Bibliothek nicht besser gemacht werden könnte.

Wie würden Sie Manager in diesem Code vermeiden? Löschen Sie im Grunde alles.

DeadMG
quelle
14
Ich habe eine Weile gebraucht, um zu lernen, aber die zuverlässigsten und fehlerfreiesten Teile des Codes sind diejenigen, die es nicht gibt .
Tacroy
1
Ich habe std :: type_info nicht verwendet, weil ich RTTI vermeiden wollte . Ich erwähnte, dass das Framework auf Spiele abzielt, was im Grunde der Grund ist, warum ich typeidoder nicht benutze dynamic_cast. Vielen Dank für das Feedback, ich weiß es zu schätzen.
Miguel.Martin
Basieren diese Kritiker auf Ihrem Wissen über Komponenten-Entity-System-Game-Engines oder handelt es sich um eine allgemeine C ++ - Codeüberprüfung?
Den
1
@ Dann denke ich eher an ein Designproblem als an irgendetwas anderes.
Miguel.Martin
10

Das Problem mit Manager-Klassen ist, dass ein Manager alles tun kann. Sie können den Klassennamen nicht lesen und wissen nicht, was die Klasse tut. EntityManager ... Ich weiß, dass es etwas mit Entities zu tun hat, aber wer weiß was. SystemManager ... ja, es verwaltet das System. Das ist sehr hilfreich.

Meine Vermutung ist, dass Ihre Manager-Klassen wahrscheinlich eine Menge Dinge tun. Ich wette, wenn Sie diese Dinge in eng verwandte funktionale Teile unterteilen würden, könnten Sie wahrscheinlich Klassennamen finden, die mit den funktionalen Teilen zusammenhängen, die jeder anhand des Klassennamens erkennen kann, was sie tun. Dies erleichtert die Wartung und das Verständnis des Designs erheblich.

Dunk
quelle
1
+1 und sie können nicht nur irgendetwas tun, sie werden es auf einer ausreichend langen Zeitachse tun. Die Leute betrachten den "Manager" -Deskriptor als eine Aufforderung, Spüle / Schweizer Taschenmesser-Klassen zu erstellen.
Erik Dietrich
@Erik - Ah ja, ich hätte hinzufügen sollen, dass ein guter Klassenname nicht nur wichtig ist, weil er jemandem sagt, was die Klasse tun kann; Ebenso sagt der Klassenname aber auch, was die Klasse nicht tun soll.
Dunk
3

Eigentlich finde ich Managerdas in diesem Zusammenhang gar nicht so schlecht, Achselzucken. Wenn es einen Ort gibt, den ich für Managerverzeihlich halte , dann für ein Entity-Component-System, da es wirklich die Lebensdauer von Komponenten, Entitäten und Systemen " verwaltet ". Zumindest ist das hier ein aussagekräftigerer Name als zum Beispiel, Factorywas in diesem Zusammenhang nicht so viel Sinn macht, da ein ECS tatsächlich ein bisschen mehr tut als Dinge zu erschaffen.

Ich nehme an, Sie könnten verwenden Database, wie ComponentDatabase. Oder Sie könnten nur verwenden Components, Systemsund Entities(das ist , was ich persönlich verwenden und vor allem , nur weil ich wie terse Identifikatoren obwohl es zugegebenermaßen vermitteln noch weniger Informationen, vielleicht als „Manager“).

Der eine Teil, den ich etwas ungeschickt finde, ist der folgende:

Entity entity = scene.getEntityManager().getPool().create();

Das ist eine Menge Arbeit, getbevor Sie eine Entität erstellen können, und es scheint, als ob beim Entwurf Implementierungsdetails verloren gehen, wenn Sie sich mit Entitätspools und "Managern" auskennen müssen, um sie zu erstellen. Ich denke, Dinge wie "Pools" und "Manager" (wenn Sie sich an diesen Namen halten) sollten Implementierungsdetails des ECS sein, nicht etwas, das dem Kunden angezeigt wird.

Aber weiß nich, habe ich einige sehr einfallslos und allgemeine Verwendungen gesehen Manager, Handler, Adapterund die Namen dieser Art, aber ich denke , das ist ein ziemlich verzeihlich Anwendungsfall ist , wenn das Ding namens „Manager“ ist eigentlich verantwortlich für die „Verwaltung“ ( "Controlling? "," Umgang mit? ") die Lebensdauer von Objekten, die für die Erstellung und Zerstörung von Objekten verantwortlich sind und den Zugriff auf diese Objekte individuell ermöglichen. Das ist zumindest für mich die einfachste und intuitivste Verwendung von "Manager".

Das heißt, eine allgemeine Strategie, um "Manager" in Ihrem Namen zu vermeiden, besteht darin, den "Manager" -Teil herauszunehmen und -sam Ende einen hinzuzufügen . :-D Nehmen wir zum Beispiel an, Sie haben ein ThreadManagerund es ist für das Erstellen von Threads und das Bereitstellen von Informationen über Threads und den Zugriff auf Threads usw. verantwortlich. Es werden jedoch keine Threads zusammengefasst, sodass wir es nicht aufrufen können ThreadPool. In diesem Fall nennst du es einfach Threads. Das mache ich sowieso, wenn ich einfallslos bin.

Ich erinnere mich irgendwie an damals, als das analoge Äquivalent von "Windows Explorer" "Dateimanager" hieß:

Bildbeschreibung hier eingeben

Und ich denke tatsächlich, dass "Datei-Manager" in diesem Fall aussagekräftiger ist als "Windows Explorer", selbst wenn es einen besseren Namen gibt, der nicht "Manager" beinhaltet. Bitte seien Sie also zumindest nicht zu schick mit Ihren Namen und beginnen Sie, Dinge wie "ComponentExplorer" zu benennen. "ComponentManager" gibt mir zumindest eine vernünftige Vorstellung davon, was das Ding macht, als jemand, der mit ECS-Motoren arbeitet.


quelle
Einverstanden. Wenn eine Klasse für typische Managementaufgaben verwendet wird (Erstellen ("Einstellen"), Zerstören ("Entlassen"), Versehen und "Angestellter" / höheres Interface), ist der Name Managerangemessen. Die Klasse hat möglicherweise Entwurfsprobleme , aber den Namen ist genau das.
Justin Time 2 Setzen Sie Monica