Gestione della segregazione dell'interfaccia in C ++

6

Sto progettando un sistema di gestione "vista" per un gioco. L'obiettivo è essere in grado di avere diverse "viste" che possono essere mostrate in sequenza o impilate l'una sull'altra. Ad esempio, la schermata iniziale iniziale è una View e il menu principale che segue la schermata iniziale è anche un View che viene visualizzato al termine della schermata iniziale. Anche la visualizzazione della telecamera del mondo di gioco è un View e l'HUD è un View che viene visualizzato in cima al gioco View .

Durante la definizione dell'interfaccia per View mi rendo conto che la natura impilata di View s corrisponde alla priorità del fuoco di input. Cioè il View che è in cima allo stack dovrebbe essere offerto per agire su qualsiasi input dell'utente prima di qualsiasi altro View s perché in sostanza è in "foreground" per lo stato attivo.

Quindi il design dell'interfaccia per View sembra al momento:

/**
 * Interface for a "view". A view is a renderable target that can accept user input.
 *
 * One can think of it as a "layer", many layers can be drawn over each other in a
 * stack like fashion where the input focus travels from the top to the bottom of
 * the stack.
 */
class IView{
public:
    virtual ~IView() {}

    IView(const IView&) = delete; // Not allowed
    void operator = (const IView&) = delete; // Not allowed

    /**
     * Called to handle user keyboard input. May be called multiple times per update.
     * @param aKeyEvent The type of input event.
     * @param aKeyCode The key code of the event key.
     * @param aKeyboardState The current state of they keyboard.
     * @return True if the event was handled. False if the event should continue to lower views.
     */
    virtual bool handleInput(KeyEvent aKeyEvent, KeyCode aKeyCode, const KeyboardState& aKeyboardState) = 0;

    /**
     * Called to handle user mouse clicks. May be called multiple times per update.
     * @param aMouseButtonEvent The event to handle.
     * @param aMouseButton Which button it was.
     * @param aMouseState The current mouse state.
     * @return True if the event was handled. False if the event should continue to lower views.
     */
    virtual bool handleInput(MouseButtonEvent aMouseButtonEvent, MouseButton aMouseButton,
                             const MouseState& aMouseState) = 0;

    /**
     * Called to handle user mouse movement. Only called once per update.
     * @param aMouseEvent The event to handle.
     * @param aMouseState
     * @return True if the event was handled. False if the event should continue to lower views.
     */
    virtual bool handleInput(MouseMoveEvent aMouseMoveEvent, const MouseState& aMouseState) = 0;

    /**
     * Called when this view becomes the foreground view.
     */
    virtual void foreground() = 0;

    /**
     * Called when this view loses its foreground status and
     * has another view drawn over it.
     */
    virtual void background() = 0;

    /**
     * Called once every frame to perform periodic processing and rendering.
     */
    virtual void update() = 0;
};

Questa non è l'interfaccia finale, sto solo prototipando al momento. Ma ti dà un'idea.

Considera un gioco View , con un HUD in cima, e quindi il gioco viene messo in pausa in modo che una pausa View sia resa in cima all'HUD. La pausa View offre la prima possibilità di agire sull'input dell'utente per continuare il gioco e ingoia tutti gli altri input. Per me questo ha perfettamente senso.

Ora, non tutti i View s sono necessariamente interessati a ricevere input. Alcuni potrebbero essere interessati solo agli eventi del mouse e alcuni potrebbero essere interessati solo agli eventi della tastiera. Il mio approccio iniziale era di lasciargli avere gestori di input pass-through. Ma poi ho ricordato il principio di segregazione dell'interfaccia , quindi avrebbe senso definire altre due interfacce IKeyboardHandler e IMouseHandler e lascia che le pertinenti classi View implementino quelle quando necessario. Ma poi ho bisogno di usare dynamic_cast quando passi l'input agli oggetti View per vedere se un particolare View implementa per esempio IKeyboardHandler .

Non sono sicuro che questo approccio sia più elegante di avere gestori di input vuoti e sto cercando input e idee sul design.

    
posta Emily L. 04.10.2014 - 14:51
fonte

2 risposte

3

Il principio di segregazione dell'interfaccia si concentra maggiormente su funzionalità completamente irrilevanti. Qui non è così chiaro.

Un esempio evidente di violazione dell'ISP è un'interfaccia Scrollbar con una funzione setText ereditata da Widget , quando una barra di scorrimento non mostra nemmeno testo. In tal caso, la funzione potrebbe persino essere documentata come irrilevante per i chiamanti e tentare di chiamare tale funzione su una barra di scorrimento potrebbe indicare un errore logico.

