Como criar um tipo de dados para algo que represente a si próprio ou duas outras coisas

16

fundo

Aqui está o problema real em que estou trabalhando: quero uma maneira de representar cartas no jogo de cartas Magic: The Gathering . A maioria das cartas do jogo são de aparência normal, mas algumas são divididas em duas partes, cada uma com seu próprio nome. Cada metade desses cartões de duas partes é tratada como um cartão em si. Portanto, para maior clareza, usarei Cardapenas para me referir a algo que é um cartão comum ou metade de um cartão de duas partes (em outras palavras, algo com apenas um nome).

cartões

Portanto, temos um tipo base, cartão. O objetivo desses objetos é realmente apenas manter as propriedades do cartão. Eles realmente não fazem nada sozinhos.

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

Existem duas subclasses de Cardque estou chamando PartialCard(metade de um cartão de duas partes) e WholeCard(um cartão comum). PartialCardpossui dois métodos adicionais: PartialCard otherPart()e boolean isFirstPart().

Representantes

Se eu tiver um deck, ele deve ser composto por WholeCards, não Cards, como Cardpoderia ser um PartialCard, e isso não faria sentido. Então, eu quero um objeto que represente um "cartão físico", ou seja, algo que possa representar um WholeCardou dois PartialCards. Estou provisoriamente chamando este tipo Representative, e Cardteria o método getRepresentative(). A Representativeforneceria quase nenhuma informação direta no (s) cartão (s) que representa, apenas apontaria para ele / eles. Agora, minha idéia brilhante / louca / burra (você decide) é que o WholeCard herda de ambos Card e Representative. Afinal, são cartões que se representam! WholeCards poderia implementar getRepresentativecomo return this;.

Quanto a PartialCards, eles não se representam, mas eles têm um externo Representativeque não é um Card, mas fornecem métodos para acessar os dois PartialCards.

Eu acho que essa hierarquia de tipos faz sentido, mas é complicado. Se pensarmos em Cards como "cartões conceituais" Representativees como "cartões físicos", bem, a maioria dos cartões é ambos! Eu acho que você poderia argumentar que os cartões físicos contêm de fato cartões conceituais e que eles não são a mesma coisa , mas eu argumentaria que eles são.

Necessidade de fundição

Como PartialCards e WholeCardssão ambos Cards, e geralmente não há uma boa razão para separá-los, normalmente eu apenas estaria trabalhando Collection<Card>. Então, às vezes, eu precisaria converter PartialCards para acessar seus métodos adicionais. No momento, estou usando o sistema descrito aqui porque realmente não gosto de elencos explícitos. E como Card, Representativeprecisaria ser convertido para a WholeCardou Compositepara acessar os Cards reais que eles representam.

Então, apenas para resumo:

  • Tipo de base Representative
  • Tipo de base Card
  • Tipo WholeCard extends Card, Representative(não é necessário acesso, ele representa a si próprio)
  • Tipo PartialCard extends Card(dá acesso a outra parte)
  • Tipo Composite extends Representative(dá acesso às duas partes)

Isso é loucura? Acho que faz muito sentido, mas sinceramente não tenho certeza.

