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


16

sfondo

Ecco il vero problema sul quale 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 trattata come una carta stessa. Quindi, per chiarezza, userò Cardsolo per fare riferimento a qualcosa che è una carta normale o metà di una carta in due parti (in altre parole, qualcosa con un solo nome).

carte

Quindi abbiamo un tipo di base, il card. Lo scopo di questi oggetti è proprio quello di contenere le proprietà della carta. Non fanno davvero nulla da soli.

interface Card {
    String name();
    String text();
    // etc
}

Esistono due sottoclassi di Card, che sto chiamando PartialCard(metà di una carta in due parti) e WholeCard(una carta normale). PartialCardha due metodi aggiuntivi: PartialCard otherPart()e boolean isFirstPart().

rappresentanti

Se ho un mazzo, dovrebbe essere composto da WholeCards, non da Cards, come Cardpotrebbe essere a PartialCard, e non avrebbe senso. Quindi voglio un oggetto che rappresenti una "carta fisica", cioè qualcosa che possa rappresentare una WholeCardo due PartialCards. Sto provvisoriamente chiamando questo tipo Representativee Cardavrei il metodo getRepresentative(). A Representativenon fornirebbe quasi nessuna informazione diretta sulla o sulle carte che rappresenta, indicherà solo loro. Ora, la mia idea geniale / pazza / stupida (decidi) è che WholeCard eredita da entrambi Card e Representative. Dopotutto, sono carte che si rappresentano! WholeCard potrebbe implementare getRepresentativecome return this;.

Per quanto riguarda PartialCards, non rappresentano se stessi, ma hanno un esterno Representativeche non è un Card, ma fornisce metodi per accedere alle due PartialCards.

Penso che questa gerarchia di tipi abbia senso, ma è complicata. Se pensiamo a Cards come "carte concettuali" e Representatives come "carte fisiche", beh, la maggior parte delle carte sono entrambe! Penso che potresti argomentare che le carte fisiche contengono effettivamente carte concettuali e che non sono la stessa cosa , ma direi che lo sono.

Necessità della fusione del tipo

Perché PartialCards e WholeCardssono entrambi Cards, e di solito c'è nessuna buona ragione per separare fuori, avrei normalmente solo lavorare con Collection<Card>. Quindi a volte dovrei lanciare PartialCards per accedere ai loro metodi aggiuntivi. In questo momento, sto usando il sistema descritto qui perché non mi piacciono davvero i cast espliciti. E come Card, Representativedovrebbe essere lanciato su uno WholeCardo Composite, per accedere ai messaggi reali Cardche rappresentano.

Quindi, solo per un riepilogo:

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

È folle? Penso che abbia davvero molto senso, ma sinceramente non ne sono sicuro.


1
Le carte parziali sono effettivamente carte diverse da quelle ce ne sono due per ogni carta fisica? L'ordine in cui vengono riprodotti è importante? Potresti non solo creare una classe "Deck Slot" e "WholeCard" e consentire ai DeckSlot di avere più WholeCard, e quindi fare semplicemente qualcosa come DeckSlot.WholeCards.Play e se ce ne sono 1 o 2 giocare entrambi come se fossero separati carte? Sembra che, dato il tuo design molto più complicato, ci debba essere qualcosa che mi manca :)
Enderland

Davvero non mi sento di poter usare il polimorfismo tra carte intere e carte parziali per risolvere questo problema. Sì, se volessi una funzione di "gioco", potrei implementarla facilmente, ma il problema è che le carte parziali e le carte intere hanno diversi set di attributi. Devo davvero essere in grado di vedere questi oggetti come sono, non solo come la loro classe base. A meno che non ci sia una soluzione migliore a quel problema. Ma ho scoperto che il polimorfismo non si combina bene con i tipi che condividono una classe base comune, ma hanno metodi diversi tra loro, se questo ha senso.
codebreaker,

1
Onestamente, penso che sia ridicolmente complicato. Sembri solo modellare "è un" rapporto, portando a un design molto rigido con accoppiamento stretto. Più interessante sarebbe ciò che quelle carte stanno effettivamente facendo?
Daniel Jour,

2
Se sono "solo oggetti dati", allora il mio istinto sarebbe quello di avere una classe che contiene una matrice di oggetti di una seconda classe, e lasciarla a quello; nessuna eredità o altre complicazioni inutili. Se queste due classi debbano essere PlayableCard e DrawableCard o WholeCard e CardPart non ho idea, perché non so abbastanza su come funziona il tuo gioco, ma sono sicuro che puoi pensare a qualcosa che possa chiamarle.
Ixrec,

1
Che tipo di proprietà detengono? Puoi fornire esempi di azioni che utilizzano queste proprietà?
Daniel Jour,

Risposte:


14

Mi sembra che dovresti avere una lezione simile

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

Il codice relativo alla carta fisica può gestire la classe di carta fisica e il codice relativo alla carta logica può occuparsene.

