La chiave qui non è solo separazione delle preoccupazioni , ma anche principio di responsabilità singola . I due sono lati fondamentalmente diversi della stessa medaglia: quando penso che il SOC ritengo top-down (ho queste preoccupazioni, come li separo?) Mentre SRP è più dal basso (ho questo oggetto, ha un preoccupazione singola? Dovrebbe essere divisa? Le sue preoccupazioni sono già troppo divise?)
Nel tuo esempio hai le seguenti entità e le loro responsabilità:
- Gioco: questo è il codice che rende il programma "vai".
- GameBoard: mantiene lo stato dell'area di gioco.
- Carta: una singola entità sul tabellone.
- Giocatore: esegue azioni che cambiano lo stato del tabellone.
Una volta che pensi alla singola responsabilità >> di ciascuna entità, le linee diventano più chiare.
In an app such as a game, there is a main class that runs the main
loop, such as Program or Game. My question is, do I maintain every
reference to every instance of a class in this class, and make that
the only way in which they interact?
Qui ci sono davvero due problemi da tenere a mente. La prima cosa da decidere è quali entità conoscono altre entità? Quali entità appartengono ad altre entità?
Guarda le responsabilità che ho delineato sopra. I giocatori eseguono azioni che cambiano lo stato del tabellone. In altre parole, i giocatori inviano messaggi a (chiama i metodi su) il tabellone. Questi messaggi riguardano probabilmente le carte: per esempio, un giocatore può mettere una carta in mano sul tabellone, o cambiare lo stato di una carta esistente (ad esempio girare una carta o spostarla in una nuova posizione).
Chiaramente, un giocatore deve conoscere il tabellone di gioco che contraddice l'assunto che hai fatto nella tua domanda. Altrimenti, il giocatore deve inviare un messaggio al gioco, che quindi trasmette quel messaggio al tabellone. Poiché i giocatori eseguono azioni su il tabellone di gioco, i giocatori devono essere a conoscenza del tabellone. Questo aumenta l'accoppiamento: invece che il giocatore che invia il messaggio direttamente, ora due attori devono sapere come inviare quel messaggio. La Legge di Demetra implica che se un oggetto deve agire su un altro oggetto, in questo scenario, quell'altro oggetto deve essere passato in tramite parametro per ridurre l'accoppiamento.
Quindi, dove memorizzi quale stato? Il gioco è il driver qui, deve creare tutti gli oggetti direttamente o tramite proxy (ad esempio una fabbrica o un costruttore che chiama il gioco). La seguente domanda logica è: quali oggetti hanno bisogno di quali altri oggetti? Questo è fondamentalmente quello che ho chiesto sopra, ma un modo diverso di chiedermelo.
Il modo in cui lo progetterei è come questo:
-
Il gioco crea tutti gli oggetti necessari per il gioco.
-
Il gioco mescola le carte e le divide in base al gioco che rappresenta (poker, solitario, ecc.)
-
Il gioco posiziona le carte nelle loro posizioni iniziali: forse alcune sulla plancia di gioco, altre nelle mani dei giocatori.
-
Il gioco entra quindi nel suo ciclo principale che rappresenta un turno.
Ogni turno sarebbe simile a questo:
-
Il gioco invia un messaggio a (invoca un metodo su) il giocatore corrente e fornisce un riferimento al tabellone.
-
Il giocatore esegue qualsiasi logica interna (lettore di computer) o interazione dell'utente necessaria per determinare quale gioco eseguire.
-
Il giocatore manda un messaggio alla plancia di gioco fornita chiedendogli di cambiare lo stato della scheda di gioco.
-
Il tabellone di gioco decide se il movimento è valido o meno (è responsabile per mantenere lo stato di gioco valido).
-
Il controllo ritorna al gioco, che poi decide cosa fare dopo. Verifica le condizioni di vittoria? Disegnare? Prossimo giocatore? Prossima svolta? Dipende dallo specifico gioco di carte che si sta giocando.
Should it be up to the Game class to place the cards on the board, or
does it make more sense that, because it is the player's action, it
should be within the Player class.
Entrambi: il gioco è responsabile della configurazione iniziale, ma il giocatore esegue le azioni alla lavagna. GameBoard è responsabile della garanzia di uno stato valido. Ad esempio, nel solitario classico, solo la prima carta di una pila può essere scoperta.
Ritorno al punto originale: hai le giuste separazioni di preoccupazioni. Hai identificato gli oggetti giusti. Ciò che ti ha inciampato è stato capire come i messaggi fluiscono attraverso il sistema e quali oggetti dovrebbero mantenere i riferimenti ad altri oggetti. Lo progetterei in questo modo, che è pseudocodice:
class Game {
main();
}
class GameBoard {
// Data structures specific to the game being played. There is a
// lot of hand-waving here to give the general idea without
// getting bogged down in the implementation.
Map<Card, Location> cards;
GameBoard(Map<Card, Location>);
// Return false if the move is invalid.
bool flip(Card);
bool move(Card, Location);
}
class Card {
// Make Rank and Suit enums.
Suit suit;
Rank rank;
bool faceUp;
}
class Player {
Set<Card> hand;
Player(Set<Card>);
void takeTurn(GameBoard);
}