Manter a separação de preocupações

8

Estou criando meu primeiro aplicativo C # e estou tendo um pouco de dificuldade com a separação de preocupações. Eu entendo o conceito, mas não sei se estou fazendo certo. Eu tenho isso como um exemplo rápido para ilustrar minha pergunta. 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?

Por exemplo, um jogo de cartas com um jogador, cartas e um tabuleiro. Digamos que o jogador queira colocar cartas no tabuleiro, mas a classe Jogador apenas possui uma Lista <> de Cartas e não faz ideia do tabuleiro do jogo. No entanto, a classe principal do jogo conhece os jogadores, as cartas e o tabuleiro. 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.

Exemplo:

public class Game{
    private GameBoard gameBoard;
    private Player[] players;

   public Game(){
     gameBoard = new GameBoard(10,10);
     Player player1 = new Player();
     Player player2 = new Player();
     players = {player1, player2};
   }

   // Create method here?
   public void PlayerPlaceCard(int x, int y, int cardIndex){
      gameBoard.grid[1,1] = player1.cards[cardIndex];
   }
}

public class Player {
     public List<Cards> cards = new List<Cards>();

     public Player(){
     }

     // Or place method here?
     public PlaceCard(Card card, int x, int y, GameBoard gameBoard){
     }
}

public class GameBoard{
    public Card[,] grid;

    public GameBoard(int x, int y){
       // Make the game board
    }
}

public class Card{
   public string name;
   public string value;
}

Para mim, faz mais sentido ter o método no jogo, porque o jogo sabe de tudo. Mas à medida que adiciono mais código, o jogo está ficando bastante inchado e estou escrevendo muitas funções PlayerDoesThis ().

Obrigado por qualquer conselho

tones31
fonte

Respostas:

12

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);
}

fonte
1
Boa resposta, apenas uma coisa que sinto falta. No exemplo do OP, ele está passando um x, y para o tabuleiro de jogo para dizer exatamente onde colocar o cartão. Embora o jogador chame o tabuleiro para colocar uma carta, deve ser o tabuleiro que decide que x, y. Exigir que o jogador saiba sobre as coordenadas do tabuleiro cria uma abstração com vazamento.
David Arno
@DavidArno se as cartas podem ser jogadas em locais em uma grade retangular, como o jogador deve indicar em qual delas as jogará, se não por coordenadas? (Aqueles que não são coordintes tela, mas coordenadas de grade.)
Paulo Ebermann
1
Os detalhes específicos sobre como colocar o cartão devem ser abstraídos de alguma forma por uma classe Location, que seria uma peça muito menor nesse design. Claro, as coordenadas podem funcionar. Também pode ser mais apropriado usar locais nomeados como "descartar pilha". Os detalhes da implementação não são importantes ao mapear o design, conforme indicado na pergunta.
@ Snowman Obrigado, esta resposta está no local. Se tivéssemos o Player sempre atuando no GameBoard, não faria mais sentido ter uma referência local dentro da classe, que seria definida durante o construtor? Dessa forma, o GameBoard não precisaria ser transmitido ao Player todas as vezes (haverá muitas interações com o quadro).
tones31
@DavidArno Na verdade, o jogador precisa informar ao tabuleiro onde colocá-lo; o tabuleiro precisa validá-lo. O jogador pode pegar e mover cartas.
tones31