2008-12-21 9 views
6

Tengo los tipos Rock, Paper y Scissors. Estos son componentes, o "manos" del juego Rock, Paper, Scissors. Dadas las manos de dos jugadores, el juego debe decidir quién gana. ¿Cómo puedo solucionar el problema de almacenar esta tabla cadenaEliminando el acoplamiento de clases que tienen fuertes vínculos conceptuales entre sí

Rock, Paper, Scissors chart

sin acoplar las diversas manos el uno al otro? El objetivo es permitir agregar una nueva mano al juego (Jon Skeet, por ejemplo) sin cambiar ninguno de los otros.

Estoy abierto a cualquier idea de proxies, pero no a grandes declaraciones de cambio o duplicación de código. Por ejemplo, introducir un nuevo tipo que gestione las comparaciones de la cadena está bien, siempre que no necesite cambiarlo por cada nueva mano que agregue. Por otra parte, si puede racionalizar tener un proxy que debe cambiar para cada mano nueva o cuando una mano cambia, también es bienvenido.

Esto es una especie de problema de Diseño 101, pero tengo curiosidad por saber qué soluciones pueden surgir para esto. Obviamente, este problema puede escalarse fácilmente a sistemas mucho más grandes con muchos más componentes con cualquier relación arbitrariamente compleja entre ellos. Es por eso que estoy poniendo un ejemplo muy simple y concreto para resolver. Cualquier paradigma utilizado, OOP u otro, es bienvenido.

+0

A veces, las preguntas pueden ser * demasiado * genéricas. –

+0

Sí. Es por eso que puse un ejemplo muy concreto. Resolvamos este ejemplo, y me imagino que podremos generalizar la solución. – wilhelmtell

+0

El problema es que hay muchas "soluciones" para cualquier ejemplo dado, y no siempre se puede generalizar. Tal vez si tuviera varios ejemplos diferentes, sería razonable considerar una solución generalizada (realmente solo un patrón de diseño) – Tom

Respuesta

4

Tiene una clase GameStrategy que implementa un método Win. El método de ganar toma una lista de Manos, y devuelve una Mano - si hay un ganador - o nula si el juego fue un empate. Creo que la estrategia ganadora no es realmente una propiedad de la mano, sino del juego. Incorpora la determinación de un ganador de un par de manos en la clase GameStrategy.

EDITAR: estrategia potencial

public enum RPSEnum { Rock, Paper, Scissors } 

private RPSEnum FirtRPS = RPSEnum.Rock; 
private RPSEnum LastRPS = RPSEnum.Scissors; 

public Hand Win(Hand firstPlayer, Hand secondPlayer) 
{ 
    if (firstPlayer.Value == FirstRPS 
     && secondPlayer.Value == LastRPS) 
    { 
     return firstPlayer; 
    } 
    else if (secondPlayer.Value == FirstRPS 
      && firstPlayer.Value == LastRPS) 
     return secondPlayer; 
    } 
    else 
    { 
     int compare = (int)firstPlayer.Value - (int)secondPlayer.Value; 
     if (compare > 0) 
     { 
      return firstPlayer; 
     } 
     else if (compare < 0) 
     { 
      return secondPlayer; 
     } 
     else 
     { 
      return null; 
     }  
    } 
} 

Para añadir un nuevo valor de la mano, sólo tiene que añadir el valor a RPSEnum en la secuencia apropiada. Si es la nueva mano "más baja", actualice FirstRPS. Si es la nueva mano "más alta", actualice LastRPS. No debería necesitar cambiar el algoritmo real en absoluto.

NOTA: esto es más complejo de lo que necesita para solo tres valores, pero el requisito es que se puedan agregar valores adicionales sin actualizar mucho código.

+0

Entonces, ¿cómo implementarías la estrategia? ¿Tendría una jerarquía de estrategia, con una realización para cada posible coincidencia de manos? Esta jerarquía crecerá demasiado rápido a medida que se agreguen las manos. – wilhelmtell

