Cómo hacer un tipo de datos para algo que se representa a sí mismo o a otras dos cosas

16

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é Cardsolo 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).

tarjetas

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). PartialCardtiene dos métodos adicionales: PartialCard otherPart()y boolean isFirstPart().

Representantes

Si tengo un mazo, debería estar compuesto de WholeCards, no de Cards, como a Cardpodrí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 WholeCardo dos PartialCards. Estoy tentativamente llamando a este tipo Representative, y Cardtendría el método getRepresentative(). A Representativecasi 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 getRepresentativecomo return this;.

En cuanto a PartialCards, no se representan a sí mismos, pero tienen un externo Representativeque no es un Card, pero proporciona métodos para acceder a los dos PartialCards.

Creo que esta jerarquía de tipos tiene sentido, pero es complicada. Si pensamos en Cards como "tarjetas conceptuales" Representativeys 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 PartialCardsy WholeCardsson 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 PartialCards 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 , Representativenecesitaría ser lanzado a a WholeCardo Composite, para acceder a los Cards 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.

descifrador de códigos
fuente
1
¿Las tarjetas PartialCards son efectivamente distintas de las que hay dos por tarjeta física? ¿Importa el orden en que se juegan? ¿No podría simplemente hacer una clase "Deck Slot" y "WholeCard" y permitir que DeckSlots tenga múltiples WholeCards, y luego simplemente hacer algo como DeckSlot.WholeCards.Play y si hay 1 o 2 juegue a ambos como si estuvieran separados tarjetas? Parece que dado su diseño mucho más complicado, debe haber algo que me falta :)
enderland
Realmente no siento que pueda usar el polimorfismo entre cartas enteras y cartas parciales para resolver este problema. Sí, si quisiera una función de "juego", podría implementarla fácilmente, pero el problema es que las cartas parciales y las cartas enteras tienen diferentes conjuntos de atributos. Realmente necesito poder ver estos objetos como lo que son, no solo como su clase base. A menos que haya una mejor solución para ese problema. Pero he descubierto que el polimorfismo no se mezcla bien con los tipos que comparten una clase base común, pero tienen diferentes métodos entre ellos, si eso tiene sentido.
Codebreaker
1
Honestamente, creo que esto es ridículamente complicado. Parece que solo modela relaciones "es un", lo que lleva a un diseño muy rígido con un acoplamiento apretado. ¿Más interesante sería lo que esas cartas están haciendo realmente?
Daniel Jour
2
Si son "solo objetos de datos", entonces mi instinto sería tener una clase que contenga una matriz de objetos de una segunda clase, y dejarla así; sin herencia u otras complicaciones sin sentido. No sé si esas dos clases deberían ser PlayableCard y DrawableCard o WholeCard y CardPart, porque no sé lo suficiente sobre cómo funciona tu juego, pero estoy seguro de que puedes pensar en algo para llamarlos.
Ixrec
1
¿Qué tipo de propiedades tienen? ¿Puedes dar ejemplos de acciones que usen estas propiedades?
Daniel Jour

Respuestas:

14

Me parece que deberías tener una clase como

class PhysicalCard {
    List<LogicalCard> getLogicalCards();
}

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.

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.

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.

Winston Ewert
fuente
2
Votado porque esta es la mejor respuesta, aunque no por la razón dada. Esta es la mejor respuesta no porque sea simple, sino porque las tarjetas físicas y las tarjetas conceptuales no son lo mismo, y esta solución modela correctamente su relación. Es cierto que un buen modelo de OOP no siempre refleja la realidad física, pero siempre debe reflejar la realidad conceptual. La simplicidad es buena, por supuesto, pero lleva a un segundo plano a la corrección conceptual.
Kevin Krumwiede
2
@KevinKrumwiede, como lo veo, no hay una sola realidad conceptual. Diferentes personas piensan en la misma cosa de diferentes maneras, y las personas pueden cambiar su forma de pensar. Puedes pensar en las tarjetas físicas y lógicas como entidades separadas. O puede pensar en las tarjetas divididas como una especie de excepción a la noción general de una tarjeta. Ninguno de los dos es intrínsecamente incorrecto, pero el uno se presta a un modelado más simple.
Winston Ewert
8

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 Cardentidad contenga N de cada atributo: nombre jugable, costo de maná, tipo, rareza, texto, efectos, etc.

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* etc. */
}

