A chave aqui não é apenas a separação de preocupações , mas também o princípio da responsabilidade única . Os dois são basicamente lados diferentes da mesma moeda: quando penso em SOC, penso de cima para baixo (tenho essas preocupações, como as separo?) Enquanto o SRP é mais de baixo para cima (tenho esse objeto, ele tem um preocupação única? Deve ser dividida? Suas preocupações já estão divididas demais?).
No seu exemplo, você tem as seguintes entidades e suas responsabilidades:
- Jogo: este é o código que faz o programa "ir".
- GameBoard: mantém o estado da área de jogo.
- Cartão: uma única entidade no tabuleiro de jogo.
- Jogador: realiza ações que alteram o estado do tabuleiro do jogo.
Depois de pensar na responsabilidade única de cada entidade, as linhas se tornam mais claras.
Em um aplicativo como um jogo, há uma classe principal que executa o loop principal, como Programa ou Jogo. Minha pergunta é: eu mantenho todas as referências a todas as instâncias de uma classe nessa classe e faço dessa a única maneira pela qual elas interagem?
Há realmente duas questões aqui a serem lembradas. A primeira coisa a decidir é que entidades sabem sobre outras entidades? Quais entidades pertencem a outras entidades?
Veja as responsabilidades que descrevi acima. Os jogadores realizam ações que mudam o estado do tabuleiro do jogo. Em outras palavras, os jogadores enviam mensagens para (chamar métodos) o tabuleiro do jogo. Essas mensagens provavelmente envolvem cartões: por exemplo, um jogador pode colocar um cartão em sua mão no tabuleiro ou alterar o estado de um cartão existente (por exemplo, vire um cartão ou mova-o para um novo local).
Claramente, um jogador deve saber sobre o tabuleiro do jogo que contradiz a suposição que você fez na sua pergunta. Caso contrário, o jogador deve enviar uma mensagem para o jogo, que então retransmitirá essa mensagem para o tabuleiro. Como os jogadores realizam ações no tabuleiro, os jogadores devem conhecer o tabuleiro. Isso aumenta o acoplamento: em vez de o jogador enviar a mensagem diretamente, agora dois atores precisam saber como enviar essa mensagem. A Lei de Demeter implica que, se um objeto deve agir em outro objeto, nesse cenário, esse outro objeto deve ser passado através de parâmetro para reduzir o acoplamento.
Em seguida, onde você armazena qual estado? O jogo é o driver aqui, ele deve vincar todos os objetos diretamente ou via proxy (por exemplo, uma fábrica ou um construtor que o jogo chama). A próxima pergunta lógica é quais objetos precisam de quais outros objetos? Isso é basicamente o que eu perguntei acima, mas uma maneira diferente de perguntar.
A maneira como eu projetaria é assim:
Jogo cria todos os objetos necessários para o jogo.
O jogo embaralha as cartas e as divide por qualquer jogo que represente (pôquer, paciência, etc.).
O jogo coloca as cartas em seus locais iniciais: talvez algumas no tabuleiro, outras nas mãos dos jogadores.
O jogo entra em seu loop principal, representando um turno.
Cada turno ficaria assim:
O jogo envia uma mensagem para (invoca um método) o jogador atual e fornece uma referência ao tabuleiro do jogo.
O Player executa qualquer lógica interna (player do computador) ou interação do usuário necessária para determinar qual peça executar.
O jogador envia uma mensagem para o tabuleiro de jogo, solicitando a alteração do estado do tabuleiro de jogo.
O tabuleiro de jogo decide se a jogada é válida ou não (é responsável por manter o estado válido do jogo).
O controle retorna ao jogo, que decide o que fazer em seguida. Verificar condições de vitória? Desenhar? Próximo jogador? Próximo turno? Depende do jogo de cartas específico que está sendo jogado.
Caso a classe Game coloque as cartas no tabuleiro, ou faz mais sentido que, por ser uma ação do jogador, ela esteja dentro da classe Player.
Ambos: O jogo é responsável pela configuração inicial, mas o Jogador executa ações no tabuleiro. O GameBoard é responsável por garantir um estado válido. Por exemplo, no clássico Paciência, apenas a carta do topo de uma pilha pode estar voltada para cima.
Voltando ao meu argumento original: você tem as separações certas de preocupações. Você identificou os objetos adequados. O que fez você se enganar foi descobrir como as mensagens fluem pelo sistema e quais objetos devem manter referências a outros objetos. Eu o projetaria assim, que é pseudocódigo:
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);
}