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