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

16

Fondo

Aquí está el problema real en el que estoy trabajando: quiero una forma de representar las cartas en el juego de cartas Magic: The Gathering . La mayoría de las cartas en el juego son de apariencia 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í misma. Entonces, para mayor claridad, usaré Card solo para referirme a algo que es una tarjeta normal o la mitad de una tarjeta de dos partes (en otras palabras, algo con un solo nombre).

Entoncestenemosuntipobase,Tarjeta.Elpropósitodeestosobjetosesrealmentesoloparamantenerlaspropiedadesdelatarjeta.Realmentenohacennadaporsímismos.

interfaceCard{Stringname();Stringtext();//etc}

HaydossubclasesdeCard,alasquellamoPartialCard(lamitaddeunatarjetadedospartes)yWholeCard(unatarjetanormal).PartialCardtienedosmétodosadicionales:PartialCardotherPart()ybooleanisFirstPart().

Representantes

Sitengounmazo,deberíaestarcompuestodeWholeCards,noCards,yaqueCardpodríaserPartialCard,yesonotendríasentido.Asíquequierounobjetoquerepresenteuna"tarjeta física", es decir, algo que pueda representar un WholeCard , o dos PartialCard s. Tentativamente estoy llamando a este tipo Representative , y Card tendría el método getRepresentative() . Un Representative no proporcionaría casi ninguna información directa sobre la (s) tarjeta (s) que representa, solo apuntaría a ella / ellos. Ahora, mi idea brillante / loca / tonta (tú decides) es que WholeCard hereda de ambos Card y Representative . Después de todo, ¡son cartas que se representan a sí mismas! WholeCards podría implementar getRepresentative como return this; .

En cuanto a PartialCards , no se representan a sí mismos, pero tienen un Representative externo 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" y Representative s como "tarjetas físicas", bueno, ¡la mayoría de las tarjetas son ambas! Creo que podría argumentar que las tarjetas físicas sí que contienen tarjetas conceptuales , y que no son lo lo mismo , pero yo diría que sí lo son.

Necesidad de conversión de tipos

Dado que PartialCard sy WholeCards son Card s, y generalmente no hay una buena razón para separarlos, normalmente solo trabajo con 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 lanzamientos explícitos. Y al igual que Card , Representative tendría que convertirse en WholeCard o Composite , para acceder al Card s que representan.

Tan solo para el resumen:

  • Tipo de base Representative
  • Tipo de base Card
  • Escriba WholeCard extends Card, Representative (no se necesita acceso, se representa a sí mismo)
  • Escriba PartialCard extends Card (le da acceso a otra parte)
  • Escriba Composite extends Representative (da acceso a ambas partes)

¿Esto es una locura? Creo que en realidad tiene mucho sentido, pero sinceramente no estoy seguro.

    
pregunta codebreaker 24.09.2015 - 23:11

5 respuestas

14

Me parece que deberías tener una clase como

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

El código relacionado con la tarjeta física puede manejar la clase de tarjeta física, y el código relacionado con la tarjeta lógica puede lidiar con eso.

  

Creo que podrías argumentar que las tarjetas físicas sí lo hacen   contienen tarjetas conceptuales, y que no son lo mismo, pero yo   Argumentaría que son.

No importa si cree o no que la tarjeta física y lógica son la misma cosa. 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, al adoptar un modelo más simple en el que cada tarjeta física se trata como un conjunto de tarjetas lógicas de manera coherente, el 100% del tiempo dará como resultado un código más simple.

    
respondido por el Winston Ewert 25.09.2015 - 05:53
8

Para ser franco, creo que la solución propuesta es demasiado restrictiva y demasiado contorsionada y ajena a la realidad física son los modelos, con poca ventaja.

Sugeriría una de dos alternativas:

Opción 1. Considérelo como una sola tarjeta, identificada como Half A // Half B , como las listas de sitios de MTG Wear // Tear . Pero, permita que su entidad Card contenga N de cada atributo: nombre reproducible, 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 se diferencia de la Opción 1, ejemplifica la realidad física. Tiene una entidad Card que representa una tarjeta física . Y, su propósito es entonces mantener N Playable things. Esos Playable 's pueden tener cada uno un nombre distinto, costo de maná, lista de efectos, lista de habilidades, etc. Y tu "físico" Card puede tener su propio identificador (o nombre) que es un compuesto de cada uno El nombre de Playable , muy parecido a lo que parece hacer la base de datos de MTG.

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 mire su código. (Como tu propio yo en 6 meses).

    
respondido por el svidgen 25.09.2015 - 04:08
5
  

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

Esta oración es una señal de que hay algo incorrecto en su diseño: en OOP, cada clase debe tener exactamente un rol, y la falta de comportamiento revela un potencial Data Class , 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. Periodo.

No sé nada acerca de Magic: La reunión , pero creo que quieres usar tus tarjetas de una manera similar, cualquiera que 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 composites , a pesar del hecho de que este DP se presenta normalmente para resolviendo un problema más general:

  1. Crea una interfaz Card , como ya hiciste.
  2. Crea un ConcreteCard , que implementa Card y define una tarjeta de cara simple. No dude en poner el comportamiento de una tarjeta normal en esta clase.
  3. Cree un CompositeCard , que implemente Card y tenga dos adicionales (y a priori private) Card s. Llamémoslos leftCard y rightCard .

La elegancia del enfoque es que un CompositeCard contiene dos tarjetas, que a su vez pueden ser ConcreteCard o CompositeCard. En su juego, leftCard y rightCard probablemente serán sistemáticamente ConcreteCard s, pero el Patrón de diseño le permite diseñar composiciones de nivel superior gratis si lo desea. La manipulación de tu tarjeta no tendrá en cuenta el tipo real de tus tarjetas y, por lo tanto, no necesitas cosas como la conversión a subclase.

CompositeCard debe implementar los métodos especificados en Card , por supuesto, y lo hará teniendo en cuenta el hecho de que dicha tarjeta está hecha de 2 tarjetas (más, si lo desea, algo específico del CompositeCard de la propia tarjeta. Por ejemplo, es posible que desee la 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, puedes usar CompositeCard exactamente como lo haces con cualquier Card , y el comportamiento específico se oculta gracias al polimorfismo.

Si está seguro de que CompositeCard siempre contendrá dos Card s normales, puede mantener la idea y simplemente usar ConcreateCard como tipo para leftCard y rightCard .

    
respondido por el mgoeminne 25.09.2015 - 07:38
3

Tal vez todo sea una Carta cuando está en el mazo o en el cementerio, y cuando la juegas, construyes una Criatura, Tierra, Encantamiento, etc. a partir de uno o más objetos de la Carta, todos los cuales implementan o extienden el Juego. Luego, un compuesto se convierte en un solo jugador jugable cuyo constructor toma dos cartas parciales, y una tarjeta con un kicker se convierte en un jugador 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 una carta jugable es solo una carta que debe ser revertida cuidadosamente (perder bonificaciones y contadores, dividirse) cuando se saca de juego, si es realmente útil usar la misma interfaz para invocar una carta y predecir lo que hace.

Quizás Card y Playable tienen un efecto .

    
respondido por el Davislor 24.09.2015 - 23:46
3

El patrón de visitante es una técnica clásica para recuperar información de tipo oculto. Podemos usarlo (aquí una pequeña variación) para discernir entre los dos tipos incluso cuando están almacenados en variables de mayor abstracción.

Comencemos con esa abstracción superior, una interfaz Card :

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

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

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

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

Ahora podemos tener un SimpleCard que representa una tarjeta completa con un único conjunto de propiedades:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

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

Vemos cómo comienzan a encajar CardProperties y CardVisitor aún por escribir. Hagamos un CompoundCard para 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);
    }
}

El CardVisitor comienza a emerger. 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 realizar mejoras, que se analizarán más adelante).

Ahora hemos desarrollado todas las partes. Ahora solo tenemos que juntarlos:

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 polimorfismo #visit method through en lugar de intentar romperlo.

En lugar de usar una clase anónima, incluso puedes promover el CardVisitor a una clase interna o incluso a una clase completa si el comportamiento es reutilizable o si quieres la capacidad de cambiar el comportamiento en tiempo de ejecución.

Podemos usar las clases como están ahora, pero hay algunas mejoras que podemos hacer en la interfaz CardVisitor . Por ejemplo, puede llegar un momento en que Card s pueda tener tres o 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 múltiples 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 estamos interesados en Card s de cara única). También podemos agregar nuevos métodos sin forzar a cada clase existente a implementar esos métodos o no compilar.

    
respondido por el cbojar 25.09.2015 - 02:34

Lea otras preguntas en las etiquetas