Come evitare "gestori" nel mio codice

21

Attualmente sto riprogettando il mio sistema Entity , per C ++ e ho molti manager. Nel mio design, ho queste classi, per legare insieme la mia biblioteca. Ho sentito un sacco di cose brutte quando si tratta di classi "manager", forse non sto nominando le mie classi in modo appropriato. Tuttavia, non ho idea di cos'altro chiamarli.

La maggior parte dei gestori, nella mia libreria, è composta da queste classi (anche se varia un po '):

  • Contenitore: un contenitore per oggetti nel gestore
  • Attributi - attributi per gli oggetti nel gestore

Nel mio nuovo design per la mia libreria, ho queste classi specifiche, al fine di legare insieme la mia libreria.

  • ComponentManager: gestisce i componenti nel sistema Entity

    • ComponentContainer
    • ComponentAttributes
    • Scena * - un riferimento a una scena (vedi sotto)
  • SystemManager: gestisce i sistemi nel sistema Entity

    • SystemContainer
    • Scena * - un riferimento a una scena (vedi sotto)
  • EntityManager: gestisce le entità nel Entity System

    • EntityPool - un pool di entità
    • EntityAttributes - attributi di un'entità (questo sarà accessibile solo a classi ComponentContainer e System)
    • Scena * - un riferimento a una scena (vedi sotto)
  • Scene - lega tutti i gestori insieme

    • ComponentManager
    • SystemManager
    • EntityManager

Stavo pensando di mettere tutto il contenitore / i pool nella classe Scene stessa.

cioè.

Al posto di questo:

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

Sarebbe questo:

Scene scene; // create a Scene

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

Ma non sono sicuro. Se dovessi fare quest'ultimo, significherebbe che avrei un sacco di oggetti e metodi dichiarati all'interno della mia classe Scene.

NOTE:

  1. Un sistema di entità è semplicemente un progetto utilizzato per i giochi. È composto da 3 parti principali: componenti, entità e sistemi. I componenti sono semplicemente dati, che possono essere "aggiunti" alle entità, in modo che le entità siano distintive. Un'entità è rappresentata da un numero intero. I sistemi contengono la logica per un'entità, con componenti specifici.
  2. La ragione per cui sto cambiando il mio design per la mia libreria, è perché penso che possa essere cambiata parecchio, non mi piace la sensazione / flusso ad esso, al momento.
posta miguel.martin 12.02.2013 - 10:53
fonte

4 risposte

17

DeadMG è azzeccato sulle specifiche del tuo codice ma ritengo che manchi un chiarimento. Inoltre non sono d'accordo con alcune delle sue raccomandazioni che non si applicano ad alcuni contesti specifici come la maggior parte dello sviluppo di videogiochi ad alte prestazioni, ad esempio. Ma ha ragione a livello globale che la maggior parte del tuo codice non è utile in questo momento.

Come dice Dunk, le classi Manager sono chiamate così perché "gestiscono" le cose. "gestire" è come "dati" o "fare", è una parola astratta che può contenere quasi tutto.

Ero principalmente nello stesso posto di te circa 7 anni fa, e ho iniziato a pensare che ci fosse qualcosa di sbagliato nel mio modo di pensare perché erano così tanti sforzi da codificare per non fare ancora niente.

Ciò che ho modificato per risolvere questo problema è cambiare il vocabolario che uso nel codice. Evito totalmente parole generiche (a meno che non si tratti di codice generico, ma è raro che non si stiano creando librerie generiche). Evito di nominare qualsiasi tipo "Manager" o "oggetto".

L'impatto è diretto nel codice: ti costringe a trovare la parola giusta corrispondente alla reale responsabilità del tuo tipo. Se ritieni che il tipo faccia diverse cose (tieni un indice dei libri, mantienile in vita, crea / distruggi libri) allora hai bisogno di diversi tipi che ognuno avrà una responsabilità, quindi li combini nel codice che li usa. A volte ho bisogno di una fabbrica, a volte no. A volte ho bisogno di un registro, quindi ne ho installato uno (usando contenitori standard e puntatori intelligenti). A volte ho bisogno di un sistema composto da diversi sottosistemi diversi, quindi separo tutto quanto posso in modo che ogni parte faccia qualcosa di utile.

Non nominare mai un tipo "manager" e assicurarti che tutto il tuo tipo abbia un unico ruolo unico è la mia raccomandazione. Potrebbe essere difficile trovare nomi qualche volta, ma è una delle cose più difficili da fare nella programmazione in generale

    
risposta data 12.02.2013 - 23:17
fonte
23

Bene, ho letto alcuni dei codici a cui ti sei collegato e il tuo post, e il mio sommario onesto è che la maggior parte di essi è fondamentalmente completamente inutile. Scusate. Voglio dire, hai tutto questo codice, ma non hai raggiunto nulla. Affatto. Dovrò approfondire qui, quindi abbi pazienza con me.

Iniziamo con ObjectFactory . Innanzitutto, i puntatori di funzione. Questi sono apolidi, l'unico modo stateless utile per creare un oggetto è new e non è necessario uno stabilimento per quello. La creazione statica di un oggetto è ciò che è utile e i puntatori di funzione non sono utili per questa attività. In secondo luogo, ogni oggetto deve sapere come distruggere stesso , non dover trovare quell'istanza di creazione esatta per distruggerlo. Inoltre, è necessario restituire un puntatore intelligente, non un puntatore raw, per l'eccezione e la sicurezza delle risorse dell'utente. L'intera classe ClassRegistryData è solo std::function<std::unique_ptr<Base, std::function<void(Base*)>>()> , ma con i fori sopracitati. Non ha bisogno di esistere affatto.

Poi abbiamo AllocatorFunctor- ci sono già interfacce di allocatore standard fornite- Senza contare che sono solo wrapper su delete obj; e return new T; - che di nuovo, è già fornito, e non interagiranno con nessun stateful o allocatori di memoria personalizzati che esistono.

E ClassRegistry è semplicemente std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>> ma hai scritto una classe per questo invece di usare la funzionalità esistente. È lo stesso, ma completamente inutilmente stato mutabile globale con un mucchio di macro schifose.

Quello che sto dicendo qui è che hai creato delle classi in cui puoi sostituire l'intera cosa con funzionalità create dalle classi standard, e poi renderle ancora peggiori rendendole globali e modificabili e coinvolgendo un sacco di macro, qual è la cosa peggiore che potresti fare.

Quindi abbiamo Identificabile . Qui c'è un sacco della stessa storia: std::type_info esegue già il lavoro di identificazione di ogni tipo come valore di runtime e non richiede un numero di caratteri eccessivo per la parte dell'utente.

    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 risolto, ma senza nessuna delle precedenti duecento righe di macro e quant'altro. Ciò segna anche il tuo IdentifiableArray come nient'altro std::vector ma devi utilizzare il conteggio dei riferimenti anche se la proprietà o la non proprietà univoche sarebbero migliori.

Oh, e ho detto che I tipi contengono un gruppo di typedef assolutamente inutili ? Non ottieni letteralmente alcun vantaggio sull'utilizzo diretto dei tipi grezzi e perdi una buona parte della leggibilità.

Il prossimo è ComponentContainer . Non è possibile scegliere la proprietà, non è possibile avere contenitori separati per le classi derivate di vari componenti, non è possibile utilizzare la propria semantica di durata. Elimina senza rimorso.

Ora ComponentFilter potrebbe effettivamente valere un po ', se lo hai rifattorizzato molto. Qualcosa come

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

Questo non riguarda le classi derivate da quelle che stai cercando di affrontare, ma nemmeno le tue.

Il resto di questo è praticamente la stessa storia. Qui non c'è niente che non possa essere fatto molto meglio senza l'uso della tua biblioteca.

Come eviteresti i manager in questo codice? Elimina praticamente tutto.

    
risposta data 12.02.2013 - 20:31
fonte
10

Il problema con le classi Manager è che un manager può fare qualsiasi cosa. Non puoi leggere il nome della classe e sapere cosa fa la classe. EntityManager .... So che fa qualcosa con Entities ma chissà cosa. SystemManager ... sì, gestisce il sistema. È molto utile.

La mia ipotesi è che le tue classi di manager probabilmente fanno un sacco di cose. Scommetto che se dovessi suddividere quelle cose in parti funzionali strettamente correlate, allora probabilmente sarai in grado di trovare nomi di classi relativi ai pezzi funzionali che chiunque può dire quello che fanno semplicemente guardando il nome della classe. Questo renderà molto più semplice mantenere e comprendere il design.

    
risposta data 12.02.2013 - 20:42
fonte
1

In realtà non penso che Manager sia così brutto in questo contesto, un'alzata di spalle. Se c'è un posto dove penso che Manager sia perdonabile, sarebbe per un sistema di entità-componente, dato che è veramente " gestire " le vite di componenti, entità e sistemi. Come minimo, qui c'è un nome più significativo di, per esempio, Factory che in questo contesto non ha molto senso dal momento che un ECS fa davvero un po 'di più che creare cose.

Suppongo che potresti usare Database , come ComponentDatabase . Oppure potresti semplicemente usare Components , Systems e Entities (questo è quello che uso personalmente e principalmente solo perché mi piacciono gli identificatori concisi anche se certamente trasmette anche meno informazioni di "Manager").

L'unica parte che trovo un po 'maldestra è questa:

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

Questo è un bel po 'di roba a get prima di poter creare un'entità e sembra che il progetto stia perdendo un po' i dettagli di implementazione se devi sapere su pool di entità e "gestori" per crearli. Penso che cose come "pool" e "manager" (se ti attieni a questo nome) dovrebbero essere i dettagli di implementazione di ECS, non qualcosa di esposto al client.

Ma non so, ho visto alcuni usi non fantasiosi e generici di Manager , Handler , Adapter e nomi di questo tipo, ma penso che questo sia un caso d'uso ragionevolmente perdonabile quando la cosa chiamata "Manager "è in realtà responsabile della" gestione "(" controllo? "," gestione? ") delle vite degli oggetti, della responsabilità di creare e distruggere e di fornire loro l'accesso individuale. Questo è l'uso più diretto e intuitivo di "Manager" almeno per me.

Detto questo, una strategia generale per evitare "Manager" nel tuo nome è semplicemente eliminare la parte "Manager" e aggiungere un -s alla fine. :-D Ad esempio, supponiamo che tu abbia un ThreadManager ed è responsabile della creazione di thread e della fornitura di informazioni su di essi e dell'accesso a tali e così via, ma non raggruppa i thread, quindi non possiamo chiamarlo ThreadPool . Bene, in tal caso, lo chiami solo Threads . Questo è quello che faccio comunque quando sono privo di immaginazione.

Mi viene in mente di ricordare quando l'equivalente analogico di "Windows Explorer" era chiamato "File Manager", in questo modo:

Einrealtàpensoche"File Manager" in questo caso sia più descrittivo di "Windows Explorer" anche se c'è un nome migliore che non includa "Manager". Quindi, per lo meno, per favore non essere troppo stravagante con i tuoi nomi e inizia a nominare cose come "ComponentExplorer". "ComponentManager" mi fornisce almeno un'idea ragionevole di ciò che questa cosa fa come una volta abituato a lavorare con i motori ECS.

    
risposta data 18.12.2017 - 05:22
fonte