Opción 2. No todo lo que es diferente de la Opción 1, modelarlo según la realidad física. Tienes una Cardentidad que representa una tarjeta física . Y, su propósito es entonces mantener N Playable cosas. Esos Playablepueden tener un nombre distinto, costo de maná, lista de efectos, lista de habilidades, etc. Y su "físico" Cardpuede tener su propio identificador (o nombre) que es un compuesto del Playablenombre de cada uno , al igual que la base de datos MTG parece hacer.

interface Card {
  String Name();
  List<Playable> Playables();
}

interface Playable {
  String Name();
  ManaCost Cost();
  CardType Type();
  /* etc. */
}

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).

svidgen
fuente
5

El propósito de estos objetos es realmente mantener las propiedades de la tarjeta. Realmente no hacen nada por sí mismos.

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.

Después de todo, ¡son cartas que se representan a sí mismas!

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:

  1. Cree una Cardinterfaz, como ya lo hizo.
  2. Crea un ConcreteCard, que implementeCard y defina una tarjeta de cara simple. No dudes en poner el comportamiento de una carta normal en esta clase.
  3. Cree a CompositeCard, que implemente Cardy tenga dos s adicionales (y a priori privados) Card. Vamos a llamarlosleftCard y rightCard.

La elegancia del enfoque es que a CompositeCardcontiene dos tarjetas, que pueden ser ConcreteCard o CompositeCard. En su juego, leftCardy rightCardprobablemente 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.

CompositeCarddebe implementar los métodos especificados en Card, 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 la CompositeCardtarjeta en sí. Por ejemplo, puede que desee siguiente implementación:

public class CompositeCard implements Card
{ 
   private final Card leftCard, rightCard;
   private final double factor;

   @Override // Defined in Card
   public double attack(Player p){
      return factor * (leftCard.attack(p) + rightCard.attack(p));
   }

   @Override // idem
   public String name()
   {
       return leftCard.name() + " combined with " + rightCard.name();
   }

   ...
}

Al hacer eso, puede usar CompositeCardexactamente como lo hace para cualquiera Card, y el comportamiento específico se oculta gracias al polimorfismo.

Si está seguro de que un CompositeCardsiempre contendrá dos Cards normales , puede mantener la idea y simplemente usarla ConcreateCardcomo un tipo para leftCardy rightCard.

mgoeminne
fuente
Usted está hablando de la modelo compuesto , pero, de hecho, ya que está sosteniendo dos referencias de Carden CompositeCardque esté implementando el patrón decorador . También recomiendo al OP que use esta solución, ¡el decorador es el camino a seguir!
Visto el
No veo por qué tener dos instancias de tarjeta hace que mi clase sea decoradora. Según su propio enlace, un decorador agrega características a un solo objeto, y es en sí mismo una instancia de la misma clase / interfaz que este objeto. Mientras que, de acuerdo con su otro enlace, un compuesto permite la posesión de múltiples objetos de la misma clase / interfaz. Pero en última instancia, las palabras no importan, y solo la idea es genial.
mgoeminne
@Spotted Esto definitivamente no es el patrón decorador ya que el término se usa en Java. El patrón decorador se implementa anulando métodos en un objeto individual, creando una clase anónima que es única para ese objeto.
Kevin Krumwiede
@KevinKrumwiede siempre CompositeCardque no exponga métodos adicionales, CompositeCardes solo un decorador.
Visto el
"... Design Pattern te permite diseñar composiciones de nivel superior de forma gratuita", no, no es gratis , sino todo lo contrario, tiene el precio de tener una solución que es más complicada de lo necesario para los requisitos.
Doc Brown
3

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.

