Come creare un tipo di dati per qualcosa che rappresenta se stesso o altre due cose

16

Sfondo

Ecco il problema reale su cui sto lavorando: voglio un modo per rappresentare le carte nel gioco di carte Magic: The Gathering . La maggior parte delle carte nel gioco sono carte dall'aspetto normale, ma alcune sono divise in due parti, ognuna con il proprio nome. Ogni metà di queste carte in due parti viene considerata come una carta stessa. Quindi, per chiarezza, userò Card solo per fare riferimento a qualcosa che è una carta normale o metà di una carta a due parti (in altre parole, qualcosa con un solo nome).

Quindiabbiamountipobase,Card.Loscopodiquestioggettièinrealtàsoloquellodimantenereleproprietàdellacarta.Nonfannonulladasoli.

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

CisonoduesottoclassidiCard,chestochiamandoPartialCard(metàdiunacartaadueparti)eWholeCard(unacartanormale).PartialCardhaduemetodiaggiuntivi:PartialCardotherPart()ebooleanisFirstPart().

Rappresentanti

Sehounmazzo,dovrebbeesserecompostodaWholeCards,nonCards,inquantoCardpotrebbeessereunPartialCard,equestononavrebbesenso.Quindivogliounoggettocherappresentiuna"carta fisica", cioè qualcosa che possa rappresentare un WholeCard o due PartialCard s. Chiamerò provvisoriamente questo tipo Representative e Card avrà il metodo getRepresentative() . Un Representative non fornirebbe quasi nessuna informazione diretta sulla / e carta / e che rappresenta, verrebbe solo ad essa / loro. Ora, la mia idea geniale / pazza / stupida (decidi tu) è che WholeCard eredita da entrambi Card e Representative . Dopotutto, sono carte che rappresentano se stesse! WholeCards potrebbe implementare getRepresentative come return this; .

Come per PartialCards , non rappresentano se stessi, ma hanno un Representative esterno che non è un Card , ma fornisce metodi per accedere ai due PartialCard s.

Penso che questo tipo di gerarchia abbia senso, ma è complicato. Se pensiamo a Card s come "carte concettuali" e Representative s come "carte fisiche", beh, la maggior parte delle carte sono entrambe! Penso che potresti argomentare che le carte fisiche in effetti contengono carte concettuali, e che non sono la stessa cosa , ma direi che lo sono.

Necessità di type-casting

Poiché PartialCard s e WholeCards sono entrambi Card s, e di solito non ci sono buoni motivi per separarli, normalmente lavorerei con Collection<Card> . Quindi a volte avrei bisogno di lanciare PartialCard s per accedere ai loro metodi aggiuntivi. In questo momento, sto usando il sistema descritto qui perché in realtà non mi piacciono i cast espliciti. E come Card , Representative avrebbe bisogno di essere castato a WholeCard o Composite , per accedere al% effettivo di% di% che rappresentano.

Quindi, solo per il sommario:

  • Tipo di base Card
  • Tipo di base Representative
  • Digita Card (nessun accesso necessario, rappresenta se stesso)
  • Digita WholeCard extends Card, Representative (dà accesso ad altra parte)
  • Digita PartialCard extends Card (consente di accedere ad entrambe le parti)

Questo è folle? Penso che abbia davvero molto senso, ma onestamente non ne sono sicuro.

    
posta codebreaker 24.09.2015 - 23:11
fonte

5 risposte

14

Mi sembra che dovresti avere una classe come

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

Il codice che riguarda la scheda fisica può occuparsi della classe della carta fisica e il codice interessato alla scheda logica può occuparsene.

I think you could make an argument that physical cards do in fact contain conceptual cards, and that they're not the same thing, but I would argue that they are.

Non importa se pensi che la carta fisica e quella logica siano la stessa cosa. Non dare per scontato che solo perché sono lo stesso oggetto fisico, dovrebbero essere lo stesso oggetto nel codice. Ciò che conta è se l'adozione di quel modello renda più facile la lettura e la scrittura del programmatore. Il fatto è che, adottando un modello più semplice in cui ogni scheda fisica viene trattata come una raccolta di schede logiche in modo coerente, il 100% del tempo, risulterà in un codice più semplice.

    
risposta data 25.09.2015 - 05:53
fonte
8

Per essere sincero, penso che la soluzione proposta sia troppo restrittiva e troppo contorta e disgiunta dalla realtà fisica, è un modello, con un piccolo vantaggio.

Suggerirei una delle due alternative:

Opzione 1. Tratta come singola carta, identificata come Half A // Half B , come gli elenchi del sito MTG Wear // Tear . Ma, permetti al tuo Card di entità di contenere N di ciascun attributo: nome-giocabile, costo di mana, tipo, rarità, testo, effetti, ecc.

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

Opzione 2. Non è tutto ciò che è dissimile dall'opzione 1, modellandola dopo la realtà fisica. Hai un'entità Card che rappresenta una scheda fisica . Inoltre, lo scopo è di mantenere N Playable cose. Quelli di Playable possono avere ciascuno un nome distinto, costo di mana, lista di effetti, elenco di abilità, ecc. E il tuo "fisico" Card può avere il proprio identificatore (o nome) che è un composto di ciascuno % nome diPlayable, proprio come sembra fare il database MTG.

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

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

Penso che entrambe queste opzioni siano abbastanza vicine alla realtà fisica. E, penso che sarà vantaggioso per chiunque guardi il tuo codice. (Come te stesso in 6 mesi.)

    
risposta data 25.09.2015 - 04:08
fonte
5

The purpose of these objects is really just to hold properties of the card. They don't really do anything by themselves.