+0

¿Tendrá algo así como una matriz o un interruptor en la estrategia? Esa es una solución razonable. Originalmente me preguntaba si de alguna manera podría terminar diciendo algo como "result = hand1.fight (hand2)", para hacer que el código del cliente se vea bien. – wilhelmtell

+0

Supongo que las manos están ordenadas y tienen el valor de la enumeración que proporciona el pedido. Simplemente compararía los valores de la enumeración. El único caso especial que necesitaría comprobar es que la primera mano supera la última mano; todas las demás se podrían hacer mediante conversión a int y utilizando la resta. – tvanfosson

2

Si tienen una similitud conceptual suficiente, es posible que no desee desanclarse reduciendo el acoplamiento.

"Acoplamiento" es realmente solo una medida de la cantidad de código de rotura que habrá si se cambia la implementación interna de una cosa. Si la implementación interna de estas cosas es intrínsecamente sensible a las demás, entonces lo es; reducir el acoplamiento está bien, pero el software, antes que nada, debe reflejar la realidad.

1

No creo que las diferentes manos sean de tipos diferentes: son instancias separadas del mismo tipo. Ese tipo tiene atributos como nombre, posiblemente imagen, etc.

Inicializas el juego cargando, a partir de datos, la lista de nombres de manos y una matriz que indica qué mano pega cada mano. Quizás los datos se cargarían en una clase de Juego con un método de Comparación.

0

En este caso, lo importante es que si se comparan dos objetos , vuelvas resultado .

Básicamente, debe desacoplar esta comparación de tal forma que no solo pueda agregar un nuevo tipo de objeto a la mezcla, sino también agregar reglas de comparación en la mezcla que pueda manejar este objeto.

Ahora, también comentó sobre su pregunta diciendo que "A veces, las preguntas pueden ser demasiado genérico", y el problema aquí es que no importa cuánto te digo cómo hacer lo que usted pregunta por, se no te ayudará ni un poco sobre el problema real.

A menos que esté construyendo un juego de piedra-papel-tijera-X.

Personalmente, haría lo siguiente, con mi propio contenedor IoC.

ServiceContainer.Global.RegisterFactory<IHandType>() 
    .FromDelegate(() => RandomRockScissorPaper()); 
ServiceContainer.Global.RegisterFactory<IHandComparison, DefaultHandComparison>(); 

(o más bien, me gustaría configurar hasta lo anterior en el archivo app.config o similar, de modo que pudiera ser cambiado después de que el proyecto se ha construido).

Luego, si un usuario/cliente/punto final necesita sobrescribir, para agregar otro tipo, aumentaría los registros de la siguiente manera (recuerde que los anteriores están en app.config, entonces los reemplazaría con los que están a continuación):

ServiceContainer.Global.RegisterFactory<IHandType>() 
    .FromDelegate(() => RandomRockScissorPaper()) 
    .ForPolicy("original"); 
ServiceContainer.Global.RegisterFactory<IHandType>() 
    .FromDelegate((IHandType original) => RandomRockScissorPaperBlubb(original)) 
    .WithParameters(
     new Parameter<IHandType>("original").WithPolicy("original")) 
    .ForPolicy("new") 
    .AsDefaultPolicy(); 

Aquí añado una nueva manera de resolver IHandType, no sólo por mantener la forma original, pero añadiendo uno nuevo también. A este nuevo se le dará el resultado de llamar al anterior, y luego tendría que decidir internamente si una posibilidad aleatoria debería devolver el cuarto tipo, o el tipo original (uno de los tres originales).

Entonces yo también anular la regla original comparación:

ServiceContainer.Global.RegisterFactory<IHandComparison, DefaultHandComparison>() 
    .ForPolicy("original"); 
ServiceContainer.Global.RegisterFactory<IHandComparison, NewHandComparison>() 
    .ForPolicy("new") 
    .AsDefaultPolicy() 
    .WithParameters(
     new Parameter<IHandType>("original")); 

