Estoy intentando modelar un juego de cartas donde las cartas tienen dos conjuntos importantes de características:
El primero es un efecto. Estos son los cambios en el estado del juego que ocurren cuando juegas la carta. La interfaz para el efecto es la siguiente:
boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);
Y podría considerar que la carta se puede jugar si y solo si puede cumplir con su costo y todos sus efectos son jugables. Al igual que:
// in Card class
boolean isPlayable(Player p, GameState gs) {
if(p.resource < this.cost) return false;
for(Effect e : this.effects) {
if(!e.isPlayable(p,gs)) return false;
}
return true;
}
Bien, hasta ahora, bastante simple.
El otro conjunto de características en la tarjeta son habilidades. Estas habilidades son cambios en el estado del juego que puedes activar a voluntad. Cuando se me ocurrió la interfaz para estos, me di cuenta de que necesitaban un método para determinar si se pueden activar o no, y un método para implementar la activación. Termina siendo
boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);
Y me doy cuenta de que, con la excepción de llamarlo "activar" en lugar de "jugar", Ability
y Effect
tener exactamente la misma firma.
¿Es malo tener múltiples interfaces con una firma idéntica? ¿Debo simplemente usar uno y tener dos conjuntos de la misma interfaz? Como tal:
Set<Effect> effects;
Set<Effect> abilities;
Si es así, ¿qué pasos de refactorización debo tomar en el camino si se vuelven no idénticos (a medida que se lanzan más características), particularmente si son divergentes (es decir, ambos obtienen algo que el otro no debería, en lugar de que solo uno gane y el otro es un subconjunto completo)? Me preocupa especialmente que combinarlos no sea sostenible tan pronto como algo cambie.
La letra pequeña:
Reconozco que esta cuestión se genera por el desarrollo de juegos, pero creo que es el tipo de problema que podría surgir fácilmente en el desarrollo que no es de juegos, particularmente cuando se trata de acomodar los modelos de negocio de múltiples clientes en una aplicación como sucede con casi cada proyecto que he realizado con más de una influencia comercial ... Además, los fragmentos utilizados son fragmentos de Java, pero esto podría aplicarse fácilmente a una multitud de lenguajes orientados a objetos.
fuente
Respuestas:
El hecho de que dos interfaces tengan el mismo contrato no significa que sean la misma interfaz.
El principio de sustitución de Liskov establece:
O, en otras palabras: todo lo que sea cierto para una interfaz o supertipo debe ser cierto para todos sus subtipos.
Si entiendo tu descripción correctamente, una habilidad no es un efecto y un efecto no es una habilidad. Si alguno cambia su contrato, es poco probable que el otro cambie con él. No veo ninguna buena razón para tratar de vincularlos entre sí.
fuente
De Wikipedia : "la interfaz se usa a menudo para definir un tipo abstracto que no contiene datos, pero expone comportamientos definidos como métodos ". En mi opinión, se utiliza una interfaz para describir un comportamiento, por lo que si tiene comportamientos diferentes, tiene sentido tener interfaces diferentes. Al leer su pregunta, la impresión que obtuve es que está hablando de diferentes comportamientos, por lo que diferentes interfaces pueden ser el mejor enfoque.
Otro punto que usted mismo dijo es que si uno de esos comportamientos cambia. Entonces, ¿qué sucede cuando solo tienes una interfaz?
fuente
Si las reglas de su juego de cartas hacen una distinción entre "efectos" y "habilidades", debe asegurarse de que sean interfaces diferentes. Esto evitará que uses accidentalmente uno de ellos donde se requiera el otro.
Dicho esto, si son extremadamente similares, puede tener sentido derivarlos de un antepasado común. Considere cuidadosamente: ¿tiene alguna razón para creer que los "efectos" y las "habilidades" siempre tendrán necesariamente la misma interfaz? Si agrega un elemento a la
effect
interfaz, ¿esperaría necesitar el mismo elemento agregado a laability
interfaz?Si es así, puede colocar dichos elementos en una
feature
interfaz común de la que ambos se derivan. Si no es así, entonces no deberías intentar unificarlos; perderás tu tiempo moviendo cosas entre las interfaces base y derivadas. Sin embargo, dado que no tiene la intención de utilizar la interfaz base común para otra cosa que no sea "no repetir", puede que no haya tanta diferencia. Y, si te apegas a esa intención, supongo que si tomas la decisión equivocada al principio, refactorizar para solucionarlo más tarde puede ser relativamente simple.fuente
Lo que está viendo es básicamente un artefacto de la expresividad limitada de los sistemas de tipos.
Teóricamente, si su sistema de tipos le permite especificar con precisión el comportamiento de esas dos interfaces, entonces sus firmas serían diferentes porque sus comportamientos son diferentes. Prácticamente, la expresividad de los sistemas de tipos está limitada por limitaciones fundamentales como el Problema de detención y el Teorema de Rice, por lo que no se pueden expresar todas las facetas del comportamiento.
Ahora, los diferentes tipos de sistemas tienen diferentes grados de expresividad, pero siempre habrá algo que no se puede expresar.
Por ejemplo: dos interfaces que tienen un comportamiento de error diferente pueden tener la misma firma en C #, pero no en Java (porque en Java las excepciones son parte de la firma). Dos interfaces cuyo comportamiento solo difiere en sus efectos secundarios pueden tener la misma firma en Java, pero tendrán firmas diferentes en Haskell.
Por lo tanto, siempre es posible que termines con la misma firma para diferentes comportamientos. Si cree que es importante poder distinguir entre esos dos comportamientos en más de un nivel nominal, entonces necesita cambiar a un sistema de tipo expresivo más (o diferente).
fuente