quebra-código
fonte
1
Os cartões PartialCards são efetivamente cartões diferentes dos que existem dois por cartão físico? A ordem em que são tocadas importa? Você não pode apenas criar uma classe "Deck Slot" e "WholeCard" e permitir que o DeckSlots tenha vários WholeCards e, em seguida, fazer algo como DeckSlot.WholeCards.Play e se houver 1 ou 2, reproduzir os dois como se fossem separados cartões? Parece dado o seu design muito mais complicado que deve haver algo que eu estou ausente :)
enderland
Eu realmente não sinto que posso usar o polimorfismo entre cartões inteiros e parciais para resolver esse problema. Sim, se eu quisesse uma função "play", poderia implementá-la facilmente, mas o problema são cartões parciais e cartões inteiros com diferentes conjuntos de atributos. Eu realmente preciso ser capaz de visualizar esses objetos como eles são, não apenas como sua classe base. A menos que haja uma solução melhor para esse problema. Mas descobri que o polimorfismo não se combina bem com tipos que compartilham uma classe base comum, mas têm métodos diferentes entre eles, se isso faz sentido.
Codebreaker
1
Sinceramente, acho isso ridiculamente complicado. Parece que apenas o modelo "é um" relacionamento, levando a um design muito rígido com acoplamento apertado. Mais interessante seria o que esses cartões estão realmente fazendo?
Daniel Jour
2
Se eles são "apenas objetos de dados", meu instinto seria ter uma classe que contenha uma matriz de objetos de uma segunda classe e deixar assim; sem herança ou outras complicações inúteis. Se essas duas classes devem ser PlayableCard e DrawableCard ou WholeCard e CardPart, não faço ideia, porque não sei o suficiente sobre como o jogo funciona, mas tenho certeza que você pode pensar em algo para chamá-las.
Ixrec 24/09/2015
1
Que tipo de propriedades eles possuem? Você pode dar exemplos de ações que usam essas propriedades?
Daniel Jour

Respostas:

14

Parece-me que você deveria ter uma aula como

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

O código relacionado ao cartão físico pode lidar com a classe do cartão físico e o código relacionado ao cartão lógico pode lidar com isso.

Acho que você poderia argumentar que os cartões físicos contêm de fato cartões conceituais e que não são a mesma coisa, mas eu argumentaria que são.

Não importa se você acha que o cartão físico e o lógico são a mesma coisa. Não presuma que, apenas porque eles são o mesmo objeto físico, eles devem ser o mesmo objeto no seu código. O que importa é se a adoção desse modelo facilita a leitura e gravação do codificador. O fato é que a adoção de um modelo mais simples, no qual cada cartão físico é tratado como uma coleção de cartões lógicos de forma consistente, 100% do tempo, resultará em um código mais simples.

Winston Ewert
fonte
2
Promovido porque esta é a melhor resposta, embora não pelo motivo exposto. Essa é a melhor resposta não porque é simples, mas porque cartões físicos e conceituais não são a mesma coisa, e essa solução modela corretamente o relacionamento deles. É verdade que um bom modelo de POO nem sempre reflete a realidade física, mas deve sempre refletir a realidade conceitual. A simplicidade é boa, é claro, mas leva um banco traseiro para a correção conceitual.
Kevin Krumwiede
2
@KevinKrumwiede, a meu ver, não existe uma única realidade conceitual. Pessoas diferentes pensam da mesma coisa de maneiras diferentes, e as pessoas podem mudar a maneira como pensam sobre isso. Você pode pensar em cartões físicos e lógicos como entidades separadas. Ou você pode pensar nos cartões divididos como algum tipo de exceção à noção geral de um cartão. Nenhuma delas é intrinsecamente incorreta, mas a que se presta a modelagem mais simples.
Winston Ewert
8

Para ser franco, acho que a solução proposta é muito restritiva, muito distorcida e desarticulada da realidade física: são modelos, com pouca vantagem.

Eu sugeriria uma das duas alternativas:

Opção 1. Trate-o como um único cartão, identificado como Metade A // Metade B , como o site MTG lista Wear // Tear . Mas permita que sua Cardentidade contenha N de cada atributo: nome jogável, custo de mana, tipo, raridade, texto, efeitos, etc.

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

Opção 2. Nem tudo o que é diferente da Opção 1, modele-a segundo a realidade física. Você tem uma Cardentidade que representa um cartão físico . E, seu objetivo é guardar N Playable coisas. Esses Playable's cada um pode ter um nome distinto, custo de mana, lista de efeitos, lista de habilidades, etc .. E o seu 'físico' Cardpode ter seu próprio identificador (ou nome) que é um composto de cada Playable' s nome, bem como o banco de dados MTG parece funcionar.

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

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

Eu acho que qualquer uma dessas opções está bem próxima da realidade física. E acho que será benéfico para quem olha o seu código. (Como você mesmo em 6 meses.)

svidgen
fonte
5

O objetivo desses objetos é realmente apenas manter as propriedades do cartão. Eles realmente não fazem nada sozinhos.

