Comment éviter les «managers» dans mon code


26

Je suis en train de repenser mon système d'entité , pour C ++, et j'ai beaucoup de gestionnaires. Dans ma conception, j'ai ces classes, afin de lier ma bibliothèque. J'ai entendu beaucoup de mauvaises choses en ce qui concerne les classes "manager", peut-être que je ne nomme pas mes classes de manière appropriée. Cependant, je n'ai aucune idée quoi d'autre pour les nommer.

La plupart des gestionnaires, dans ma bibliothèque, sont composés de ces classes (bien que cela varie un peu):

  • Conteneur - un conteneur pour les objets dans le gestionnaire
  • Attributs - attributs pour les objets dans le gestionnaire

Dans mon nouveau design pour ma bibliothèque, j'ai ces classes spécifiques, afin de lier ma bibliothèque ensemble.

  • ComponentManager - gère les composants dans le système d'entité

    • ComponentContainer
    • ComponentAttributes
    • Scène * - une référence à une scène (voir ci-dessous)
  • SystemManager - gère les systèmes dans le système d'entité

    • SystemContainer
    • Scène * - une référence à une scène (voir ci-dessous)
  • EntityManager - gère les entités dans le système d'entités

    • EntityPool - un pool d'entités
    • EntityAttributes - attributs d'une entité (cela ne sera accessible qu'aux classes ComponentContainer et System)
    • Scène * - une référence à une scène (voir ci-dessous)
  • Scène - relie tous les managers

    • ComponentManager
    • SystemManager
    • EntityManager

Je pensais simplement mettre tous les conteneurs / pools dans la classe Scene elle-même.

c'est à dire

Au lieu de cela:

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();

Ce serait ceci:

Scene scene; // create a Scene

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

Mais je ne suis pas sûr. Si je devais faire ce dernier, cela signifierait que beaucoup d'objets et de méthodes seraient déclarés dans ma classe Scene.

REMARQUES:

  1. Un système d'entités est simplement une conception utilisée pour les jeux. Il est composé de 3 parties principales: composants, entités et systèmes. Les composants sont simplement des données, qui peuvent être "ajoutées" aux entités, afin que les entités soient distinctives. Une entité est représentée par un entier. Les systèmes contiennent la logique d'une entité, avec des composants spécifiques.
  2. La raison pour laquelle je modifie la conception de ma bibliothèque, c'est parce que je pense qu'elle peut être beaucoup modifiée, je n'aime pas le toucher / le flux pour le moment.

Pouvez-vous expliquer les mauvaises choses que vous avez entendues au sujet des gestionnaires et comment elles vous concernent?
MrFox


@MrFox En gros, j'ai entendu ce que Dunk a mentionné, et j'ai beaucoup de classes * Manager dans ma bibliothèque, dont je veux me débarrasser.
miguel.martin

Cela pourrait également être utile: medium.com/@wrong.about/…
Zapadlo

Vous factorisez un cadre utile à partir d'une application qui fonctionne. Il est presque impossible d'écrire un cadre utile pour les applications imaginées.
Kevin Cline

Réponses:


19

DeadMG est sur place sur les spécificités de votre code mais j'ai l'impression qu'il manque une clarification. De plus, je ne suis pas d'accord avec certaines de ses recommandations qui ne tiennent pas dans certains contextes spécifiques comme la plupart des développements de jeux vidéo haute performance par exemple. Mais il a globalement raison de dire que la plupart de votre code n'est pas utile pour le moment.

Comme le dit Dunk, les classes Manager sont appelées ainsi car elles "gèrent" les choses. "gérer" est comme "données" ou "faire", c'est un mot abstrait qui peut contenir presque n'importe quoi.

J'étais principalement au même endroit que vous il y a environ 7 ans, et j'ai commencé à penser qu'il y avait quelque chose qui n'allait pas dans ma façon de penser parce qu'il y avait tellement d'efforts à coder pour ne rien faire encore.

