Quanto dovrebbe essere specifico il modello di responsabilità singola 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 intelligente tenerli tutti in un unicoinputOutput classe o li abbattere a più classi specifiche come startMenuIO, inGameIO, playerIO, gameBoardIO, ecc in modo tale che ogni classe ha circa 1-5 metodi?

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



Correlati: non ho mai visto un buon motivo per combinare input e output. E quando li separo deliberatamente, il mio codice risulta molto più pulito.
Mooing Duck

1
In quale lingua chiami le classi in lowerCamelCase, comunque?
user253751

Risposte:


7

Aggiornamento (riepilogo)

Da quando ho scritto una risposta piuttosto dettagliata, ecco cosa si riduce a:

  • Gli spazi dei nomi sono buoni, usali ogni volta che ha senso
  • L'uso inGameIOe le playerIOclassi costituirebbero probabilmente una violazione dell'SRP. Probabilmente significa che stai accoppiando il modo in cui gestisci IO con la logica dell'applicazione.
  • Avere un paio di classi IO generiche, che sono usate (o talvolta condivise) dalle classi gestore. Queste classi di gestori traducono quindi l'input non elaborato in un formato comprensibile per la logica dell'applicazione.
  • 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 lo stia guardando nel modo sbagliato. Stai separando l'IO in funzione dei componenti dell'applicazione, mentre - per me - ha più senso avere classi IO separate in base alla sorgente e al "tipo" di IO.

Avere alcune KeyboardIOclassi MouseIOdi base / generiche con cui iniziare, e quindi in base a quando e dove sono necessarie, hanno sottoclassi che gestiscono l'IO in modo diverso.
Ad esempio, l'immissione di testo è qualcosa che probabilmente vorresti gestire diversamente dai controlli di gioco. Ti ritroverai a voler mappare determinate chiavi in ​​modo diverso a seconda di ciascun caso d'uso, ma quella mappatura non fa parte dell'IO stesso, è il modo in cui gestisci 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 è dirmi cosa sta facendo l'utente.

Inietterei quindi questi oggetti in un oggetto gestore che mapperebbe l'IO grezzo su qualcosa con cui la mia logica applicativa può funzionare (ad esempio: l'utente preme "w" , il gestore lo mappa suMOVE_FORWARD ).

Questi gestori, a loro volta, sono usati per far muovere i personaggi e per disegnare lo schermo di conseguenza. Una grossolana semplificazione, ma l'essenza di questo è 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 dell'IO della tastiera nella sua forma grezza. Un'altra classe che traduce questi dati in qualcosa che il motore di gioco può effettivamente dare un senso, questi dati vengono quindi utilizzati per aggiornare lo stato di tutti i componenti coinvolti e, infine, una classe separata si occuperà dell'output sullo schermo.

Ogni singola classe ha un singolo lavoro: la gestione dell'input da tastiera viene eseguita da una classe che non sa / cura / deve sapere cosa significa l'input che sta elaborando. Tutto quello che fa è sapere come ottenere l'input (bufferizzato, senza buffer, ...).

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 tradotti e li utilizza per avvisare tutti i componenti rilevanti che qualcosa sta succedendo. Ognuno di questi componenti fa solo una cosa, che si tratti di controlli di collisione o modifiche dell'animazione del personaggio, non importa, dipende da ogni singolo oggetto.

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


Dal momento che si tratta di un'applicazione console non c'è il mouse e la stampa dei messaggi o della scheda è strettamente collegata all'input. Nel tuo esempio, sono le IOe gamespazi dei nomi o le classi con sottoclassi?
shinzou,

@kuhaku: sono spazi dei nomi. L'essenza di ciò che sto dicendo è che, se si sceglie di creare sottoclassi in base alla parte dell'applicazione in cui ci si trova, si sta effettivamente accoppiando efficacemente la funzionalità IO di base con la logica dell'applicazione. Ti ritroverai con le classi che sono responsabili dell'IO in funzione dell'applicazione . Questo, per me, sembra una violazione dell'SRP. Per quanto riguarda i nomi rispetto alle classi
spaziali

Ho aggiornato la mia risposta, per riassumere la mia risposta
Elias Van Ootegem,

Sì, questo è in realtà un altro problema che sto riscontrando (accoppiando IO con la logica), quindi in realtà consiglieresti di separare l'input dall'output?
shinzou,

@kuhaku: dipende molto da cosa stai facendo e dalla complessità delle cose di input / output. Se i gestori (che traducono lo stato del gioco contro l'input / output grezzo) differiscono troppo, probabilmente vorrai separare le classi di input e output, in caso contrario, una singola classe IO va bene
Elias Van Ootegem

23

Il principio della singola responsabilità può essere difficile da capire. Ciò che ho trovato utile è pensarlo come scrivere frasi. Non provi a racimolare molte idee in una sola frase. Ogni frase dovrebbe indicare chiaramente un'idea e rinviare i dettagli. Ad esempio, se volessi definire un'auto, diresti:

Un veicolo stradale, in genere a quattro ruote, alimentato da un motore a combustione interna.

Quindi definiresti cose come "veicolo", "strada", "ruote", ecc. Separatamente. Non proveresti a dire:

Un veicolo per il trasporto di persone su un'arteria, un percorso o una strada a terra tra due luoghi che è stato pavimentato o altrimenti migliorato per consentire il viaggio che ha quattro oggetti circolari che ruotano su un asse fissato sotto il veicolo ed è alimentato da un motore che genera forza motrice bruciando benzina, petrolio o altro combustibile con aria.

Allo stesso modo, dovresti cercare di rendere le tue classi, metodi, ecc., Affermare il concetto centrale nel modo più semplice possibile e rinviare i dettagli ad altri metodi e classi. Proprio come con la scrittura di frasi, non esiste una regola rigida su quanto dovrebbero essere grandi.


Quindi, come nel definire l'auto con poche parole anziché molte, quindi definire piccole classi specifiche ma simili va bene?
shinzou,

2
@kuhaku: Piccolo è buono. Specifico è buono. Simile va bene fintanto che lo mantieni SECCO. Stai cercando di semplificare la modifica del tuo design. Mantenere le cose piccole e specifiche ti aiuta a sapere dove apportare modifiche. Mantenerlo SECCO aiuta a evitare di cambiare molti posti.
Vaughn Cato,

6
La tua seconda frase sembra "Accidenti, insegnante ha detto che deve essere di 4 pagine ... 'genera forza motrice' è !!"
corsiKa

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.

Per quanto riguarda il tuo caso specifico, penso che avere il separato possa aiutarti a cambiare la logica di uno di quei gestori specifici senza influire sugli altri e, se necessario, sarebbe più facile per te aggiungere un nuovo metodo di input / output.


0

Il Responsabile unico responsabile afferma che una classe dovrebbe avere solo una ragione per cambiare.Se la tua classe ha più ragioni 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 solo un motivo per cambiare? In caso contrario, non abbiate paura di continuare ad aggiungere classi più specializzate fino a quando ognuna ha solo un motivo per cambiare.

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.