Penso che potresti argomentare che le carte fisiche contengono effettivamente carte concettuali e che non sono la stessa cosa, ma direi che lo sono.

Non importa se pensi che la scheda fisica e 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 rende il programmatore più facile da leggere e scrivere. Il fatto è che l'adozione di un modello più semplice in cui ogni carta fisica viene trattata come una raccolta di carte logiche in modo coerente, il 100% delle volte, si tradurrà in un codice più semplice.


2
È stato votato perché questa è la risposta migliore, anche se non per il motivo indicato. Questa è la risposta migliore non perché sia ​​semplice, ma perché le carte fisiche e le carte concettuali non sono la stessa cosa, e questa soluzione modella correttamente la loro relazione. È vero che un buon modello OOP non riflette sempre la realtà fisica, ma dovrebbe sempre riflettere la realtà concettuale. La semplicità è buona, ovviamente, ma fa un passo indietro rispetto alla correttezza concettuale.
Kevin Krumwiede,

2
@KevinKrumwiede, secondo me non esiste un'unica realtà concettuale. Diverse persone pensano alla stessa cosa in modi diversi e le persone possono cambiare il modo in cui la pensano. Puoi pensare alle carte fisiche e logiche come entità separate. Oppure puoi pensare alle carte divise come una sorta di eccezione alla nozione generale di una carta. Nessuno dei due è intrinsecamente errato, ma quello si presta alla modellazione più semplice.
Winston Ewert,

8

Ad essere sincero, penso che la soluzione proposta sia troppo restrittiva, troppo contorta e disgiunta dalla realtà fisica siano modelli, con scarso vantaggio.

Suggerirei una delle due alternative:

Opzione 1. Trattala come una singola carta, identificata come metà A // metà B , come negli elenchi dei siti MTG Wear // Tear . Tuttavia, consenti alla tua Cardentità 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 molto diversamente dall'opzione 1, modellala secondo la realtà fisica. Hai Cardun'entità che rappresenta una carta fisica . E il suo scopo è quindi quello di contenere N Playable cose. Quelle Playable's possono avere ciascuno un nome distinta, il costo di mana, lista degli effetti, l'elenco delle abilità, ecc .. E il tuo 'fisica' Cardpuò avere il proprio identificativo (o il nome), che è un composto di ogni Playable' s nome, molto simile il database MTG sembra fare.

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

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

Penso che una di queste opzioni sia abbastanza vicina alla realtà fisica. E penso che sarà utile a chiunque guardi il tuo codice. (Come te stesso tra 6 mesi.)


5

Lo scopo di questi oggetti è proprio quello di contenere le proprietà della carta. Non fanno davvero nulla da soli.

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

Dopotutto, sono carte che si rappresentano!

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

Non so nulla di Magic: il raduno , ma immagino che tu voglia usare le tue carte in un modo simile, qualunque sia la loro struttura reale: vuoi mostrare una rappresentazione di stringa, vuoi calcolare un valore di attacco, ecc.

Per il problema che descrivi, consiglierei un modello di progettazione composita , nonostante il fatto che questa DP sia in genere presentata per risolvere un problema più generale:

  1. Creare un Card un'interfaccia, come hai già fatto.
  2. Crea un ConcreteCard, che implementa Carde definisce una semplice carta scoperta. Non esitate a mettere il comportamento di un normale carta in questa classe.
  3. Crea un CompositeCard, che implementa Carde ha due ulteriori (e a priori privati) Card. Chiamiamoli leftCarde rightCard.

L'eleganza dell'approccio è che a CompositeCardcontiene due carte, che a loro volta possono essere sia ConcreteCard che CompositeCard. Nel tuo gioco, leftCarde rightCardprobabilmente sarà sistematicamente ConcreteCards, ma il Design Pattern ti consente di progettare composizioni di livello superiore gratuitamente, se lo desideri. La manipolazione delle tue carte non terrà conto del tipo reale delle tue carte, e quindi non hai bisogno di cose come il cast da sottoclassare.

CompositeCarddeve implementare i metodi specificati in Card, ovviamente, e lo farà tenendo conto del fatto che tale carta è composta da 2 carte (più, se vuoi, qualcosa di specifico per la CompositeCardcarta stessa. Ad esempio, potresti voler 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();
   }

   ...
}

In questo modo, puoi usare CompositeCardesattamente come fai per qualsiasi Card, e il comportamento specifico viene nascosto grazie al polimorfismo.

Se sei sicuro CompositeCardche conterrà sempre due normali Card, puoi mantenere l'idea e semplicemente usare ConcreateCardcome tipo per leftCarde rightCard.


Si sta parlando della composite , ma in realtà da quando si tiene in mano due riferimenti di Cardin CompositeCardsi implementa il pattern DECORATOR . Raccomando anche all'OP di utilizzare questa soluzione, decoratore è la strada da percorrere!
Scoperto il