Así es como se utilizaría:

IHandType hand1 = ServiceContainer.Global.Resolve<IHandType>(); 
IHandType hand2 = ServiceContainer.Global.Resolve<IHandType>(); 
IHandComparison comparison = ServiceContainer.Global.Resolve<IHandComparison>(); 
if (comparison.Compare(hand1, hand2) < 0) 
    Console.Out.WriteLine("hand 1 wins"); 
else if (comparison.Compare(hand1, hand2) > 0) 
    Console.Out.WriteLine("hand 1 wins"); 
else 
    Console.Out.WriteLine("equal"); 

Así es como para poner en práctica:

public interface IHandComparison 
{ 
    Int32 Compare(IHandType hand1, IHandType hand2); 
} 
public class DefaultHandComparison : IHandComparison 
{ 
    public Int32 Compare(IHandType hand1, IHandType hand2) 
    { 
     ... normal rules here 
    } 
} 
public class NewHandComparison : IHandComparison 
{ 
    private IHandComparison _Original; 
    public NewHandComparison(IHandComparison original) 
    { 
     _Original = original; 
    } 
    public Int32 Compare(IHandType hand1, IHandType hand2) 
    { 
     if hand1 is blubb or hand2 is blubb then ... 
     else 
      return _Original.Compare(hand1, hand2); 
    } 
} 

Después de escribir todo esto me doy cuenta de que la configuración de app.config no será capaz de manejar delegados, por lo que un objeto de fábrica w debería ser necesario, pero lo mismo sigue siendo válido.

Necesita poder resolver la forma de obtener nuevas manos, así como también resolver las reglas de las manos ganadoras.

0

¿Por qué desacoplar? Estos elementos están intrínsecamente vinculados entre sí, y agregar una nueva mano no cambiará eso.

Solo tiene una clase de mano base que se extiende por Piedra, Papel y Tijera. Asigne a la clase base un atributo .beatenby que tome una de las otras clases del tipo Mano. Si terminas con una situación en la que una mano puede ser vencida por más de otra mano, simplemente haz que el atributo .beatenby tome una matriz en su lugar.

0

Varias otras respuestas han proporcionado soluciones muy flexibles y muy desacopladas ... y son manera más complejas de lo que deben ser. La respuesta corta al desacoplamiento es double dispatch, que son muy pocos los idiomas que admiten de forma nativa. El patrón de visitante de GoF existe para simular el envío doble en idiomas que admiten el envío único (cualquier lenguaje de OO lo admite, incluso C con punteros de función).

Aquí hay un ejemplo un poco más grande en el mismo espíritu. Supongamos que está supervisando el tráfico HTTP, y está tratando de clasificar patrones basados ​​en una combinación del método de solicitud y el código de respuesta. En vez de mirar esta serie:

rock/paper 
paper/scissors 
paper/paper 
... 

... usted está buscando en esta serie:

GET/200 
GET/304 
POST/401 
POST/200 
... 

Si el sistema tenía HttpRequest y HttpResponse objetos, la forma más sencilla de enviar sería utilizar una única función que las rutas a todas las posibles opciones:

HttpRequest req; 
HttpResponse resp; 
switch ((req.method, resp.code)) { 
    case (GET, 200): return handleGET_OK(req, resp); 
    case (GET, 304); return handleGET_NotModified(req, resp); 
    case (POST, 404): return handlePOST_NotFound(req, resp); 
    ... 
    default: print "Unhandled combination"; break; 
} 

Con esto, los objetos subyacentes no se ven afectadas por la adición de nuevos tipos. Sin embargo, su método de clasificación sigue siendo muy simple y, por lo tanto, puede mantenerse para otros desarrolladores. Si lo desea (o si su idioma lo facilita), puede convertir esa declaración switch en una especie de mapa de funciones y registrar las diferentes combinaciones de métodos de solicitud y códigos de respuesta en el inicio.

Cuestiones relacionadas