Como evitar "gerentes" no meu código


26

Atualmente, estou redesenhando meu Sistema de Entidades , para C ++, e tenho muitos Gerentes. No meu design, eu tenho essas classes, para amarrar minha biblioteca. Ouvi muitas coisas ruins quando se trata de aulas de "gerente", talvez eu não esteja nomeando minhas aulas adequadamente. No entanto, não tenho mais idéia de como nomeá-los.

A maioria dos gerentes, na minha biblioteca, é composta dessas classes (embora varie um pouco):

  • Contêiner - um contêiner para objetos no gerenciador
  • Atributos - atributos para os objetos no gerenciador

No meu novo design para minha biblioteca, tenho essas classes específicas, a fim de vincular minha biblioteca.

  • ComponentManager - gerencia componentes no Sistema de entidades

    • ComponentContainer
    • ComponentAttributes
    • Cena * - uma referência a uma Cena (veja abaixo)
  • SystemManager - gerencia sistemas no Sistema de entidades

    • SystemContainer
    • Cena * - uma referência a uma Cena (veja abaixo)
  • EntityManager - gerencia entidades no sistema de entidades

    • EntityPool - um conjunto de entidades
    • EntityAttributes - atributos de uma entidade (isso só estará acessível às classes ComponentContainer e System)
    • Cena * - uma referência a uma Cena (veja abaixo)
  • Cena - une todos os gerentes

    • ComponentManager
    • SystemManager
    • EntityManager

Eu estava pensando em colocar todos os contêineres / piscinas na própria classe Scene.

ie

Em vez disso:

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

Seria o seguinte:

Scene scene; // create a Scene

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

Mas não tenho certeza. Se eu fizesse o último, isso significaria que eu teria muitos objetos e métodos declarados dentro da minha classe Scene.

NOTAS:

  1. Um sistema de entidades é simplesmente um design usado para jogos. É composto por três partes principais: componentes, entidades e sistemas. Os componentes são simplesmente dados, que podem ser "adicionados" às entidades, para que as entidades sejam distintas. Uma entidade é representada por um número inteiro. Os sistemas contêm a lógica de uma entidade, com componentes específicos.
  2. A razão pela qual estou mudando meu design para minha biblioteca é porque acho que pode ser alterado bastante, não gosto da sensação / fluxo dela no momento.

Você pode, por favor, elaborar as coisas ruins que ouviu sobre os gerentes e como elas pertencem a você?
21813 MrFox


@MrFox Basicamente, ouvi o que Dunk mencionou e tenho muitas classes * Manager na minha biblioteca, das quais quero me livrar.
Miguel.martin

Isso também pode ser útil: medium.com/@wrong.about/…
Zapadlo

Você fatora uma estrutura útil de um aplicativo em funcionamento. É quase impossível escrever uma estrutura útil para aplicativos imaginados.
Kevin cline

Respostas:


19

DeadMG é direto sobre as especificidades do seu código, mas sinto que falta um esclarecimento. Também não concordo com algumas de suas recomendações que não se aplicam a contextos específicos, como o desenvolvimento de videogames de alto desempenho, por exemplo. Mas ele está globalmente certo que a maior parte do seu código não é útil no momento.

Como Dunk diz, as classes de gerente são chamadas assim porque "gerenciam" as coisas. "gerenciar" é como "dados" ou "do", é uma palavra abstrata que pode conter quase qualquer coisa.

Eu estava no mesmo lugar que você há cerca de 7 anos e comecei a pensar que havia algo errado na minha maneira de pensar, porque havia muito esforço para codificar para não fazer nada ainda.

O que eu mudei para corrigir isso é mudar o vocabulário que eu uso no código. Eu evito totalmente palavras genéricas (a menos que seja código genérico, mas é raro quando você não está criando bibliotecas genéricas). I evitar a nomear qualquer tipo "Manager" ou "objeto".

O impacto é direto no código: força você a encontrar a palavra certa correspondente à responsabilidade real do seu tipo. Se você acha que o tipo faz várias coisas (mantenha um índice de Livros, mantenha-os vivos, crie / destrua livros), precisará de tipos diferentes, cada um com uma responsabilidade, e combiná-los no código que os utiliza. Às vezes eu preciso de uma fábrica, às vezes não. Às vezes, preciso de um registro, por isso configurei um (usando contêineres padrão e indicadores inteligentes). Às vezes, preciso de um sistema composto por vários subsistemas diferentes, para separar tudo o que puder para que cada parte faça algo útil.

Nunca nomeie um tipo de "gerente" e verifique se todo o seu tipo tem uma única função única. É minha recomendação. Pode ser difícil encontrar nomes em algum momento, mas é uma das coisas mais difíceis de fazer na programação em geral