Essa frase é um sinal de que há algo errado no seu design: no OOP, cada classe deve ter exatamente uma função, e a falta de comportamento revela uma Classe de Dados em potencial , que é um mau cheiro no código.

Afinal, são cartões que se representam!

IMHO, parece um pouco estranho, e até um pouco estranho. Um objeto do tipo "Cartão" deve representar um cartão. Período.

Não sei nada sobre Magic: The gathering , mas acho que você deseja usar suas cartas de maneira semelhante, seja qual for a estrutura real: deseja exibir uma representação de string, calcular um valor de ataque etc.

Para o problema que você descreve, eu recomendaria um padrão de design de composição , apesar do DP normalmente ser apresentado para resolver um problema mais geral:

  1. Crie uma Cardinterface, como você já fez.
  2. Crie um ConcreteCard, que implemente Carde defina um cartão de face simples. Não hesite em colocar o comportamento de uma carta normal nessa classe.
  3. Crie a CompositeCard, que implementa Carde tem dois adicionais (e a priori private) Cards. Vamos chamá-los leftCarde rightCard.

A elegância da abordagem é que a CompositeCardcontém dois cartões, que eles mesmos podem ser ConcreteCard ou CompositeCard. No seu jogo, leftCarde rightCardprovavelmente será sistematicamente ConcreteCards, mas o Design Pattern permite que você crie composições de nível superior gratuitamente, se desejar. A manipulação de seu cartão não levará em consideração o tipo real de seus cartões e, portanto, você não precisa de coisas como transmissão para subclasse.

CompositeCarddeve implementar os métodos especificados Card, é claro, e fará isso levando em consideração o fato de que esse cartão é composto de 2 cartões (mais, se você desejar, algo específico para o CompositeCardpróprio cartão. Por exemplo, você pode seguinte implementação:

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

   ...
}

Ao fazer isso, você pode usar CompositeCardexatamente o que você faz para qualquer um Card, e o comportamento específico fica oculto graças ao polimorfismo.

Se você tem certeza de que a CompositeCardsempre conterá dois Cards normais , você pode manter a ideia e simplesmente usar ConcreateCardcomo um tipo para leftCarde rightCard.

mgoeminne
fonte
Você está falando sobre o padrão Composite , mas na verdade desde que você está segurando duas referências de Cardem CompositeCardque está a implementar o padrão Decorator . Eu também recomendo que o OP use esta solução, o decorador é o caminho a seguir!
Visto
Não vejo por que ter duas instâncias de cartão faz da minha classe um decorador. De acordo com o seu próprio link, um decorador adiciona recursos a um único objeto e é uma instância da mesma classe / interface que esse objeto. Enquanto, de acordo com seu outro link, um composto permite a propriedade de vários objetos da mesma classe / interface. Mas, no final das contas, as palavras não importam, e apenas a ideia é ótima.
precisa saber é o seguinte
@ Spotted Este definitivamente não é o padrão do decorador, pois o termo é usado em Java. O padrão decorador é implementado substituindo métodos em um objeto individual, criando uma classe anônima exclusiva para esse objeto.
Kevin Krumwiede
@KevinKrumwiede, desde CompositeCardque não exponha métodos adicionais, CompositeCardé apenas um decorador.
manchado
"... Design Pattern permite que você crie composições de nível superior gratuitamente" - não, não é de graça , exatamente o oposto - é pelo preço de ter uma solução mais complicada do que o necessário para os requisitos.
Doc Brown
3

Talvez tudo seja um Card quando está no baralho ou no cemitério, e quando você o joga, você constrói uma Criatura, Terra, Encantamento etc. a partir de um ou mais objetos do Card, todos os quais implementam ou estendem o Playable. Então, um composto se torna um único jogável cujo construtor recebe duas cartas parciais e uma carta com um kicker se torna um jogável cujo construtor usa um argumento de mana. O tipo reflete o que você pode fazer com ele (desenhar, bloquear, dissipar, tocar) e o que pode afetá-lo. Ou um Playable é apenas um card que precisa ser revertido com cuidado (perdendo bônus e marcadores, sendo dividido) quando retirado do jogo, se for realmente útil usar a mesma interface para invocar um card e prever o que ele faz.

