Come evitare i "gestori" nel mio codice


26

Attualmente sto riprogettando il mio Entity System , per C ++, e ho molti manager. Nel mio progetto, ho queste lezioni, al fine di legare insieme la mia biblioteca. Ho sentito molte cose brutte quando si tratta di lezioni di "manager", forse non sto nominando le mie lezioni in modo appropriato. Tuttavia, non ho idea di cos'altro nominarli.

La maggior parte dei gestori, nella mia libreria, sono composti da queste classi (anche se varia leggermente):

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

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

  • ComponentManager: gestisce i componenti nel sistema di entità

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

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

    • EntityPool - un pool di entità
    • EntityAttributes - attributi di un'entità (questo sarà accessibile solo alle classi ComponentContainer e System)
    • Scena * - un riferimento a una scena (vedi sotto)
  • Scena: unisce tutti i manager

    • ComponentManager
    • Gestore di sistema
    • EntityManager

Stavo pensando di mettere tutti i container / pool nella classe Scene stessa.

vale a dire

Invece 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, ciò significherebbe che molti oggetti e metodi sarebbero stati dichiarati nella mia classe Scene.

GLI APPUNTI:

  1. Un sistema di entità è semplicemente un disegno 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 di un'entità, con componenti specifici.
  2. Il motivo per cui sto cambiando il mio design per la mia biblioteca, è perché penso che possa essere cambiato molto, al momento non mi piace il feeling / flusso.

Puoi per favore approfondire le cose brutte che hai sentito sui manager e su come ti riguardano?
MrFox,


@MrFox Fondamentalmente ho sentito quello che Dunk ha menzionato e ho molte * classi Manager nella mia libreria, di cui voglio liberarmi.
miguel.martin,

Anche questo potrebbe essere utile: medium.com/@wrong.about/…
Zapadlo,

Un fattore utile viene estratto da un'applicazione funzionante. È quasi impossibile scrivere un framework utile per applicazioni immaginate.
Kevin Cline,

Risposte:


19

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

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

Ero quasi 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é ci sono stati così tanti sforzi per programmare di non fare ancora nulla.

Quello che ho cambiato per risolvere questo problema è cambiare il vocabolario che uso nel codice. Evito totalmente le parole generiche (a meno che non sia un codice generico, ma è raro quando non si creano librerie generiche). Io 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 (mantieni un indice di Libri, mantienili in vita, crea / distruggi libri), allora hai bisogno di tipi diversi che ognuno avrà una responsabilità, quindi li combini nel codice che li utilizza. A volte ho bisogno di una fabbrica, a volte no. A volte ho bisogno di un registro, quindi ne installo uno (usando contenitori standard e puntatori intelligenti). A volte ho bisogno di un sistema composto da diversi sottosistemi, quindi separo tutto il più possibile in modo che ogni parte faccia qualcosa di utile.

Non nominare mai un tipo "manager" e assicurarsi 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


Se un paio di dynamic_casts stanno uccidendo le tue prestazioni, usa il sistema di tipo statico, ecco a cosa serve. È davvero un contesto specializzato, non generale, dover girare il proprio RTTI come ha fatto LLVM. Anche allora, non lo fanno.
DeadMG