Se alguns dynamic_castestão matando seu desempenho, use o sistema de tipo estático - é para isso que serve. É realmente um contexto especializado, não geral, que precisa rolar seu próprio RTTI, como o LLVM. Mesmo assim, eles não fazem isso.
DeadMG

@DeadMG Eu não estava falando sobre essa parte em particular, mas a maioria dos jogos de alto desempenho não pode pagar, por exemplo, alocações dinâmicas por meio de coisas novas ou até outras que são boas para a programação em geral. Eles são bestas específicas para hardware específico que você não pode generalizar e é por isso que "depende" é uma resposta mais geral, em particular no C ++. (ninguém no game dev usa elenco dinâmico por sinal, nunca é útil até que você tenha plugins, e mesmo assim é raro).
22413 Klaim

@DeadMG Não estou usando dynamic_cast, nem uma alternativa ao dynamic_cast. Eu o implementei na biblioteca, mas não me incomodei em tirá-lo. template <typename T> class Class { ... };é, na verdade, para atribuir IDs para diferentes classes (componentes e sistemas personalizados). A atribuição de ID é usada para armazenar componentes / sistemas em um vetor, pois um mapa pode não trazer o desempenho necessário para um jogo.
Miguel.martin

Bem, eu também não implementei uma alternativa ao dynamic_cast, apenas um type_id falso. Mas ainda não queria o que type_id conseguiu.
Miguel.martin

"encontre a palavra certa correspondente à responsabilidade real do seu tipo" - embora eu concorde totalmente com isso, muitas vezes não há uma única palavra (se houver) que a comunidade de desenvolvedores concordou com um tipo específico de responsabilidade.
Bydevev

23

Bem, eu li alguns dos códigos aos quais você vinculou e sua postagem, e meu resumo honesto é que a maioria deles é basicamente completamente inútil. Desculpe. Quero dizer, você tem todo esse código, mas não conseguiu nada. Em absoluto. Vou ter que me aprofundar um pouco aqui, então tenha paciência comigo.

Vamos começar com o ObjectFactory . Em primeiro lugar, ponteiros de função. Elas são apátridas, a única maneira útil e apátrida de criar um objeto é newe você não precisa de uma fábrica para isso. Criar um objeto com estado é o que é útil e ponteiros de função não são úteis para esta tarefa. E em segundo lugar, cada objeto precisa saber como destruir em si , não ter que encontrar essa instância exata criando para destruí-la. Além disso, você deve retornar um ponteiro inteligente, não um ponteiro bruto, para a exceção e a segurança dos recursos do usuário. Toda a sua classe ClassRegistryData é justa std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, mas com os buracos mencionados acima. Não precisa existir.

Então temos o AllocatorFunctor - já existem interfaces de alocador padrão fornecidas - Sem mencionar que elas são apenas wrappers delete obj;e return new T;- que novamente já são fornecidas e não interagem com nenhum alocador de memória com estado ou personalizado que exista.

E ClassRegistry é simples, std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>mas você escreveu uma classe para ela em vez de usar a funcionalidade existente. É o mesmo, mas totalmente mutável estado global desnecessário, com várias macros ruins.

O que estou dizendo aqui é que você criou algumas classes nas quais poderia substituir a coisa toda pela funcionalidade criada a partir das classes Standard e as piorou ainda mais, tornando-as em um estado mutável global e envolvendo um monte de macros, que é o pior coisa que você poderia fazer.

Então nós temos identificável . É muito parecido com a história aqui - std::type_infojá realiza o trabalho de identificar cada tipo como um valor em tempo de execução e não requer clichê excessivo por parte do usuário.

    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 resolvido, mas sem nenhuma das duzentas linhas anteriores de macros e outros enfeites. Isso também marca o seu IdentifiableArray como nada, mas std::vectorvocê precisa usar a contagem de referências, mesmo que a propriedade exclusiva ou a não propriedade sejam melhores.

Ah, e eu mencionei que Types contém um monte de typedefs absolutamente inúteis ? Você literalmente não obtém nenhuma vantagem sobre o uso direto dos tipos brutos e perde uma boa parte da legibilidade.

O próximo passo é ComponentContainer . Você não pode escolher a propriedade, não pode ter contêineres separados para classes derivadas de vários componentes, não pode usar sua própria semântica vitalícia. Excluir sem remorso.

Agora, o ComponentFilter pode realmente valer um pouco, se você o refatorar bastante. 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();
    }
};

Isso não lida com classes derivadas daquelas com as quais você está tentando lidar, mas nem a sua.

O resto disso é praticamente a mesma história. Não há nada aqui que não poderia ser feito muito melhor sem o uso da sua biblioteca.

Como você evitaria os gerentes nesse código? Exclua basicamente tudo isso.


14
Demorei um pouco para aprender, mas os códigos mais confiáveis ​​e sem erros são os que não estão lá .
Tacroy