Ce que j'ai changé pour résoudre ce problème, c'est de changer le vocabulaire que j'utilise dans le code. J'évite totalement les mots génériques (sauf si c'est du code générique, mais c'est rare quand on ne fait pas de bibliothèques génériques). J'évite de nommer tout type « Manager » ou « objet ».

L'impact est direct dans le code: il vous oblige à trouver le bon mot correspondant à la vraie responsabilité de votre type. Si vous avez l'impression que le type fait plusieurs choses (garder un index des livres, les maintenir en vie, créer / détruire des livres), alors vous avez besoin de différents types qui auront chacun une responsabilité, puis vous les combinerez dans le code qui les utilise. Parfois j'ai besoin d'une usine, parfois non. Parfois, j'ai besoin d'un registre, j'en ai donc configuré un (en utilisant des conteneurs standard et des pointeurs intelligents). Parfois, j'ai besoin d'un système composé de plusieurs sous-systèmes différents, je sépare donc tout ce que je peux pour que chaque partie fasse quelque chose d'utile.

Ne nommez jamais un type «gestionnaire» et assurez-vous que tous vos types ont un seul rôle unique est ma recommandation. Il peut parfois être difficile de trouver des noms, mais c'est l'une des choses les plus difficiles à faire dans la programmation en général


Si un couple dynamic_casttue vos performances, utilisez le système de type statique, c'est à cela qu'il sert. C'est vraiment un contexte spécialisé, pas un contexte général, d'avoir à rouler votre propre RTTI comme LLVM l'a fait. Même alors, ils ne font pas ça.
DeadMG

@DeadMG Je ne parlais pas de cette partie en particulier, mais la plupart des jeux haute performance ne peuvent pas se permettre, par exemple, des allocations dynamiques via de nouveaux ou même d'autres trucs qui conviennent à la programmation en général. Ce sont des bêtes spécifiques pour du matériel spécifique que vous ne pouvez pas généraliser, c'est pourquoi "ça dépend" est une réponse plus générale, en particulier avec C ++. (personne dans le développement de jeux n'utilise la distribution dynamique d'ailleurs, ce n'est jamais utile tant que vous n'avez pas de plugins, et même alors c'est rare).
Klaim

@DeadMG Je n'utilise pas dynamic_cast, ni une alternative à dynamic_cast. Je l'ai implémenté dans la bibliothèque, mais cela ne me dérangeait pas de le retirer. template <typename T> class Class { ... };est en fait pour attribuer des ID pour différentes classes (à savoir les composants personnalisés et les systèmes personnalisés). L'attribution d'ID est utilisée pour stocker les composants / systèmes dans un vecteur, car une carte peut ne pas apporter les performances dont j'ai besoin pour un jeu.
miguel.martin

Eh bien, je n'ai pas non plus implémenté d'alternative à dynamic_cast, juste un faux type_id. Mais, je ne voulais toujours pas ce que type_id a réalisé.
miguel.martin

"trouver le bon mot correspondant à la responsabilité réelle de votre type" - bien que je sois totalement d'accord avec cela, souvent il n'y a pas un seul mot (le cas échéant) sur lequel la communauté des développeurs s'est mise d'accord pour un type de responsabilité particulier.
bytedev

23

Eh bien, j'ai lu quelques-uns des codes auxquels vous avez lié, et votre message, et mon honnête résumé est que la majorité d'entre eux est pratiquement sans valeur. Désolé. Je veux dire, vous avez tout ce code, mais vous n'avez rien obtenu . Du tout. Je vais devoir approfondir ici, alors soyez indulgent avec moi.

Commençons par ObjectFactory . Premièrement, les pointeurs de fonction. Ce sont des apatrides, le seul moyen apatride utile pour créer un objet est newet vous n'avez pas besoin d'une usine pour cela. La création avec état d'un objet est ce qui est utile et les pointeurs de fonction ne sont pas utiles pour cette tâche. Et d' autre part, chaque objet a besoin de savoir comment détruire lui - même , ne pas avoir à constater que par exemple la création exacte de le détruire. De plus, vous devez renvoyer un pointeur intelligent, pas un pointeur brut, pour l'exception et la sécurité des ressources de votre utilisateur. Toute votre classe ClassRegistryData est juste std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, mais avec les trous susmentionnés. Il n'a pas du tout besoin d'exister.

Ensuite, nous avons AllocatorFunctor - il y a déjà des interfaces d'allocateur standard fournies - Sans parler du fait qu'ils ne sont que des wrappers delete obj;et return new T;- ce qui est déjà fourni, et ils n'interagiront pas avec les allocateurs de mémoire avec état ou personnalisés qui existent.

Et ClassRegistry est tout simplement, std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>mais vous avez écrit une classe pour elle au lieu d'utiliser la fonctionnalité existante. C'est le même état mutable global mais inutilement global avec un tas de macros merdiques.

Ce que je dis ici, c'est que vous avez créé des classes où vous pouvez remplacer le tout par des fonctionnalités construites à partir de classes standard, puis les rendre encore pires en les rendant globalement mutables et en impliquant un tas de macros, ce qui est le pire chose que vous pourriez faire.

Ensuite, nous avons identifiable . C'est un peu la même histoire ici - std::type_infoeffectue déjà le travail d'identification de chaque type en tant que valeur d'exécution, et cela ne nécessite pas de passe-partout excessif de la part de l'utilisateur.

    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));
    }

