Una tarea en mi clase de ingeniería de software es diseñar una aplicación que pueda jugar diferentes formas en un juego en particular. El juego en cuestión es Mancala, algunos de estos juegos se llaman Wari o Kalah. Estos juegos difieren en algunos aspectos, pero para mi pregunta solo es importante saber que los juegos podrían diferir en lo siguiente:
- La forma en que se maneja el resultado de un movimiento
- La forma en que se determina el final del juego
- La forma en que se determina el ganador
Lo primero que se me ocurrió al diseñar esto fue usar el patrón de estrategia, tengo una variación en los algoritmos (las reglas reales del juego). El diseño podría verse así:
Entonces pensé para mí mismo que en el juego de Mancala y Wari la forma en que se determina el ganador es exactamente la misma y el código se duplicaría. No creo que esto sea, por definición, una violación de la 'regla única, un lugar' o el principio DRY, ya que un cambio en las reglas para Mancala no significaría automáticamente que esa regla deba cambiarse también en Wari. Sin embargo, por los comentarios que recibí de mi profesor, tuve la impresión de encontrar un diseño diferente.
Entonces se me ocurrió esto:
Cada juego (Mancala, Wari, Kalah, ...) solo tendría un atributo del tipo de interfaz de cada regla, es decir, WinnerDeterminer
y si hay una versión de Mancala 2.0 que sea igual a Mancala 1.0, excepto cómo se determina el ganador, puede usa las versiones de Mancala.
Creo que la implementación de estas reglas como patrón de estrategia es ciertamente válida. Pero el verdadero problema viene cuando quiero diseñarlo más.
Al leer sobre el patrón de método de plantilla, inmediatamente pensé que podría aplicarse a este problema. Las acciones que se realizan cuando un usuario realiza un movimiento son siempre las mismas y en el mismo orden, a saber:
- depositar piedras en los agujeros (esto es lo mismo para todos los juegos, por lo que se implementaría en el método de la plantilla en sí)
- determinar el resultado del movimiento
- determinar si el juego ha terminado debido al movimiento anterior
- si el juego ha terminado, determina quién ganó
Esos tres últimos pasos están todos en mi patrón de estrategia descrito anteriormente. Tengo muchos problemas para combinar estos dos. Una posible solución que encontré sería abandonar el patrón de estrategia y hacer lo siguiente:
¿Realmente no veo la diferencia de diseño entre el patrón de estrategia y esto? Pero estoy seguro de que necesito usar un método de plantilla (aunque estaba tan seguro de tener que usar un patrón de estrategia).
Tampoco puedo determinar quién sería responsable de crear el TurnTemplate
objeto, mientras que con el patrón de estrategia siento que tengo familias de objetos (las tres reglas) que podría crear fácilmente usando un patrón de fábrica abstracto. Entonces tendría una MancalaRuleFactory
, WariRuleFactory
, etc, y que crearían las instancias correctas de las reglas y la mano ME volver un RuleSet
objeto.
Digamos que uso el patrón de estrategia + fábrica abstracta y tengo un RuleSet
objeto que tiene algoritmos para las tres reglas. La única forma en que siento que todavía puedo usar el patrón de método de plantilla con esto es pasar este RuleSet
objeto a mi TurnTemplate
. El "problema" que luego surge es que nunca necesitaría mis implementaciones concretas de TurnTemplate
, estas clases se volverían obsoletas. En mis métodos protegidos en el TurnTemplate
solo podría llamar ruleSet.determineWinner()
. Como consecuencia, la TurnTemplate
clase ya no sería abstracta, sino que tendría que volverse concreta, ¿sigue siendo un patrón de método de plantilla?
Para resumir, ¿estoy pensando de la manera correcta o me estoy perdiendo algo fácil? Si estoy en el camino correcto, ¿cómo combino un patrón de estrategia y un patrón de método de plantilla?
fuente
Respuestas:
Después de mirar sus diseños, su primera y tercera iteraciones parecen ser diseños más elegantes. Sin embargo, mencionas que eres un estudiante y tu profesor te dio algunos comentarios. Sin saber exactamente cuál es su tarea o el propósito de la clase o más información sobre lo que sugirió su profesor, tomaría todo lo que digo a continuación con un grano de sal.
En su primer diseño, declara
RuleInterface
que es una interfaz que define cómo manejar el turno de cada jugador, cómo determinar si el juego ha terminado y cómo determinar un ganador después de que el juego termina. Parece que es una interfaz válida para una familia de juegos que experimenta variación. Sin embargo, dependiendo de los juegos, es posible que tenga un código duplicado. Estoy de acuerdo en que la flexibilidad para cambiar las reglas de un juego es algo bueno, pero también argumentaría que la duplicación de código es terrible por defectos. Si copia / pega código defectuoso entre implementaciones y uno tiene un error, ahora tiene varios errores que deben corregirse en diferentes ubicaciones. Si reescribe las implementaciones en diferentes momentos, podría introducir defectos en diferentes ubicaciones. Ninguno de esos es deseable.Su segundo diseño parece bastante complejo, con un profundo árbol de herencia. Al menos, es más profundo de lo que esperaría para resolver este tipo de problema. También está comenzando a dividir los detalles de implementación en otras clases. En última instancia, estás modelando e implementando un juego. Este podría ser un enfoque interesante si fuera necesario mezclar y combinar sus reglas para determinar los resultados de un movimiento, el final del juego y un ganador, que no parece estar en los requisitos que ha mencionado . Sus juegos son conjuntos de reglas bien definidos, y trataría de encapsular los juegos tanto como pueda en entidades separadas.
Su tercer diseño es el que más me gusta. Mi única preocupación es que no está en el nivel correcto de abstracción. En este momento, parece que estás modelando un turno. Recomendaría considerar diseñar el juego. Considera que tienes jugadores que están haciendo movimientos en algún tipo de tablero, usando piedras. Tu juego requiere que estos actores estén presentes. A partir de ahí, su algoritmo no es
doTurn()
sinoplayGame()
, que va del movimiento inicial al movimiento final, después de lo cual termina. Después del movimiento de cada jugador, ajusta el estado del juego, determina si el juego está en un estado final y, si lo está, determina el ganador.Recomiendo echar un vistazo más de cerca a su primer y tercer diseño y trabajar con ellos. También podría ayudar pensar en términos de prototipos. ¿Cómo serían los clientes que usan estas interfaces? ¿Un enfoque de diseño tiene más sentido para implementar un cliente que realmente va a instanciar un juego y jugarlo? Debes darte cuenta de con qué está interactuando. En su caso particular, es la
Game
clase y cualquier otro elemento asociado: no puede diseñar de forma aislada.Como mencionas que eres un estudiante, me gustaría compartir algunas cosas de una época en que era TA para un curso de diseño de software:
fuente
GameTemplate
que se siente mucho mejor. También me permite combinar un método de fábrica para inicializar jugadores, el tablero, etc.Tu confusión está justificada. La cuestión es que los patrones no son mutuamente excluyentes.
El método de plantilla es la base de muchos otros patrones, como Estrategia y Estado. Esencialmente, la interfaz de Estrategia contiene uno o más métodos de plantilla, cada uno de los cuales requiere que todos los objetos que implementan una estrategia tengan (al menos) algo así como un método doAction (). Esto permite que las estrategias se sustituyan entre sí.
En Java, una interfaz no es más que un conjunto de métodos de plantilla. Del mismo modo, cualquier método abstracto es esencialmente un método de plantilla. Este patrón (entre otros) era bien conocido por los diseñadores del lenguaje, por lo que lo incorporaron.
@ThomasOwens ofrece excelentes consejos para abordar su problema particular.
fuente
Si te distraen los patrones de diseño, mi consejo es que primero hagas un prototipo del juego, luego los patrones deberían saltar sobre ti. No creo que sea realmente posible o aconsejable intentar diseñar un sistema perfectamente primero y luego implementarlo (de manera similar, me resulta desconcertante cuando las personas intentan escribir programas completos primero y luego compilar, en lugar de hacerlo poco a poco .) El problema es que es poco probable que pienses en cada escenario con el que tendrá que lidiar tu lógica, y durante la fase de implementación perderás toda esperanza o tratarás de mantenerte en tu diseño defectuoso original e introducir hacks, o incluso peor no entregar nada en absoluto.
fuente
Vayamos a las tachuelas de latón. No hay absolutamente ninguna necesidad de ninguna interfaz de juego, sin patrones de diseño, sin clases abstractas y sin UML.
Si tiene una cantidad razonable de clases de soporte, como UI, simulación y lo que sea, entonces básicamente todo su código no específico de la lógica del juego se reutiliza de todos modos. Además, su usuario no cambia su juego dinámicamente. No cambias a 30Hz entre juegos. Juegas un juego durante aproximadamente media hora. Entonces su polimorfismo "dinámico" no es realmente dinámico en absoluto. Es bastante estático.
Entonces, la manera sensata de llegar aquí es usar una abstracción funcional genérica, como C #
Action
o C ++std::function
, crear una clase Mancala, una Wari y una Kalah, y avanzar desde allí.Hecho.
No llamas a juegos. Los juegos te llaman.
fuente