Talvez o Card e o Playable tenham um efeito.

Davislor
fonte
Infelizmente, não jogo essas cartas. Eles são realmente apenas objetos de dados que podem ser consultados. Se você deseja uma estrutura de dados de deck, deseja uma Lista <WholeCard> ou algo assim. Se você deseja fazer uma pesquisa por cartões instantâneos verdes, convém uma Lista <Cartão>.
Codebreaker
Ah ok. Não é muito relevante para o seu problema, então. Devo excluir?
Davislor
3

O padrão de visitante é uma técnica clássica para recuperar informações de tipo oculto. Podemos usá-lo (uma pequena variação aqui) aqui para discernir entre os dois tipos, mesmo quando eles são armazenados em variáveis ​​de abstração mais alta.

Vamos começar com essa abstração mais alta, uma Cardinterface:

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

Pode haver um pouco mais de comportamento na Cardinterface, mas a maioria dos getters de propriedades passa para uma nova classe CardProperties:

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

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

Agora podemos SimpleCardrepresentar um cartão inteiro com um único conjunto de propriedades:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

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

Vemos como o CardPropertiese o que ainda está para ser escrito CardVisitorestão começando a se encaixar. Vamos fazer a CompoundCardpara representar um cartão com duas faces:

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

    // Constructors, ...

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

O CardVisitorcomeça a emergir. Vamos tentar escrever essa interface agora:

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

(Esta é a primeira versão da interface por enquanto. Podemos fazer melhorias, que serão discutidas mais adiante.)

Agora, desenvolvemos todas as partes. Agora só precisamos reuni-los:

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

O tempo de execução manipulará o envio para a versão correta do #visitmétodo por meio de polimorfismo, em vez de tentar quebrá-lo.

Em vez de usar uma classe anônima, você pode até promover CardVisitoruma classe interna ou até uma classe completa se o comportamento for reutilizável ou se você quiser trocar o comportamento em tempo de execução.


Podemos usar as classes como estão agora, mas existem algumas melhorias que podemos fazer na CardVisitorinterface. Por exemplo, pode chegar um momento em que Cards possa ter três, quatro ou cinco faces. Em vez de adicionar novos métodos para implementar, poderíamos ter o segundo método take e array em vez de dois parâmetros. Isso faz sentido se as cartas com várias faces forem tratadas de maneira diferente, mas o número de faces acima de uma for tratado de maneira semelhante.

Também podemos converter CardVisitorpara uma classe abstrata em vez de uma interface e ter implementações vazias para todos os métodos. Isso nos permite implementar apenas os comportamentos nos quais estamos interessados ​​(talvez apenas nos interessemos por uma única cara Card). Também podemos adicionar novos métodos sem forçar todas as classes existentes a implementar esses métodos ou deixar de compilar.

cbojar
fonte
1
Eu acho que isso poderia ser facilmente expandido para o outro tipo de cartão de duas faces (frente e verso em vez de lado a lado). + +
RubberDuck 25/09
Para uma exploração adicional, o uso de um Visitante para explorar métodos específicos para uma subclasse às vezes é chamado de Despacho Múltiplo. O Double Dispatch pode ser uma solução interessante para o problema.
Mgeminemin
1
Estou votando negativamente porque acho que o padrão de visitante adiciona complexidade e acoplamento desnecessários ao código. Como uma alternativa está disponível (veja a resposta de mgoeminne), eu não a usaria.
Visto
@ Spotted O visitante é um padrão complexo, e eu também considerei o padrão composto ao escrever a resposta. A razão pela qual fui com o visitante é porque o OP quer tratar coisas semelhantes de maneira diferente, em vez de coisas diferentes da mesma forma. Se as cartas tiveram mais comportamento e estavam sendo usadas na jogabilidade, o padrão composto permite combinar dados para produzir uma estatística unificada. Porém, são apenas pacotes de dados, possivelmente usados ​​para renderizar uma exibição de cartão; nesse caso, obter informações separadas parecia mais útil do que um simples agregador composto.
Cbojar