Problème résolu, mais sans aucune des deux cents lignes de macros précédentes et ainsi de suite. Cela marque également votre tableau identifiable comme rien mais std::vectorvous devez utiliser le comptage des références même si la propriété unique ou la non-propriété serait préférable.

Oh, et ai-je mentionné que Types contient un tas de typedefs absolument inutiles ? Vous ne gagnez littéralement aucun avantage sur l'utilisation directe des types bruts et vous perdez une bonne partie de la lisibilité.

Ensuite, ComponentContainer . Vous ne pouvez pas choisir la propriété, vous ne pouvez pas avoir de conteneurs séparés pour les classes dérivées de divers composants, vous ne pouvez pas utiliser votre propre sémantique à vie. Supprimer sans remords.

Maintenant, le ComponentFilter pourrait en fait valoir un peu, si vous le refactorisiez beaucoup. Quelque chose comme

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();
    }
};

Cela ne concerne pas les classes dérivées de celles que vous essayez de gérer, mais la vôtre non plus.

Le reste est à peu près la même histoire. Il n'y a rien ici qui ne pourrait être fait beaucoup mieux sans l'utilisation de votre bibliothèque.

Comment éviteriez-vous les managers dans ce code? Supprimez pratiquement tout cela.


14
Il m'a fallu un certain temps pour apprendre, mais les éléments de code les plus fiables et sans bogue sont ceux qui ne sont pas là .
Tacroy

1
La raison pour laquelle je n'ai pas utilisé std :: type_info est que j'essayais d' éviter RTTI. J'ai mentionné que le cadre visait les jeux, ce qui est essentiellement la raison pour laquelle je n'utilise pas typeidou dynamic_cast. Merci pour vos commentaires, je l'apprécie.
miguel.martin

Est-ce que ces critiques sont basées sur votre connaissance des moteurs de jeu de système entité-composant ou s'agit-il d'une révision générale du code C ++?
Den

1
@ Den, je pense plus à un problème de conception qu'à toute autre chose.
miguel.martin

10

Le problème avec les classes Manager est qu'un manager peut tout faire. Vous ne pouvez pas lire le nom de la classe et savoir ce que fait la classe. EntityManager .... Je sais que cela fait quelque chose avec Entities mais qui sait quoi. SystemManager ... oui, il gère le système. C'est très utile.

Je suppose que vos classes de manager font probablement beaucoup de choses. Je parie que si vous segmentiez ces choses en éléments fonctionnels étroitement liés, vous pourrez probablement trouver des noms de classe liés aux éléments fonctionnels que n'importe qui peut dire ce qu'ils font simplement en regardant le nom de la classe. Cela rendra la conception et la maintenance beaucoup plus faciles.


