Quanto dovrebbe essere specifico il pattern di Responsabilità Unica per le classi?

14

Ad esempio, supponiamo di avere un programma di gioco per console, che ha tutti i tipi di metodi di input / output da e verso la console. Sarebbe opportuno tenerli tutti in una sola classe inputOutput o suddividerli in classi più specifiche come startMenuIO , inGameIO , playerIO , gameBoardIO , ecc. In modo che ogni classe abbia circa 1 5 metodi?

E sulla stessa nota, se è meglio scomporli, sarebbe intelligente metterli in uno spazio dei nomi IO , rendendoli così un po 'più prolissi, ad es .: IO.inGame ecc .?

    
posta shinzou 20.04.2016 - 11:46
fonte

4 risposte

7

Aggiornamento (recap)

Dato che ho scritto una risposta piuttosto prolissa, ecco cosa si riassume in:

  • I namespace sono buoni, li usano quando ha senso
  • Utilizzare le classi inGameIO e playerIO potrebbe costituire una violazione dell'SRP. Probabilmente significa che stai accoppiando il modo in cui gestisci l'IO con la logica dell'applicazione.
  • Avere un paio di classi IO generiche, che vengono utilizzate (o talvolta condivise) dalle classi del gestore. Queste classi di gestori traducono quindi l'input non elaborato in un formato in cui la logica dell'applicazione può dare un senso.
  • Lo stesso vale per l'output: questo può essere fatto da classi abbastanza generiche, ma passa lo stato del gioco attraverso un oggetto handler / mapper che traduce lo stato del gioco interno in qualcosa che le classi IO generiche possono gestire.

Penso che tu stia guardando questo nel modo sbagliato. Stai separando l'IO in funzione dei componenti dell'applicazione, mentre per me ha più senso avere classi IO separate basate sul sorgente e "tipo" di IO.

Avendo alcune classi base / generiche KeyboardIO MouseIO per iniziare, e poi in base a quando e dove ne hai bisogno, avere sottoclassi che gestiscono diversamente detto IO.
Ad esempio, l'inserimento di testo è qualcosa che probabilmente vorresti gestire in modo diverso dai controlli di gioco. Ti troverai a voler mappare alcune chiavi in modo diverso a seconda dei casi d'uso, ma quella mappatura non fa parte dell'IO stesso, è come gestirai l'IO.

Seguendo l'SRP, avrei un paio di classi che posso usare per l'IO della tastiera. A seconda della situazione, probabilmente vorrò interagire con queste classi in modo diverso, ma il loro unico compito è quello di dirmi cosa sta facendo l'utente.

Inietterei quindi questi oggetti in un oggetto gestore che potrebbe mappare l'IO grezzo su qualcosa con cui la logica della mia applicazione può lavorare (ad esempio: pressioni utente "w" , il gestore mappa che su MOVE_FORWARD ).

Questi gestori, a loro volta, sono utilizzati per far muovere i personaggi e disegnare lo schermo di conseguenza. Una grossolana semplificazione, ma il nocciolo della questione è questo tipo di struttura:

[ IO.Keyboard.InGame ] // generic, if SoC and SRP are strongly adhered to, changing this component should be fairly easy to do
   ||
   ==> [ Controls.Keyboard.InGameMapper ]

[ Game.Engine ] <- Controls.Keyboard.InGameMapper
                <- IO.Screen
                <- ... all sorts of stuff here
    InGameMapper.move() //returns MOVE_FORWARD or something
      ||
      ==> 1. Game.updateStuff();//do all the things you need to do to move the character in the given direction
          2. Game.Screen.SetState(GameState); //translate the game state (inverse handler)
          3. IO.Screen.draw();//generate actual output

Ciò che abbiamo ora è una classe che è responsabile della tastiera IO nella sua forma grezza. Un'altra classe che traduce questi dati in qualcosa che il motore di gioco può effettivamente dare un senso, viene quindi utilizzata per aggiornare lo stato di tutti i componenti coinvolti e, infine, una classe separata si prenderà cura dell'output sullo schermo. / p>

Ogni singola classe ha un singolo lavoro: la gestione dell'input da tastiera viene eseguita da una classe che non conosce / cura / deve sapere che cosa significa l'input che sta elaborando. Tutto ciò che fa è sapere come ottenere l'input (buffered, unbuffered, ...).

Il gestore lo traduce in una rappresentazione interna per il resto dell'applicazione per dare un senso a queste informazioni.

Il motore di gioco prende i dati che sono stati tradotti e li usa per notificare a tutti i componenti rilevanti che qualcosa sta succedendo. Ognuno di questi componenti fa solo una cosa, indipendentemente dal fatto che si tratti di controlli di collisione o di modifiche all'animazione del personaggio, non importa, questo dipende da ogni singolo oggetto.

Questi oggetti quindi ritrasmettono il loro stato e questi dati vengono passati a Game.Screen , che è essenzialmente un gestore di IO inverso. Mappa la rappresentazione interna su qualcosa che il componente IO.Screen può utilizzare per generare l'output effettivo.

    
risposta data 20.04.2016 - 15:47
fonte
23

Il principio di responsabilità singola può essere difficile da capire. Quello che ho trovato utile è pensarlo come il modo in cui scrivi le frasi. Non provi a racchiudere un sacco di idee in una singola frase. Ogni frase dovrebbe indicare chiaramente un'idea e rinviare i dettagli. Ad esempio, se si desidera definire un'auto, si direbbe:

A road vehicle, typically with four wheels, powered by an internal combustion engine.

Quindi definiresti cose come "veicolo", "strada", "ruote", ecc. separatamente. Non proverai a dire:

A vehicle for transporting people on a thoroughfare, route, or way on land between two places that has been paved or otherwise improved to allow travel that has four circular objects that revolve on an axle fixed below the vehicle and is powered by an engine that generates motive power by the burning of gasoline, oil, or other fuel with air.

Allo stesso modo, dovresti provare a rendere le tue classi, i metodi, ecc., il concetto centrale nel modo più semplice possibile e rinviare i dettagli ad altri metodi e classi. Proprio come con le frasi di scrittura, non ci sono regole rigide su quanto dovrebbero essere grandi.

    
risposta data 20.04.2016 - 16:31
fonte
2

Direi che il modo migliore per andare è tenerli in classi separate. Le classi piccole non sono male, in realtà il più delle volte sono una buona idea.

Riguardo al tuo caso specifico, penso che avere il separato possa aiutarti a cambiare la logica di uno di questi specifici gestori senza influenzare gli altri e, se necessario, sarebbe più facile per te aggiungere un nuovo metodo di input / output se fosse venuto ad esso.

    
risposta data 20.04.2016 - 11:52
fonte
0

Il Principal della singola responsabilità afferma che una classe dovrebbe avere solo una ragione per cambiare. Se la tua classe ha più motivi per cambiare, puoi dividerla in altre classi e utilizzare la composizione per eliminare questo problema.

Per rispondere alla tua domanda, devo farti una domanda: la tua classe ha una sola ragione per cambiare? In caso contrario, non abbiate paura di continuare ad aggiungere altre classi specializzate fino a quando ognuna ha solo una ragione per cambiare.

    
risposta data 20.04.2016 - 18:22
fonte