Mantener la separación de las preocupaciones.

8

Estoy haciendo mi primera aplicación C # y tengo un poco de dificultad para separar las preocupaciones. Entiendo el concepto, pero no sé si lo estoy haciendo bien. Tengo esto como un ejemplo rápido para ilustrar mi pregunta. En una aplicación como un juego, hay una clase principal que ejecuta el bucle principal, como Programa o Juego. Mi pregunta es, ¿mantengo cada referencia a cada instancia de una clase en esta clase, y hago que sea la única forma en que interactúan?

Por ejemplo, un juego de cartas con un jugador, cartas y un tablero. Digamos que el jugador quiere poner cartas en el tablero, pero la clase Jugador solo tiene una Lista <> de Cartas y no tiene idea del tablero de juego. Sin embargo, la clase principal del juego sabe acerca de los jugadores, las cartas y el tablero de juego. Si corresponde a la clase de Juego colocar las cartas en el tablero, o tiene más sentido que, debido a que es la acción del jugador, debería estar dentro de la clase de Jugador.

Ejemplo:

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 mí tiene más sentido tener el método en Game, porque Game sabe de todo. Pero a medida que agrego más código, el juego se está volviendo bastante hinchado y estoy escribiendo muchas funciones PlayerDoesThis ().

Gracias por cualquier consejo

tonos31
fuente

Respuestas:

12

La clave aquí no es solo la separación de las preocupaciones , sino también el principio de responsabilidad única . Las dos son básicamente caras diferentes de la misma moneda: cuando pienso en SOC pienso de arriba hacia abajo (tengo estas preocupaciones, ¿cómo las separo?) Mientras que SRP es más de abajo hacia arriba (tengo este objeto, ¿tiene un ¿preocupación única? ¿Debería dividirse? ¿Sus preocupaciones ya se dividen demasiado?).

En su ejemplo, tiene las siguientes entidades y sus responsabilidades:

  • Juego: este es el código que hace que el programa "funcione".
  • GameBoard: mantiene el estado del área de juego.
  • Carta: una sola entidad en el tablero de juego.
  • Jugador: realiza acciones que cambian el estado del tablero de juego.

Una vez que piensa en la responsabilidad individual de cada entidad, las líneas se vuelven más claras.

En una aplicación como un juego, hay una clase principal que ejecuta el bucle principal, como Programa o Juego. Mi pregunta es, ¿mantengo cada referencia a cada instancia de una clase en esta clase, y hago que sea la única forma en que interactúan?

Realmente hay dos problemas aquí para tener en cuenta. Lo primero que debe decidir es qué entidades saben sobre otras entidades. ¿Qué entidades pertenecen a otras entidades?

Mire las responsabilidades que describí anteriormente. Los jugadores realizan acciones que cambian el estado del tablero de juego. En otras palabras, los jugadores envían mensajes a (métodos de llamada en) el tablero de juego. Es probable que esos mensajes involucren cartas: por ejemplo, un jugador puede colocar una carta en su mano en el tablero o cambiar el estado de una carta existente (por ejemplo, dar la vuelta a una carta o moverla a una nueva ubicación).

Claramente, un jugador debe saber sobre el tablero de juego que contradice la suposición que hizo en su pregunta. De lo contrario, el jugador debe enviar un mensaje al juego, que luego transmite ese mensaje al tablero de juego. Dado que los jugadores realizan acciones en el tablero de juego, los jugadores deben saber sobre el tablero de juego. Esto aumenta el acoplamiento: en lugar de que el jugador envíe el mensaje directamente, ahora dos actores deben saber cómo enviar ese mensaje. La Ley de Demeter implica que si un objeto debe actuar sobre otro objeto, en este escenario, ese otro objeto debe pasarse a través de un parámetro para reducir el acoplamiento.

A continuación, ¿dónde almacena qué estado? El juego es el controlador aquí, debe plegar todos los objetos ya sea directamente o mediante proxy (por ejemplo, una fábrica o en un constructor que el juego llama). La siguiente pregunta lógica es ¿qué objetos necesitan qué otros objetos? Esto es básicamente lo que pregunté anteriormente, pero es una forma diferente de preguntar.

La forma en que lo diseñaría es así:

  • El juego crea todos los objetos necesarios para el juego.

  • El juego baraja las cartas y las divide por cualquier juego que represente (póker, solitario, etc.).

  • El juego coloca las cartas en sus ubicaciones iniciales: tal vez algunas en el tablero de juego, otras en las manos de los jugadores.

  • El juego entra en su bucle principal que representa un turno.

Cada turno se vería así:

  • El juego envía un mensaje al jugador actual (invoca un método) y proporciona una referencia al tablero de juego.

  • El jugador realiza cualquier lógica interna (jugador de computadora) o interacción del usuario necesaria para determinar qué juego realizar.

  • El jugador envía un mensaje al tablero de juego provisto pidiéndole que cambie el estado del tablero de juego.

  • El tablero de juego decide si el movimiento es válido o no (es responsable de mantener un estado de juego válido).

  • El control vuelve al juego, que luego decide qué hacer a continuación. ¿Verifica las condiciones de victoria? ¿Dibujar? Próximo jugador? ¿Siguiente turno? Depende del juego de cartas específico que se esté jugando.

Si corresponde a la clase de Juego colocar las cartas en el tablero, o tiene más sentido que, debido a que es la acción del jugador, debería estar dentro de la clase de Jugador.

Ambos: el juego es responsable de la configuración inicial, pero el jugador realiza acciones en el tablero. GameBoard es responsable de garantizar un estado válido. Por ejemplo, en el solitario clásico, solo la carta superior de una pila puede estar boca arriba.


Volviendo a mi punto original: tienes las separaciones correctas de preocupaciones. Identificaste los objetos adecuados. Lo que lo hizo tropezar fue descubrir cómo fluyen los mensajes a través del sistema y qué objetos deben mantener referencias a otros objetos. Lo diseñaría así, que es 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);
}

fuente
1
Buena respuesta, solo falta una cosa que siento. En el ejemplo del OP, está pasando una x, y al tablero de juego para decirle con precisión dónde colocar la tarjeta. Mientras que el jugador llamará al tablero para colocar una carta, debe ser el tablero el que decida que x, y. Requerir que el jugador sepa sobre las coordenadas del tablero crea una abstracción permeable.
David Arno
@DavidArno si las cartas se pueden jugar en ubicaciones en una cuadrícula rectangular, ¿cómo debe indicar el jugador en cuál de ellas jugar, si no por coordenadas? (Esas no son coordenadas de pantalla, sino coordenadas de cuadrícula.)
Paŭlo Ebermann
1
Los detalles específicos sobre cómo colocar la tarjeta deben ser abstraídos de alguna manera por una clase de ubicación, que sería una pieza muy menor en este diseño. Claro, las coordenadas podrían funcionar. También podría ser más apropiado usar ubicaciones con nombre como "pila de descarte". Los detalles de implementación no son importantes al mapear el diseño como se indica en la pregunta.
@Snowman Gracias, esta respuesta es acertada. Si tenemos al Jugador siempre actuando sobre el Tablero de juego, ¿no tendría más sentido tener una referencia local dentro de la clase, que se establecería durante su constructor? De esa manera, GameBoard no tendría que pasarse al jugador cada vez (habrá muchas interacciones con el tablero).
tonos31
@DavidArno El jugador realmente necesita decirle al tablero dónde colocarlo, el tablero debe validarlo. El jugador puede recoger y mover cartas.
tonos31