Cómo evitar "gerentes" en mi código

26

Actualmente estoy rediseñando mi Sistema de Entidades , para C ++, y tengo muchos Gerentes. En mi diseño, tengo estas clases, para unir mi biblioteca. He escuchado muchas cosas malas cuando se trata de clases de "gerente", quizás no estoy nombrando mis clases apropiadamente. Sin embargo, no tengo idea de qué más nombrarlos.

La mayoría de los gerentes, en mi biblioteca, se componen de estas clases (aunque varía un poco):

  • Contenedor: un contenedor para objetos en el administrador
  • Atributos: atributos para los objetos en el administrador

En mi nuevo diseño para mi biblioteca, tengo estas clases específicas, para unir mi biblioteca.

  • ComponentManager: administra componentes en el Sistema de entidades

    • ComponentContainer
    • Atributos de componente
    • Escena *: una referencia a una escena (ver más abajo)
  • SystemManager: administra sistemas en el sistema de entidades

    • Contenedor de sistema
    • Escena *: una referencia a una escena (ver más abajo)
  • EntityManager: administra entidades en el sistema de entidades

    • EntityPool - un grupo de entidades
    • Atributos de entidad: atributos de una entidad (esto solo será accesible para las clases ComponentContainer y System)
    • Escena *: una referencia a una escena (ver más abajo)
  • Escena: une a todos los gerentes

    • ComponentManager
    • Administrador de sistemas
    • EntityManager

Estaba pensando en poner todos los contenedores / piscinas en la clase Scene.

es decir

En lugar de esto:

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

Sería esto:

Scene scene; // create a Scene

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

Pero no estoy seguro. Si tuviera que hacer esto último, eso significaría que tendría muchos objetos y métodos declarados dentro de mi clase de escena.

NOTAS

  1. Un sistema de entidad es simplemente un diseño que se utiliza para juegos. Se compone de 3 partes principales: componentes, entidades y sistemas. Los componentes son simplemente datos, que pueden "agregarse" a las entidades, para que las entidades sean distintivas. Una entidad está representada por un número entero. Los sistemas contienen la lógica de una entidad, con componentes específicos.
  2. La razón por la que estoy cambiando mi diseño para mi biblioteca es porque creo que se puede cambiar bastante, no me gusta la sensación / flujo en este momento.
miguel.martin
fuente
¿Puedes por favor explicar las cosas malas que has escuchado sobre los gerentes y cómo te pertenecen?
MrFox
@MrFox Básicamente he escuchado lo que Dunk ha mencionado, y tengo muchas * clases de Gerente en mi biblioteca, de las cuales quiero deshacerme.
miguel.martin
Esto también podría ser útil: medium.com/@wrong.about/…
Zapadlo
Factoriza un marco útil a partir de una aplicación de trabajo. Es casi imposible escribir un marco útil para aplicaciones imaginarias.
Kevin Cline

Respuestas:

19

DeadMG es acertado en los detalles de su código, pero siento que falta una aclaración. Además, no estoy de acuerdo con algunas de sus recomendaciones que no se cumplen en algunos contextos específicos, como la mayoría de los desarrollos de videojuegos de alto rendimiento, por ejemplo. Pero tiene razón a nivel mundial de que la mayoría de su código no es útil en este momento.

Como dice Dunk, las clases de Manager se llaman así porque "gestionan" las cosas. "administrar" es como "datos" o "hacer", es una palabra abstracta que puede contener casi cualquier cosa.

Hace más de 7 años estuve en el mismo lugar que usted, y comencé a pensar que había algo mal en mi forma de pensar porque era mucho esfuerzo codificar para no hacer nada todavía.

Lo que cambié para arreglar esto es cambiar el vocabulario que uso en el código. Evito totalmente las palabras genéricas (a menos que sea un código genérico, pero es raro cuando no está creando bibliotecas genéricas). Yo evito nombrar cualquier tipo "Administrador" o "objeto".

El impacto es directo en el código: te obliga a encontrar la palabra correcta correspondiente a la responsabilidad real de tu tipo. Si siente que el tipo hace varias cosas (mantener un índice de libros, mantenerlos vivos, crear / destruir libros), entonces necesita diferentes tipos, cada uno tendrá una responsabilidad, luego los combina en el código que los usa. A veces necesito una fábrica, a veces no. En algún momento necesito un registro, así que configuro uno (usando contenedores estándar y punteros inteligentes). En algún momento necesito un sistema que se compone de varios subsistemas diferentes, así que separo todo lo que puedo para que cada parte haga algo útil.

Nunca nombre a un tipo "gerente" y asegúrese de que todo su tipo tenga un rol único único es mi recomendación. Puede ser difícil encontrar nombres alguna vez, pero es una de las cosas más difíciles de hacer en general.

