Combinando método de plantilla con estrategia

14

Una asignación 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 me vino a la mente al diseñar esto fue utilizar el patrón de estrategia, tengo una variación en los algoritmos (las reglas reales del juego). El diseño podría verse así:

EntoncespenséparamímismoqueeneljuegodeMancalayWari,laformaenquesedeterminaelganadoresexactamentelamismayelcódigoseduplicaría.Nocreoqueestosea,pordefinición,unaviolaciónde'unaregla,unlugar'oelprincipioDRY,yaqueuncambioenlasreglasparaMancalanosignificaríaautomáticamentequeesareglatambiéndeberíacambiarseenWari.Sinembargo,apartirdeloscomentariosquerecibídemiprofesor,tuvelaimpresióndeencontrarundiseñodiferente.

Entoncessemeocurrióesto:

Cada juego (Mancala, Wari, Kalah, ...) tendrá un atributo del tipo de la interfaz de cada regla, es decir, WinnerDeterminer y si hay una versión de Mancala 2.0 que es la misma que la de Mancala 1.0, excepto por cómo el ganador está determinado a que solo puede usar las versiones de Mancala.

Creo que la implementación de estas reglas como patrón de estrategia es ciertamente válida. Pero el problema real viene cuando quiero diseñarlo más.

Al leer sobre el patrón del 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 propio método de la plantilla)
  • determinar el resultado del movimiento
  • determina si el juego ha terminado debido al movimiento anterior
  • si el juego ha terminado, determina quién ha ganado

Esos tres últimos pasos están todos en mi patrón de estrategia descrito anteriormente. Estoy teniendo 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 objeto TurnTemplate , 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 abstracto de fábrica. Luego tendría un MancalaRuleFactory , WariRuleFactory , etc., crearían las instancias correctas de las reglas y me devolverían un objeto RuleSet .

Digamos que uso la estrategia + patrón abstracto de fábrica y tengo un objeto RuleSet 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 objeto RuleSet a mi TurnTemplate . El 'problema' que luego surge es que nunca necesitaría mis implementaciones concretas del TurnTemplate , estas clases se volverían obsoletas. En mis métodos protegidos en el TurnTemplate podría simplemente llamar a ruleSet.determineWinner() . Como consecuencia, la clase TurnTemplate ya no sería abstracta sino que tendría que concretarse, ¿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?

    
pregunta Mekswoll 27.10.2012 - 22:25

4 respuestas

6

Después de ver tus diseños, tanto la primera como la 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 asignación o el propósito de la clase o más información sobre lo que sugirió su profesor, tomaría cualquier cosa que diga a continuación con un grano de sal.

En su primer diseño, declara que su RuleInterface 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 finalice el juego. Parece que es una interfaz válida para una familia de juegos que experimenta variaciones. 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 diría que la duplicación de códigos es terrible para los defectos. Si copia / pega código defectuoso entre implementaciones y uno tiene un error, ahora tiene varios errores que deben solucionarse en diferentes ubicaciones. Si reescribe las implementaciones en diferentes momentos, podría introducir defectos en diferentes ubicaciones. Ninguno de los dos es deseable.

Su segundo diseño parece bastante complejo, con un árbol de herencia profunda. Al menos, es más profundo de lo que esperaría para resolver este tipo de problema. También estás empezando a dividir los detalles de la implementación en otras clases. En última instancia, estás modelando e implementando un juego. Este podría ser un enfoque interesante si se le exigiera 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 yo trataría de encapsular los juegos tanto como pueda en entidades separadas.

Tu 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. Yo recomendaría considerar el diseño del 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() sino playGame() , que va desde el movimiento inicial hasta el movimiento final, después de lo cual termina. Después de cada movimiento del jugador, ajusta el estado del juego, determina si el juego está en un estado final y, si lo está, determina el ganador.

Recomendaría echar un vistazo más de cerca a su primer y tercer diseño y trabajar con ellos. También podría ayudar a 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? Necesitas darte cuenta de lo que está interactuando. En su caso particular, es la clase Game y cualquier otro elemento asociado, no puede diseñar de forma aislada.

Ya que mencionas que eres un estudiante, me gustaría compartir algunas cosas de una época en la que fui TA para un curso de diseño de software:

  • Los patrones son simplemente una forma de capturar cosas que han funcionado en el pasado, pero abstraerlas en un punto en el que se pueden usar en otros diseños. Cada catálogo de patrones de diseño le da un nombre a un patrón, explica sus intenciones y dónde se puede usar, y las situaciones en las que en última instancia restringirían su diseño.
  • El diseño viene con experiencia. La mejor manera de ser bueno en el diseño no es simplemente centrarse en los aspectos de modelado, sino darse cuenta de lo que implica la implementación de ese modelo. El diseño más elegante es útil si no se puede implementar fácilmente o no encaja en el diseño más grande del sistema o con otros sistemas.
  • Muy pocos diseños son "correctos" o "incorrectos". Mientras el diseño cumpla con los requisitos del sistema, no puede estar equivocado. Una vez que hay un mapeo de cada requisito en alguna representación de cómo el sistema va a cumplir ese requisito, el diseño no puede estar equivocado. En este punto, solo se trata de un aspecto cualitativo sobre conceptos como flexibilidad o reutilización o probabilidad o mantenibilidad.
respondido por el Thomas Owens 28.10.2012 - 14:43
3

Tu confusión está justificada. La cosa 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 la 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 como un método de acción (). 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 construyeron.

@ThomasOwens ofrece excelentes consejos para abordar su problema particular.

    
respondido por el Matthew Flynn 28.10.2012 - 16:20
0

Si te distraen los patrones de diseño, mi consejo es que primero prototipo del juego, entonces los patrones deberían saltar hacia ti. No creo que sea realmente posible o recomendable intentar diseñar un sistema perfectamente primero y luego implementarlo (de manera similar, me parece desconcertante cuando la gente intenta escribir primero programas completos y luego compilarlos, en lugar de hacerlo poco a poco .) El problema es que es poco probable que piense en cada escenario con el que tendrá que lidiar su lógica, y durante la fase de implementación perderá toda esperanza o intentará atenerse a su diseño original defectuoso e introducir hacks, o incluso Peor no entregar nada en absoluto.

    
respondido por el James 28.10.2012 - 02:21
-1

Bajemos a las tachuelas de latón. No hay absolutamente ninguna necesidad de interfaz de juego, no hay patrones de diseño, no hay clases abstractas y no hay 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, tu usuario no cambia su juego dinámicamente. No volteas a 30Hz entre juegos. Usted juega un juego durante aproximadamente media hora. Entonces, tu polimorfismo "dinámico" no es en realidad dinámico en absoluto. Es bastante estático.

Así que la mejor manera de ir aquí es utilizar una abstracción funcional genérica, como Action de C # o std::function de C ++, crear una clase de Mancala, una de Wari y una de Kalah, y continuar desde allí.

std::unordered_map<std::string, std::function<void()>> games = {
    { "Kalah", [] { return Kalah(); } },
    { "Mancala", [] { return Mancala(); } },
    { "Wari", [] { return Wari(); } }
};
void play() {
    std::function<void()> f;
    f = games[GetChoiceFromUI()];
    f();
    if (PlayAgain()) return play();
}
int main() {
    play();
}

Hecho.

No llamas a los juegos. Los juegos te llaman.

    
respondido por el DeadMG 28.10.2012 - 00:10

Lea otras preguntas en las etiquetas