web-dev-qa-db-fra.com

Comment éviter les "managers" dans mon code

Je suis en train de repenser mon Entity System , 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 correctement. 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 change le design de ma bibliothèque, c'est parce que je pense qu'il peut être beaucoup changé, je n'aime pas le toucher/le flux pour le moment.
27
miguel.martin

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 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 "manager" 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

19
Klaim

Eh bien, j'ai lu le code auquel vous avez lié et votre message, et mon honnête résumé est que la majorité de celui-ci 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 new et vous n'avez pas besoin d'une fabrique 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 deuxièmement, chaque objet doit savoir comment se détruire lui-même , sans avoir à trouver cette instance de création exacte pour le détruire. De plus, vous devez renvoyer un pointeur intelligent, et non un pointeur brut, pour la sécurité des exceptions et des ressources de votre utilisateur. Toute votre classe ClassRegistryData est simplement 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 oublier qu'ils ne sont que des wrappers sur delete obj; Et return new T; - qui, encore une fois, 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 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_info Effectue déjà le travail d'identification de chaque type en tant que valeur d'exécution, et il 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 IdentifiableArray comme rien d'autre que std::vector Mais vous 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é.

La prochaine étape est 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.

23
DeadMG

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.

10
Dunk

En fait, je ne pense pas que Manager soit si mauvais dans ce contexte, haussement d'épaules. S'il y a un endroit où je pense que Manager est pardonnable, ce serait pour un système à composants d'entité, car c'est vraiment " gérant "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 pouvez simplement utiliser Components, Systems et Entities (c'est ce que j'utilise personnellement et principalement parce que j'aime les identifiants laconiques bien qu'il transmette encore moins d'informations, peut-être , que "Manager").

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 on a l'impression 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 les "pools" et les "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 des utilisations très peu imaginatives et génériques de Manager, Handler, Adapter et des noms de ce genre, mais je pense que c'est un cas d'utilisation raisonnablement pardonnable quand la chose appelée "Manager" est en fait responsable de "gérer" ("contrôler?", "manipuler?") les durées de vie des objets, étant responsable de la création et de la destruction et de leur donner accès 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 à ajouter un -s à la fin. :-D Par exemple, disons que vous avez un ThreadManager et 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 simplement Threads. C'est ce que je fais de toute façon quand je n'ai pas d'imagination.

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

enter image description here

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 fait cette chose en tant que personne habituée à travailler avec les moteurs ECS.

3
user204677