Klaim
fuente
Si una pareja dynamic_castestá matando su rendimiento, entonces use el sistema de tipo estático, para eso está. Es realmente un contexto especializado, no general, tener que rodar su propio RTTI como lo hizo LLVM. Incluso entonces, no hacen esto.
DeadMG
@DeadMG No estaba hablando de esta parte en particular, pero la mayoría de los juegos de alto rendimiento no pueden permitirse, por ejemplo, asignaciones dinámicas a través de cosas nuevas o incluso otras que están bien para la programación en general. Son bestias específicas para hardware específico que no puede generalizar, por lo que "depende" es una respuesta más general, en particular con C ++. (por cierto, nadie en el desarrollo de juegos usa el lanzamiento dinámico, nunca es útil hasta que tengas complementos, e incluso entonces es raro).
Klaim
@DeadMG No estoy usando dynamic_cast, ni estoy usando una alternativa a dynamic_cast. Lo implementé en la biblioteca, pero no me molesté en sacarlo. template <typename T> class Class { ... };en realidad es para asignar ID para diferentes clases (es decir, componentes personalizados y sistemas personalizados). La asignación de ID se usa para almacenar componentes / sistemas en un vector, ya que un mapa puede no proporcionar el rendimiento que necesito para un juego.
miguel.martin
Bueno, en realidad tampoco implementé una alternativa a dynamic_cast, solo un tipo_id falso. Pero, todavía no quería lo que type_id logró.
miguel.martin
"encuentre la palabra correcta correspondiente a la responsabilidad real de su tipo", aunque estoy totalmente de acuerdo con esto, a menudo no hay una sola palabra (si la hay) que la comunidad de desarrolladores haya acordado para un tipo particular de responsabilidad.
bytedev
23

Bueno, leí algunos de los códigos a los que se vinculó, y su publicación, y mi resumen honesto es que la mayoría es básicamente inútil. Lo siento. Quiero decir, tienes todo este código, pero no has logrado nada. En absoluto. Voy a tener que profundizar un poco aquí, así que tengan paciencia conmigo.

Comencemos con ObjectFactory . En primer lugar, los punteros de función. Estos son sin estado, la única forma útil sin estado de crear un objeto es newy no necesita una fábrica para eso. Crear un objeto con estado es lo que es útil y los punteros de función no son útiles para esta tarea. Y en segundo lugar, cada objeto necesita saber cómo destruirse a sí mismo , sin tener que encontrar esa instancia de creación exacta para destruirlo. Además, debe devolver un puntero inteligente, no un puntero sin formato, para la excepción y la seguridad de los recursos de su usuario. Toda su clase ClassRegistryData es justa std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, pero con los agujeros mencionados anteriormente. No necesita existir en absoluto.

Luego tenemos AllocatorFunctor, ya hay interfaces de asignadores estándar proporcionadas, sin mencionar que son solo envolturas delete obj;y return new T;, lo que de nuevo, ya está provisto, y no interactuarán con ningún asignador de memoria con estado o personalizado que exista.

Y ClassRegistry es simplemente std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>pero escribiste una clase para él en lugar de usar la funcionalidad existente. Es el mismo, pero completamente mutable estado innecesariamente global con un montón de macros cutres.

Lo que estoy diciendo aquí es que ha creado algunas clases en las que podría reemplazar todo con una funcionalidad construida a partir de clases estándar, y luego las hizo aún peor al convertirlas en un estado mutable global e involucrar un montón de macros, que es el Lo peor que podrías hacer.

Entonces tenemos identificable . Aquí es una gran parte de la misma historia: std::type_infoya realiza el trabajo de identificar cada tipo como un valor de tiempo de ejecución, y no requiere una placa repetitiva excesiva por parte del usuario.

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

Problema resuelto, pero sin ninguna de las doscientas líneas de macros anteriores y demás. Esto también marca su IdentifiableArray como nada pero std::vectortiene que usar el recuento de referencias incluso si la propiedad única o la no propiedad serían mejores.

Ah, y ¿mencioné que Tipos contiene un montón de tipos de letra absolutamente inútiles ? Literalmente, no obtiene ninguna ventaja sobre el uso directo de los tipos sin formato y pierde una buena parte de la legibilidad.

El siguiente es ComponentContainer . No puede elegir la propiedad, no puede tener contenedores separados para clases derivadas de varios componentes, no puede usar su propia semántica de por vida. Eliminar sin remordimiento.

Ahora, el ComponentFilter podría valer un poco, si lo refactorizaste mucho. Algo como

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

Esto no trata con las clases derivadas de las que estás tratando de tratar, pero tampoco las tuyas.

El resto de esto es más o menos la misma historia. No hay nada aquí que no se pueda hacer mucho mejor sin el uso de su biblioteca.

¿Cómo evitarías a los gerentes en este código? Eliminar básicamente todo.