Questa frase è un segno che c'è qualcosa di sbagliato nel tuo progetto: in OOP, ogni classe dovrebbe avere esattamente un ruolo, e la mancanza di comportamento rivela un potenziale Classe di dati , che è un cattivo odore nel codice.

After all, they are cards that represent themselves!

IMHO, suona un po 'strano, e anche un po' strano. Un oggetto di tipo "Carta" dovrebbe rappresentare una carta. Periodo.

Non so nulla di Magic: The gathering , ma suppongo che tu voglia usare le tue carte in un modo simile, qualunque sia la loro struttura attuale: vuoi mostrare una rappresentazione di stringa, vuoi calcola un valore di attacco, ecc.

Per il problema che descrivi, ti consiglierei un modello di design Composit , nonostante il fatto che questo DP sia tipicamente presentato per risolvere un problema più generale:

  1. Crea un'interfaccia Card , come già fatto.
  2. Crea un ConcreteCard , che implementa Card e definisce una semplice carta faccia. Non esitare a mettere il comportamento di una carta normale in questa classe.
  3. Crea un CompositeCard , che implementa Card e ha due addizionali (e a priori private) Card s. Li chiamiamo leftCard e rightCard .

L'eleganza dell'approccio è che un CompositeCard contiene due carte, che possono essere sia ConcreteCard che CompositeCard. Nel tuo gioco, leftCard e rightCard saranno probabilmente sistematicamente ConcreteCard s, ma il modello di progettazione ti consente di progettare composizioni di livello superiore gratuitamente se lo desideri. La manipolazione della tua carta non terrà conto del tipo reale delle tue carte e quindi non hai bisogno di cose come il cast in sottoclasse.

CompositeCard deve implementare i metodi specificati in Card , naturalmente, e lo farà tenendo conto del fatto che tale carta è composta da 2 carte (più, se vuoi, qualcosa di specifico per la CompositeCard stessa carta. Ad esempio, potresti desiderare la seguente implementazione:

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();
   }

   ...
}

Facendo ciò, puoi usare un CompositeCard esattamente come fai per qualsiasi Card , e il comportamento specifico è nascosto grazie al polimorfismo.

Se sei sicuro che un CompositeCard conterrà sempre due% normaliCard s, puoi mantenere l'idea e utilizzare semplicemente ConcreateCard come tipo per leftCard e rightCard .

    
risposta data 25.09.2015 - 07:38
fonte
3

Forse tutto è una carta quando è nel mazzo o nel cimitero, e quando la giochi, costruisci una creatura, terra, incantesimo, ecc. da uno o più oggetti carta, che implementano o estendono giocabili. Quindi, un composito diventa un singolo giocabile il cui costruttore prende due carte parziali e una carta con un calciatore diventa un giocabile il cui costruttore accetta un argomento di mana. Il tipo riflette ciò che puoi fare con esso (disegnare, bloccare, dissipare, toccare) e ciò che può influenzarlo. Oppure un giocabile è solo una carta che deve essere accuratamente ripristinata (perdendo bonus e contatori, divisa) quando viene tolta dal gioco, se è davvero utile usare la stessa interfaccia per invocare una carta e prevedere cosa fa.

Forse l'effetto ha-a della carta e giocabile

    
risposta data 24.09.2015 - 23:46
fonte
3

Il modello di visitatore è una tecnica classica per il recupero di informazioni di tipo nascosto. Possiamo usarlo qui (una leggera variazione qui) per discernere tra i due tipi anche quando sono memorizzati in variabili di astrazione superiore.

Iniziamo con quell'astrazione più alta, un'interfaccia Card :

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

Potrebbe esserci un po 'più di comportamento nell'interfaccia Card , ma la maggior parte dei getter di proprietà si spostano in una nuova classe, CardProperties :

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

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

Ora possiamo avere un SimpleCard che rappresenta un'intera carta con un singolo set di proprietà:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

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

Vediamo come stanno cominciando a funzionare CardProperties e il CardVisitor ancora da scrivere. Facciamo un CompoundCard per rappresentare una carta con due facce:

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

    // Constructors, ...

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

Il CardVisitor inizia a emergere. Proviamo a scrivere quell'interfaccia ora:

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

(Questa è una prima versione dell'interfaccia per ora. Possiamo apportare miglioramenti, che saranno discussi in seguito.)

Ora abbiamo arricchito tutte le parti. Ora dobbiamo solo metterli insieme:

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
         }
     });
 }

Il runtime gestirà l'invio alla versione corretta del polimorfismo del metodo #visit through piuttosto che tentare di romperlo.

Invece di utilizzare una classe anonima, puoi addirittura promuovere CardVisitor in una classe interna o anche in una classe completa se il comportamento è riutilizzabile o se desideri la possibilità di scambiare comportamenti in fase di runtime.

Possiamo utilizzare le classi così come sono ora, ma ci sono alcuni miglioramenti che possiamo apportare all'interfaccia CardVisitor . Ad esempio, potrebbe venire un momento in cui Card s può avere tre o quattro o cinque facce. Piuttosto che aggiungere nuovi metodi da implementare, potremmo semplicemente prendere il secondo metodo e array anziché due parametri. Questo ha senso se le carte multifaccia sono trattate in modo diverso, ma il numero di volti sopra uno è trattato in modo simile.

Potremmo anche convertire CardVisitor in una classe astratta invece di un'interfaccia e avere implementazioni vuote per tutti i metodi. Questo ci consente di implementare solo i comportamenti a cui siamo interessati (forse siamo interessati solo a% single-faced Card s). Possiamo inoltre aggiungere nuovi metodi senza forzare ogni classe esistente a implementare tali metodi o non riuscire a compilare.

    
risposta data 25.09.2015 - 02:34
fonte

Leggi altre domande sui tag