1
O motivo pelo qual não usei std :: type_info é porque estava tentando evitar o RTTI. Eu mencionei que o framework era voltado para jogos, que é basicamente a razão pela qual eu não estou usando typeidou dynamic_cast. Obrigado pelo feedback, porém, eu aprecio isso.
Miguel.martin

Esses críticos são baseados no seu conhecimento dos mecanismos de jogos do sistema de entidade componente ou é uma revisão geral do código C ++?
Den

1
@ Den Eu acho mais um problema de design do que qualquer outra coisa.
Miguel.martin

10

O problema com as classes do gerente é que um gerente pode fazer qualquer coisa. Você não pode ler o nome da turma e saber o que ela faz. EntityManager .... Eu sei que ele faz algo com Entidades, mas quem sabe o quê. SystemManager ... sim, ele gerencia o sistema. Isso é muito útil.

Meu palpite é que suas classes de gerente provavelmente fazem muitas coisas. Aposto que, se você segmentar essas coisas em partes funcionais intimamente relacionadas, provavelmente poderá criar nomes de classe relacionados às partes funcionais que qualquer pessoa pode dizer o que faz simplesmente olhando para o nome da classe. Isso tornará muito mais fácil manter e entender o design.


1
+1 e não apenas eles podem fazer qualquer coisa, como em uma linha do tempo longa o suficiente. As pessoas veem o descritor "Gerente" como um convite para criar aulas de pia da cozinha / canivete suíço.
Erik Dietrich

@Erik - Ah, sim, eu deveria ter acrescentado que ter um bom nome de classe não é importante apenas porque diz a alguém o que a classe pode fazer; mas igualmente o nome da classe também diz o que a classe não deve fazer.
Dunk

3

Na verdade, eu não acho Managertão ruim nesse contexto, encolher os ombros. Se há um lugar que eu acho que Manageré perdoável, seria para um sistema de componente de entidade, pois ele realmente está " gerenciando " a vida útil de componentes, entidades e sistemas. No mínimo, esse é um nome mais significativo aqui do que, digamos, Factoryque não faz tanto sentido nesse contexto, uma vez que um ECS realmente faz um pouco mais do que criar coisas.

Suponho que você poderia usar Database, como ComponentDatabase. Ou você pode apenas usar Components, Systemse Entities(é isso que eu pessoalmente uso e principalmente porque gosto de identificadores concisos, embora ele admita que transmita ainda menos informações, talvez, que "Gerente").

A parte que acho um pouco desajeitada é a seguinte:

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

Isso é um monte de coisas getantes que você possa criar uma entidade e parece que o design está vazando um pouco os detalhes da implementação se você precisar conhecer os pools de entidades e os "gerentes" para criá-los. Eu acho que coisas como "pools" e "gerentes" (se você se ater a esse nome) devem ser detalhes de implementação do ECS, não algo exposto ao cliente.

Mas sei lá, eu vi alguns usos muito sem imaginação e genéricas de Manager, Handler, Adaptere nomes desse tipo, mas acho que este é um caso de uso razoavelmente perdoável quando a coisa chamada "Manager" é realmente responsável por "gestão" ( "controlar? "," manipulação? ") a vida útil dos objetos, sendo responsável por criar e destruir e fornecer acesso a eles individualmente. Esse é o uso mais direto e intuitivo do "Manager" para mim, pelo menos.

Dito isto, uma estratégia geral para evitar "Gerente" em seu nome é apenas retirar a parte "Gerente" e adicionar uma -sno final. :-D Por exemplo, digamos que você tenha um ThreadManagere é responsável por criar threads e fornecer informações sobre eles e acessá-los e assim por diante, mas ele não agrupa threads e não podemos chamá-lo ThreadPool. Bem, nesse caso, você apenas chama Threads. É o que faço de qualquer maneira quando não tenho imaginação.

Eu sou meio lembrado quando o equivalente analógico do "Windows Explorer" era chamado "Gerenciador de Arquivos", assim:

insira a descrição da imagem aqui

E, na verdade, acho que "Gerenciador de Arquivos", neste caso, é mais descritivo que "Windows Explorer", mesmo que haja algum nome melhor por aí que não envolva "Gerenciador". Portanto, pelo menos, não exagere nos nomes e comece a nomear coisas como "ComponentExplorer". O "ComponentManager" pelo menos me dá uma idéia razoável do que essa coisa faz quando alguém costumava trabalhar com mecanismos ECS.


Acordado. Se uma classe for usada para tarefas gerenciais típicas (criação ("contratação"), destruição ("demissão"), supervisão e interface "funcionário" / superior, o nome Manageré apropriado. A classe pode ter problemas de design , mas o nome é spot-on.
Justin Time 2 Reintegrar Monica
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.