DeadMG
fuente
14
Me llevó un tiempo aprender, pero los códigos más confiables y libres de errores son los que no están allí .
Tacroy
1
La razón por la que no usé std :: type_info es porque estaba tratando de evitar RTTI. Mencioné que el marco estaba dirigido a juegos, que es básicamente la razón por la que no estoy usando typeido dynamic_cast. Sin embargo, gracias por los comentarios, lo aprecio.
miguel.martin
¿Estas críticas se basan en su conocimiento de los motores de juego del sistema de entidad componente o es una revisión general del código C ++?
Den
1
@ Den Creo que es más un problema de diseño que otra cosa.
miguel.martin
10

El problema con las clases Manager es que un gerente puede hacer cualquier cosa. No puede leer el nombre de la clase y saber qué hace la clase. EntityManager ... Sé que hace algo con las Entidades, pero quién sabe qué. SystemManager ... sí, administra el sistema. Eso es muy útil

Supongo que sus clases de gerentes probablemente hagan muchas cosas. Apuesto a que si segmentas esas cosas en piezas funcionales estrechamente relacionadas, entonces probablemente puedas encontrar nombres de clase relacionados con las piezas funcionales que cualquiera puede decir lo que hacen simplemente mirando el nombre de la clase. Esto hará que sea mucho más fácil mantener y comprender el diseño.

Remojar
fuente
1
+1 y no solo pueden hacer algo, sino que lo harán en una línea de tiempo lo suficientemente larga. La gente ve el descriptor "Gerente" como una invitación para crear clases de cocina-fregadero / navaja suiza.
Erik Dietrich
@Erik - Ah sí, debería haber agregado que tener un buen nombre de clase no solo es importante porque le dice a alguien lo que la clase puede hacer; pero igualmente el nombre de la clase también dice lo que la clase no debería hacer.
Dunk
3

En realidad no creo que Managersea ​​tan malo en este contexto, encogerse de hombros. Si hay un lugar donde creo que Manageres perdonable, sería para un sistema de entidad-componente, ya que realmente está " gestionando " las vidas de componentes, entidades y sistemas. Por lo menos, ese es un nombre más significativo aquí que, por ejemplo, Factorylo que no tiene mucho sentido en este contexto, ya que un ECS realmente hace un poco más que crear cosas.

Supongo que podrías usar Database, como ComponentDatabase. O simplemente podrías usar Components,Systems y Entities(esto es lo que yo uso personalmente y principalmente porque me gustan los identificadores concisos, aunque es cierto que transmite incluso menos información, tal vez, que "Gerente").

La única parte que encuentro un poco torpe es esta:

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

Eso es un montón de cosas para get antes de que pueda crear una entidad y parece que el diseño está filtrando un poco los detalles de implementación si tiene que saber sobre los grupos de entidades y los "gerentes" para crearlos. Creo que cosas como "grupos" y "gerentes" (si se apega a este nombre) deberían ser detalles de implementación del ECS, no algo expuesto al cliente.

Pero sé, he visto algunos usos muy poco imaginativas y genéricas de Manager, Handler,Adapter , y nombres de este tipo, pero creo que este es un caso de uso razonablemente perdonables cuando la cosa llamada "Administrador" es realmente responsable de la "gestión" ( "controlar? "," manejo? ") la vida útil de los objetos, siendo responsable de crear y destruir y proporcionar acceso a ellos individualmente. Ese es el uso más directo e intuitivo de "Manager" para mí al menos.

Dicho esto, una estrategia general para evitar "Gerente" en su nombre es simplemente sacar la parte "Gerente" y agregar una -sal final. :-D Por ejemplo, supongamos que tiene un ThreadManagery es responsable de crear hilos y proporcionar información sobre ellos y acceder a ellos, etc., pero no agrupa los hilos, por lo que no podemos llamarlo ThreadPool. Bueno, en ese caso, solo llámalo Threads. Eso es lo que hago de todos modos cuando no tengo imaginación.

Me recuerda un poco cuando el equivalente analógico de "Windows Explorer" se llamaba "Administrador de archivos", así:

ingrese la descripción de la imagen aquí

Y en realidad creo que "Administrador de archivos" en este caso es más descriptivo que "Explorador de Windows", incluso si hay algún nombre mejor que no implique "Administrador". Así que, al menos, no seas tan elegante con tus nombres y comienza a nombrar cosas como "ComponentExplorer". "ComponentManager" al menos me da una idea razonable de lo que hace esa cosa cuando alguien solía trabajar con motores ECS.


fuente
Convenido. Si una clase se usa para tareas gerenciales típicas (creación ("contratación"), destrucción ("despido"), supervisión e interfaz "empleado" / superior, entonces el nombre Manageres apropiado. La clase puede tener problemas de diseño , pero el nombre es perfecto
Justin Time 2 Restablece a Monica el