Davislor
fuente
Lamentablemente, no juego estas cartas. En realidad, son solo objetos de datos que se pueden consultar. Si desea una estructura de datos de cubierta, desea una Lista <WholeCard> o algo así. Si desea realizar una búsqueda de tarjetas instantáneas verdes, querrá una Lista <Card>.
Codebreaker
Ah bien. No es muy relevante para su problema, entonces. ¿Debo eliminar?
Davislor
3

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 Cardinterfaz:

public interface Card {
    public void accept(CardVisitor visitor);
}

Puede haber un poco más de comportamiento en la Cardinterfaz, pero la mayoría de los captadores de propiedades se mueven a una nueva clase CardProperties:

public class CardProperties {
    // property methods, constructors, etc.

    String name();
    String text();
    // ...
}

Ahora podemos tener una SimpleCardrepresentación de una tarjeta completa con un solo conjunto de propiedades:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

    @Override
    public void accept(CardVisitor visitor) {
        visitor.visit(properties);
    }
}

Vemos cómo el CardPropertiesy lo que está por escribir CardVisitorestán empezando a encajar. Hagamos una CompoundCardpara representar una tarjeta con dos caras:

public class CompoundCard implements Card {
    private CardProperties firstFaceProperties;
    private CardProperties secondFaceProperties;

    // Constructors, ...

    public void accept(CardVisitor visitor) {
        visitor.visit(firstFaceProperties, secondFaceProperties);
    }
}

Los CardVisitorcomienzos a surgir. Intentemos escribir esa interfaz ahora:

public interface CardVisitor {
    public void visit(CardProperties properties);
    public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties);
}

(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:

List<Card> cards = new LinkedList<>();
cards.add(new SimpleCard(new CardProperties(/* ... */)));
cards.add(new CompoundCard(new CardProperties(/* ... */), new CardProperties(/* ... */)));

 for(Card card : cards) {
     card.accept(new CardVisitor() {
         @Override
         public void visit(CardProperties properties) {
             // Do something for simple cards with a single face
         }

         public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties) {
             // Do something else for compound cards with two faces
         }
     });
 }

El tiempo de ejecución manejará el envío a la versión correcta del #visitmétodo a través del polimorfismo en lugar de tratar de romperlo.

En lugar de utilizar una clase anónima, incluso puede promocionarla CardVisitora 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 CardVisitorinterfaz. Por ejemplo, puede llegar un momento en que Cards 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 CardVisitora 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 cara Card). También podemos agregar nuevos métodos sin obligar a cada clase existente a implementar esos métodos o no compilar.

cbojar
fuente
1
Creo que esto podría expandirse fácilmente al otro tipo de tarjeta de dos caras (anverso y reverso en lugar de lado a lado). ++
RubberDuck
Para una mayor exploración, el uso de un visitante para explotar métodos específicos de una subclase a veces se denomina Despacho múltiple. Double Dispatch podría ser una solución interesante al problema.
mgoeminne
1
Voy a rechazar porque creo que el patrón de visitante agrega complejidad innecesaria y acoplamiento al código. Como hay una alternativa disponible (ver la respuesta de mgoeminne), no la usaría.
Visto el
@Spotted El visitante es un patrón complejo, y también consideré el patrón compuesto al escribir la respuesta. La razón por la que fui con el visitante es porque el OP quiere tratar las cosas similares de manera diferente, en lugar de cosas diferentes de manera similar. Si las tarjetas tenían más comportamiento y se usaban para el juego, entonces el patrón compuesto permite combinar datos para producir una estadística unificada. Sin embargo, son solo bolsas de datos, posiblemente utilizadas para representar una pantalla de tarjeta, en cuyo caso obtener información separada parecía más útil que un simple agregador compuesto.
cbojar