@DeadMG Non stavo parlando di questa parte in particolare, ma la maggior parte dei giochi ad alte prestazioni non possono permettersi, ad esempio, allocazioni dinamiche attraverso nuovi o addirittura altri elementi che vanno bene per la programmazione in generale. Sono bestie specifiche per hardware specifico che non puoi generalizzare, motivo per cui "dipende" è una risposta più generale, in particolare con C ++. (nessuno negli sviluppatori di giochi usa il cast dinamico tra l'altro, non è mai utile fino a quando non hai plug-in, e anche allora è raro).
Klaim,

@DeadMG Non sto usando dynamic_cast, né sto usando un'alternativa a dynamic_cast. L'ho implementato in biblioteca, ma non potevo preoccuparmi di eliminarlo. template <typename T> class Class { ... };è in realtà per l'assegnazione di ID per classi diverse (ovvero componenti personalizzati e sistemi personalizzati). L'assegnazione dell'ID viene utilizzata per archiviare componenti / sistemi in un vettore, poiché una mappa potrebbe non offrire le prestazioni di cui ho bisogno per un gioco.
miguel.martin,

Beh, non ho nemmeno implementato un'alternativa a dynamic_cast, solo un tipo_id falso. Ma, comunque, non volevo ciò che type_id ha ottenuto.
miguel.martin,

"trova la parola giusta corrispondente alla reale responsabilità del tuo tipo" - anche se sono totalmente d'accordo con questo, spesso non esiste una sola parola (se presente) che la comunità degli sviluppatori abbia concordato per un particolare tipo di responsabilità.
bytedev,

23

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

Cominciamo con ObjectFactory . Innanzitutto, i puntatori a funzione. Questi sono apolidi, l'unico modo utile apolidi per creare un oggetto è newe per questo non hai bisogno di una fabbrica. La creazione statica di un oggetto è ciò che è utile e i puntatori a funzione non sono utili per questa attività. E in secondo luogo, ogni oggetto deve sapere come distruggere se stesso , non dovendo trovare l'esatta istanza di creazione per distruggerlo. Inoltre, è necessario restituire un puntatore intelligente, non un puntatore non elaborato, per l'eccezione e la sicurezza delle risorse dell'utente. Tutta la tua classe ClassRegistryData è giusta std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, ma con i buchi di cui sopra. Non ha bisogno di esistere affatto.

Quindi abbiamo AllocatorFunctor - ci sono già interfacce di allocatore standard fornite - Per non parlare del fatto che sono solo wrapper su - delete obj;e return new T;che, di nuovo, è già fornito e non interagiranno con alcun allocatore di memoria con stato o personalizzato esistente.

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

Quello che sto dicendo qui è che hai creato alcune classi in cui potresti sostituire l'intera cosa con funzionalità costruite da classi Standard, e poi le hai ulteriormente peggiorate rendendole uno stato mutabile globale e coinvolgendo un sacco di macro, che è il cosa peggiore che potresti fare.

Quindi abbiamo identificabile . È molto simile alla stessa storia qui, std::type_infosvolge già il compito di identificare ogni tipo come valore di runtime e non richiede un eccessivo boilerplate da 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. Questo segna anche il tuo identificativo come nient'altro chestd::vector ma devi usare il conteggio dei riferimenti anche se la proprietà unica o la non proprietà sarebbero migliori.

Oh, e ho già detto che Tipi contiene un mucchio di assolutamente inutili typedef ? Non ottieni letteralmente alcun vantaggio sull'uso diretto dei tipi grezzi e perdi un buon pezzo di leggibilità.

Il prossimo è ComponentContainer . Non puoi scegliere la proprietà, non puoi avere contenitori separati per le classi derivate di vari componenti, non puoi usare la tua semantica a vita. Elimina senza rimorso.

Ora, il ComponentFilter potrebbe effettivamente valere un po ', se lo hai riformattato molto. Qualcosa di simile a

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 gestire, ma nemmeno le tue.

Il resto è praticamente la stessa storia. Non c'è nulla qui che non potrebbe essere fatto molto meglio senza l'uso della tua libreria.

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


14
Mi ci è voluto un po 'di tempo per imparare, ma i pezzi di codice più affidabili e senza errori sono quelli che non ci sono .
Tacroy,

1
Il motivo per cui non ho usato std :: type_info è perché stavo cercando di evitare RTTI. Ho detto che il framework era rivolto ai giochi, che è sostanzialmente il motivo per cui non sto usando typeido dynamic_cast. Grazie per il feedback, lo apprezzo molto.
miguel.martin,

Questa critica si basa sulla tua conoscenza dei motori di gioco del sistema entità-entità o è una revisione generale del codice C ++?
Den

1
@Den penso più a un problema di design che a qualsiasi altra cosa.
miguel.martin,

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. Questo è molto utile.

La mia ipotesi è che le tue lezioni manageriali probabilmente fanno molte cose. Scommetto che se dovessi segmentare quelle cose in pezzi funzionali strettamente correlati, allora probabilmente sarai in grado di inventare nomi di classe relativi ai pezzi funzionali che chiunque può dire cosa fanno semplicemente guardando il nome della classe. Ciò renderà molto più facile mantenere e comprendere il design.


1
+1 e non solo possono fare qualcosa, ma su una linea temporale abbastanza lunga lo faranno. Le persone vedono il descrittore "Manager" come un invito a creare classi lavello della cucina / coltellino svizzero.
Erik Dietrich,

@Erik - Ah, sì, avrei dovuto aggiungere che avere un buon nome di classe non è importante solo perché dice a qualcuno cosa può fare la classe; ma allo stesso modo il nome della classe dice anche cosa non dovrebbe fare la classe.
Dunk,

3

In realtà non penso che Managersia così male in questo contesto, scrollata di spalle. Se c'è un posto in cui penso Managersia perdonabile, sarebbe per un sistema a componenti di entità, dal momento che sta davvero " gestendo " la vita di componenti, entità e sistemi. Per lo meno questo è un nome più significativo qui di, diciamo,Factory che non ha molto senso in questo contesto poiché un ECS fa davvero un po 'di più che creare cose.

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

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

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

È un sacco di cose getprima che tu possa creare un'entità e sembra che il progetto stia perdendo un po 'i dettagli dell'implementazione se devi conoscere pool di entità e "gestori" per crearle. Penso che cose come "pool" e "manager" (se ti attieni a questo nome) dovrebbero essere i dettagli di implementazione dell'ECS, non qualcosa di esposto al cliente.

Ma Boh, ho visto alcuni usi molto banale e generiche di Manager, Handler, Adaptere nomi di questo genere, ma penso che questo sia un caso d'uso ragionevolmente perdonabile quando la cosa chiamata "Manager" è in realtà responsabile per la "gestione" ( "il controllo? "," maneggiamento? ") la vita degli oggetti, essendo responsabile della creazione e distruzione e fornendo accesso ad essi individualmente. Questo è per me l'uso più diretto e intuitivo di "Manager".

Detto questo, una strategia generale per evitare "Manager" nel tuo nome è semplicemente eliminare la parte "Manager" e aggiungere un -salla fine. MrGreen Ad esempio, supponiamo che tu abbia un ThreadManagered è responsabile della creazione di thread e della fornitura di informazioni su di essi e di accedervi e così via, ma non raggruppa i thread, quindi non possiamo chiamarli ThreadPool. Bene, in quel caso, lo chiami e basta Threads. Questo è quello che faccio comunque quando non sono fantasioso.

Mi viene in mente un po 'indietro quando l'equivalente analogico di "Esplora risorse" era chiamato "File Manager", in questo modo:

inserisci qui la descrizione dell'immagine

E in realtà penso che "File Manager" in questo caso sia più descrittivo di "Windows Explorer" anche se c'è qualche nome migliore là fuori che non coinvolge "Manager". Quindi almeno non esagerare con i tuoi nomi e iniziare a nominare cose come "ComponentExplorer". "ComponentManager" mi dà almeno un'idea ragionevole di cosa faccia quella cosa come qualcuno abituato a lavorare con i motori ECS.


Concordato. Se una classe viene utilizzata per compiti manageriali tipici (creazione ("assunzione"), distruzione ("licenziamento"), supervisione e interfaccia "dipendente" / superiore, il nome Managerè appropriato. La classe potrebbe avere problemi di progettazione , ma il nome è perfetto
Justin Time 2 Ripristina Monica
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.