Simili vistose violazioni dell'ISP tendono ad essere più sporche, con interfacce monolitiche che potrebbero avere 200 funzioni e solo 20 di esse sono rilevanti per l'oggetto reale in questione. Lo vedo spesso nei progetti di GUI basati su enormi gerarchie di ereditarietà e spesso con meccanismi di documentazione che cercano di filtrare l'enorme quantità di funzionalità derivate irrilevanti.

Nel tuo caso, penso che sia un po 'diverso dato una funzione come questa:

virtual bool handleInput(KeyEvent aKeyEvent, ...) = 0;

... è ancora rilevante per i client se una vista gestisce o meno l'input da tastiera. Il fatto che non sia ancora informazioni rilevanti, e vale la pena chiamare la funzione per scoprirlo. Quindi non vedo questo come una chiara violazione dell'ISP.

Potresti provare a suddividere l'interfaccia in più interfacce come IKeyboardHandler , ma è probabile che renderà le cose più difficili invece che più semplici. Vale la pena notare lo scopo qui in termini di dipendenze.

Un gioco in genere ha solo una manciata di tali visualizzazioni. Potresti avere una schermata del menu principale di sorta, schermata iniziale, HUD, vista di gioco, forse un menu di torta nel gioco o qualcosa del genere. È una manciata di punti di vista. Anche i clienti tendono ad essere pochi, forse un solo cliente ampio che gestisce gli eventi dal sistema operativo e chiama le relative funzioni di visualizzazione. Quindi è molto limitato in ambito / scala quando si tratta del numero di dipendenze, e questo è un segno che non è necessario impegnarsi troppo per trovare una soluzione ingegneristica di qualità, e questo potrebbe contraddire molto bene i tuoi obiettivi per facilità di manutenzione (facilità di modifica).

Evita la cast dinamica

Detto questo, solo per completezza, ti mostrerò un modo semplice per evitare il cast in questo tipo di scenari. Puoi fare ciò:

class IView{
public:
    ...

    // Returns null if keyboard handling is unsupported.
    virtual IKeyboardHandler* keyboard() = 0;

    // Returns null if mouse handling is unsupported.
    virtual IMouseHandler* mouse() = 0;
};

... visualizzare i tipi che non hanno bisogno di ereditare da (attuare) IKeyboardHandler può semplicemente restituire null, ad es. Quelli che possono semplicemente return this; . In questo modo puoi scoprire se una vista supporta la gestione della tastiera con una query dell'interfaccia che non prevede il cast e, in tal caso, gestire l'input da tastiera per quella vista.

Portare questa tecnica a un livello maggiore di flessibilità ti porterà spesso a progettare modelli COM (che implicano il casting ma lo nascondono al client), e ancora più a sistemi di componenti di entità (che possono essere molto utili per conoscere come sviluppatore di giochi, ma probabilmente al di fuori di questo contesto di visualizzazione). Tuttavia, ritengo che questo sia un eccessivo assoluto per il tuo caso, anche con la forma di base, e che probabilmente aggiunga altri oneri in anticipo senza alleviare i problemi successivi.

Tuttavia questa versione di base mostrata sopra può essere un'utile tecnica di progettazione se tu avessi, per esempio, funzioni nel tuo sistema che possono funzionare strettamente con IKeyboardHandler , e potenzialmente hai cose diverse dalle viste che potrebbero implementare questa interfaccia per la gestione della tastiera.

    
risposta data 07.01.2016 - 23:30
fonte
0

Non usare uno schema solo perché qualcuno dice che è una buona idea. Seguire i modelli di progettazione è un antipattern.

Mi sembra che la gestione di tastiera e mouse sia assolutamente parte di View , non di qualcos'altro.

Probabilmente dovrebbe esserci una classe base ChainedView che implementa View , che offre un'implementazione predefinita dei metodi virtuali per la gestione di tastiera e mouse. Alcuni View s, come pausa, sovrascriveranno quello per accettare solo input che capiscono e ignorano il resto - o forse non saranno nemmeno un ChainedView . La maggior parte di View s la sovrascriverà per gestire ciò che possono e passare il resto alla successiva vista concatenata. Qualsiasi View che non ha bisogno di gestire l'input stesso lo passerà automaticamente, quindi è già presente una separazione.

    
risposta data 05.10.2014 - 03:17
fonte

Leggi altre domande sui tag