Non vedo perché avere due istanze di Card renda la mia classe un decoratore. Secondo il tuo link, un decoratore aggiunge funzionalità a un singolo oggetto ed è esso stesso un'istanza della stessa classe / interfaccia di questo oggetto. Mentre, secondo l'altro tuo collegamento, un composito consente di possedere più oggetti della stessa classe / interfaccia. Ma alla fine le parole non contano e solo l'idea è fantastica.
mgoeminne,

@Spotted Questo non è sicuramente il motivo del decoratore poiché il termine è usato in Java. Il modello di decorazione viene implementato mediante metodi di sostituzione su un singolo oggetto, creando una classe anonima unica per quell'oggetto.
Kevin Krumwiede,

@KevinKrumwiede fintanto che CompositeCardnon espone metodi aggiuntivi, CompositeCardè solo un decoratore.
Scoperto il

"... Design Pattern ti consente di progettare composizioni di livello superiore gratuitamente" - no, non è gratuito , ma al contrario - ha il prezzo di avere una soluzione più complicata del necessario per i requisiti.
Doc Brown,

3

Forse tutto è una Carta quando è nel mazzo o nel cimitero, e quando 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 kicker diventa un giocabile il cui costruttore prende un argomento di mana. Il tipo riflette ciò che puoi fare con esso (disegna, blocca, elimina, tocca) e cosa può influenzarlo. Oppure una giocabile è solo una carta che deve essere accuratamente rimossa (perdendo bonus e segnalini, diviso) quando viene messa fuori gioco, se è davvero utile usare la stessa interfaccia per invocare una carta e prevederne le azioni.

Forse Card e Giocabili hanno un effetto.


Sfortunatamente, non gioco queste carte. Sono in realtà solo oggetti dati che possono essere interrogati. Se si desidera una struttura di dati del mazzo, si desidera un elenco <WholeCard> o qualcosa del genere. Se si desidera eseguire una ricerca di carte istantanee verdi, si desidera un elenco <Card>.
codebreaker

Ah ok. Non molto rilevante per il tuo problema, quindi. Devo cancellare?
Davislor,

3

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

Cominciamo con quell'astrazione più alta, Cardun'interfaccia:

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

Potrebbe esserci un po 'più di comportamento Cardsull'interfaccia, ma la maggior parte dei getter di proprietà passa a una nuova classe CardProperties:

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

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

Ora possiamo avere un SimpleCardrappresentante di 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 iniziando a inserirsi il CardPropertiese il ancora da scrivere CardVisitor. Facciamo un a CompoundCardper 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 CardVisitorinizia ad 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 verranno discussi in seguito.)

Ora abbiamo perfezionato 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 #visitmetodo attraverso il polimorfismo anziché tentare di romperlo.

Invece di utilizzare una classe anonima, puoi persino promuoverla CardVisitora una classe interna o addirittura a una classe completa se il comportamento è riutilizzabile o se desideri la possibilità di scambiare il comportamento in fase di esecuzione.


Possiamo usare le classi così come sono adesso, ma ci sono alcuni miglioramenti che possiamo apportare CardVisitorall'interfaccia. Ad esempio, potrebbe venire un momento in cui Cards può avere tre o quattro o cinque facce. Invece di aggiungere nuovi metodi da implementare, potremmo avere solo il secondo metodo take e array invece di due parametri. Ciò ha senso se le carte a più facce vengono trattate in modo diverso, ma il numero di facce sopra una viene trattato in modo simile.

Potremmo anche convertire CardVisitorin una classe astratta anziché in 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 ai single-face Card). Possiamo anche aggiungere nuovi metodi senza forzare ogni classe esistente ad implementare tali metodi o non riuscire a compilare.


1
Penso che questo potrebbe essere facilmente esteso all'altro tipo di carta a due facce (fronte e retro invece che fianco a fianco). ++
RubberDuck,

Per ulteriori esplorazioni, l'uso di un visitatore per sfruttare metodi specifici di una sottoclasse viene talvolta chiamato Invio multiplo. Double Dispatch potrebbe essere una soluzione interessante al problema.
mgoeminne,

1
Sto votando verso il basso perché penso che il modello di visitatore aggiunga complessità e accoppiamenti non necessari al codice. Poiché è disponibile un'alternativa (vedi la risposta di mgoeminne), non la userei.
Scoperto il

@Spotted Il visitatore è un modello complesso e ho anche considerato il modello composito quando ho scritto la risposta. Il motivo per cui sono andato con il visitatore è perché l'OP vuole trattare cose simili in modo diverso, piuttosto che cose diverse in modo simile. Se le carte avevano un comportamento maggiore e venivano utilizzate per il gioco, il modello composito consente di combinare i dati per produrre una statistica unificata. Sono solo sacchi di dati, tuttavia, probabilmente utilizzati per visualizzare un display a schede, nel qual caso ottenere informazioni separate sembrava più utile di un semplice aggregatore composito.
cbojar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.