1
+1 et non seulement peuvent-ils faire quoi que ce soit, sur une chronologie suffisamment longue, ils le feront. Les gens voient le descripteur "Manager" comme une invitation à créer des cours de cuisine-évier / couteau-suisse.
Erik Dietrich

@Erik - Ah oui, j'aurais dû ajouter qu'avoir un bon nom de classe n'est pas seulement important car il dit à quelqu'un ce que la classe peut faire; mais le nom de la classe indique également ce que la classe ne doit pas faire.
Dunk

3

En fait, je ne pense pas que ce Managersoit si mauvais dans ce contexte, haussement d'épaules. S'il y a un endroit où je pense qu'il Managerest pardonnable, ce serait pour un système entité-composant, car il « gère » vraiment la durée de vie des composants, des entités et des systèmes. À tout le moins, c'est un nom plus significatif ici que, disons,Factory qui n'a pas autant de sens dans ce contexte, car un ECS fait vraiment un peu plus que créer des choses.

Je suppose que vous pourriez utiliser Database, comme ComponentDatabase. Ou vous pourriez utiliser Components, Systemset Entities(ce que j'utilise personnellement et surtout parce que je comme identificateurs de laconiques mais il est vrai convoie encore moins d' informations, peut - être que « gestionnaire »).

La partie que je trouve un peu maladroite est la suivante:

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

C'est beaucoup de choses à get avant de pouvoir créer une entité et il semble que la conception fuit un peu les détails d'implémentation si vous devez connaître les pools d'entités et les "gestionnaires" pour les créer. Je pense que des choses comme "pools" et "managers" (si vous vous en tenez à ce nom) devraient être des détails d'implémentation de l'ECS, pas quelque chose exposé au client.

Mais je sais pas, j'ai vu de très imaginatifs et les utilisations génériques de Manager, Handler, Adapteret les noms de ce genre, mais je pense que cela est un cas d'utilisation raisonnable pardonnable lorsque la chose appelée « gestionnaire » est en fait responsable de la « gestion » ( "contrôle? "," manipuler? ") la durée de vie des objets, étant responsable de la création et de la destruction et de l'accès à ceux-ci individuellement. C'est l'utilisation la plus simple et la plus intuitive de "Manager" pour moi au moins.

Cela dit, une stratégie générale pour éviter «Manager» en votre nom consiste simplement à retirer la partie «Manager» et à en ajouter un -sà la fin. :-D Par exemple, disons que vous en avez un ThreadManageret qu'il est responsable de créer des threads et de fournir des informations à leur sujet et d'y accéder et ainsi de suite, mais il ne regroupe pas les threads, nous ne pouvons donc pas l'appeler ThreadPool. Eh bien, dans ce cas, vous l'appelez Threads. C'est ce que je fais de toute façon quand je n'ai pas d'imagination.

Je me rappelle un peu l'époque où l'équivalent analogique de "l'Explorateur Windows" s'appelait "Gestionnaire de fichiers", comme ceci:

entrez la description de l'image ici

Et je pense en fait que "Gestionnaire de fichiers" dans ce cas est plus descriptif que "Explorateur Windows" même s'il existe un meilleur nom qui n'implique pas "Gestionnaire". Donc, à tout le moins, ne soyez pas trop fantaisiste avec vos noms et commencez à nommer des choses comme "ComponentExplorer". "ComponentManager" me donne au moins une idée raisonnable de ce que cette chose fait en tant que personne habituée à travailler avec les moteurs ECS.


D'accord. Si une classe est utilisée pour des tâches de gestion typiques (création ("embauche"), destruction ("licenciement"), surveillance et interface "employé" / supérieur, le nom Managerest approprié. La classe peut avoir des problèmes de conception , mais le nom est sur place.
Justin Time 2 Monica Réintégrer
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.