Antecedentes
Aquí está el problema real en el que estoy trabajando: quiero una forma de representar cartas en el juego de cartas Magic: The Gathering . La mayoría de las cartas en el juego son cartas de aspecto normal, pero algunas de ellas están divididas en dos partes, cada una con su propio nombre. Cada mitad de estas tarjetas de dos partes se trata como una tarjeta en sí. Entonces, para mayor claridad, usaré Card
solo para referirme a algo que sea una tarjeta regular o la mitad de una tarjeta de dos partes (en otras palabras, algo con un solo nombre).
Entonces tenemos un tipo base, Card. El propósito de estos objetos es realmente mantener las propiedades de la tarjeta. Realmente no hacen nada por sí mismos.
interface Card {
String name();
String text();
// etc
}
Hay dos subclases de Card
, que estoy llamando PartialCard
(la mitad de una tarjeta de dos partes) y WholeCard
(una tarjeta normal). PartialCard
tiene dos métodos adicionales: PartialCard otherPart()
y boolean isFirstPart()
.
Representantes
Si tengo un mazo, debería estar compuesto de WholeCard
s, no de Card
s, como a Card
podría ser un PartialCard
, y eso no tendría sentido. Entonces, quiero un objeto que represente una "tarjeta física", es decir, algo que pueda representar uno WholeCard
o dos PartialCard
s. Estoy tentativamente llamando a este tipo Representative
, y Card
tendría el método getRepresentative()
. A Representative
casi no proporcionaría información directa sobre las tarjetas que representa, solo las señalaría. Ahora, mi idea brillante / loca / tonta (usted decide) es que WholeCard hereda de ambos Card
y Representative
. Después de todo, ¡son cartas que se representan a sí mismas! WholeCards podría implementarse getRepresentative
como return this;
.
En cuanto a PartialCards
, no se representan a sí mismos, pero tienen un externo Representative
que no es un Card
, pero proporciona métodos para acceder a los dos PartialCard
s.
Creo que esta jerarquía de tipos tiene sentido, pero es complicada. Si pensamos en Card
s como "tarjetas conceptuales" Representative
ys como "tarjetas físicas", bueno, ¡la mayoría de las tarjetas son ambas! Creo que podría argumentar que las tarjetas físicas de hecho contienen tarjetas conceptuales y que no son lo mismo , pero diría que lo son.
Necesidad de fundición de tipo
Debido a que PartialCard
sy WholeCards
son ambos Card
, y generalmente no hay una buena razón para separarlos, normalmente solo estaría trabajando con ellos Collection<Card>
. Así que a veces necesito lanzar PartialCard
s para acceder a sus métodos adicionales. En este momento, estoy usando el sistema descrito aquí porque realmente no me gustan los moldes explícitos. Y Card
, como , Representative
necesitaría ser lanzado a a WholeCard
o Composite
, para acceder a los Card
s reales que representan.
Así que solo para resumen:
- Tipo base
Representative
- Tipo base
Card
- Tipo
WholeCard extends Card, Representative
(no se necesita acceso, se representa a sí mismo) - Tipo
PartialCard extends Card
(da acceso a otra parte) - Tipo
Composite extends Representative
(da acceso a ambas partes)
¿Esto es una locura? Creo que en realidad tiene mucho sentido, pero honestamente no estoy seguro.
fuente
Respuestas:
Me parece que deberías tener una clase como
El código que se ocupa de la tarjeta física puede manejar la clase de tarjeta física, y el código que se ocupa de la tarjeta lógica puede manejar eso.
No importa si crees que la tarjeta física y la lógica son lo mismo. No asuma que solo porque son el mismo objeto físico, deberían ser el mismo objeto en su código. Lo que importa es si adoptar ese modelo hace que el codificador sea más fácil de leer y escribir. El hecho es que la adopción de un modelo más simple donde cada tarjeta física se trata como una colección de tarjeta lógica de manera consistente, el 100% del tiempo, dará como resultado un código más simple.
fuente
Para ser franco, creo que la solución propuesta es demasiado restrictiva y demasiado retorcida y desarticulada de la realidad física son los modelos, con poca ventaja.
Sugeriría una de dos alternativas:
Opción 1. Trátelo como una sola tarjeta, identificada como Half A // Half B , como el sitio MTG enumera Wear // Tear . Pero, permita que su
Card
entidad contenga N de cada atributo: nombre jugable, costo de maná, tipo, rareza, texto, efectos, etc.Opción 2. No todo lo que es diferente de la Opción 1, modelarlo según la realidad física. Tienes una
Card
entidad que representa una tarjeta física . Y, su propósito es entonces mantener NPlayable
cosas. EsosPlayable
pueden tener un nombre distinto, costo de maná, lista de efectos, lista de habilidades, etc. Y su "físico"Card
puede tener su propio identificador (o nombre) que es un compuesto delPlayable
nombre de cada uno , al igual que la base de datos MTG parece hacer.Creo que cualquiera de estas opciones está bastante cerca de la realidad física. Y, creo que será beneficioso para cualquiera que vea su código. (Como tú mismo en 6 meses).
fuente
Esta oración es una señal de que hay algo mal en su diseño: en OOP, cada clase debe tener exactamente un rol, y la falta de comportamiento revela una posible clase de datos , que es un mal olor en el código.
En mi humilde opinión, suena un poco extraño, e incluso un poco extraño. Un objeto de tipo "Tarjeta" debe representar una tarjeta. Período.
No sé nada sobre Magic: la reunión , pero supongo que quieres usar tus cartas de una manera similar, sea cual sea su estructura real: quieres mostrar una representación de cadena, quieres calcular un valor de ataque, etc.
Para el problema que describe, recomendaría un patrón de diseño de composición , a pesar de que este DP generalmente se presenta para resolver un problema más general:
Card
interfaz, como ya lo hizo.ConcreteCard
, que implementeCard
y defina una tarjeta de cara simple. No dudes en poner el comportamiento de una carta normal en esta clase.CompositeCard
, que implementeCard
y tenga dos s adicionales (y a priori privados)Card
. Vamos a llamarlosleftCard
yrightCard
.La elegancia del enfoque es que a
CompositeCard
contiene dos tarjetas, que pueden ser ConcreteCard o CompositeCard. En su juego,leftCard
yrightCard
probablemente será sistemáticamenteConcreteCard
s, pero el Patrón de diseño le permite diseñar composiciones de mayor nivel de forma gratuita si lo desea. La manipulación de su tarjeta no tendrá en cuenta el tipo real de sus tarjetas y, por lo tanto, no necesita cosas como lanzar a la subclase.CompositeCard
debe implementar los métodos especificados enCard
, por supuesto, y lo hará teniendo en cuenta el hecho de que dicha tarjeta está compuesta por 2 tarjetas (más, si lo desea, algo específico de laCompositeCard
tarjeta en sí. Por ejemplo, puede que desee siguiente implementación:Al hacer eso, puede usar
CompositeCard
exactamente como lo hace para cualquieraCard
, y el comportamiento específico se oculta gracias al polimorfismo.Si está seguro de que un
CompositeCard
siempre contendrá dosCard
s normales , puede mantener la idea y simplemente usarlaConcreateCard
como un tipo paraleftCard
yrightCard
.fuente
Card
enCompositeCard
que esté implementando el patrón decorador . También recomiendo al OP que use esta solución, ¡el decorador es el camino a seguir!CompositeCard
que no exponga métodos adicionales,CompositeCard
es solo un decorador.Tal vez todo es una Carta cuando está en la baraja o en el cementerio, y cuando la juegas, construyes una Criatura, Tierra, Encantamiento, etc. a partir de uno o más objetos de Carta, todos los cuales implementan o extienden Jugable. Luego, un compuesto se convierte en un Jugable único cuyo constructor toma dos Cartas parciales, y una carta con un pateador se convierte en un Jugable cuyo constructor toma un argumento de maná. El tipo refleja lo que puede hacer con él (dibujar, bloquear, disipar, tocar) y lo que puede afectarlo. O un Jugable es solo una Carta que tiene que ser revertida cuidadosamente (perdiendo cualquier bonificación y contadores, dividiéndose) cuando se retira del juego, si es realmente útil usar la misma interfaz para invocar una carta y predecir lo que hace.
Quizás Card y Jugable tengan un efecto.
fuente
El patrón de visitante es una técnica clásica para recuperar información de tipo oculto. Podemos usarlo (una ligera variación aquí) aquí para discernir entre los dos tipos incluso cuando están almacenados en variables de abstracción superior.
Comencemos con esa abstracción superior, una
Card
interfaz:Puede haber un poco más de comportamiento en la
Card
interfaz, pero la mayoría de los captadores de propiedades se mueven a una nueva claseCardProperties
:Ahora podemos tener una
SimpleCard
representación de una tarjeta completa con un solo conjunto de propiedades:Vemos cómo el
CardProperties
y lo que está por escribirCardVisitor
están empezando a encajar. Hagamos unaCompoundCard
para representar una tarjeta con dos caras:Los
CardVisitor
comienzos a surgir. Intentemos escribir esa interfaz ahora:(Esta es una primera versión de la interfaz por ahora. Podemos hacer mejoras, que se discutirán más adelante).
Ahora hemos desarrollado todas las partes. Ahora solo necesitamos unirlos:
El tiempo de ejecución manejará el envío a la versión correcta del
#visit
método a través del polimorfismo en lugar de tratar de romperlo.En lugar de utilizar una clase anónima, incluso puede promocionarla
CardVisitor
a una clase interna o incluso a una clase completa si el comportamiento es reutilizable o si desea la capacidad de intercambiar el comportamiento en tiempo de ejecución.Podemos usar las clases como están ahora, pero hay algunas mejoras que podemos hacer a la
CardVisitor
interfaz. Por ejemplo, puede llegar un momento en queCard
s pueda tener tres, cuatro o cinco caras. En lugar de agregar nuevos métodos para implementar, podríamos tener el segundo método take y array en lugar de dos parámetros. Esto tiene sentido si las tarjetas de varias caras se tratan de manera diferente, pero el número de caras por encima de una se trata de manera similar.También podríamos convertir
CardVisitor
a una clase abstracta en lugar de una interfaz, y tener implementaciones vacías para todos los métodos. Esto nos permite implementar solo los comportamientos que nos interesan (tal vez solo nos interesan los s de una sola caraCard
). También podemos agregar nuevos métodos sin obligar a cada clase existente a implementar esos métodos o no compilar.fuente