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 Card
apenas 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).
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 Card
que estou chamando PartialCard
(metade de um cartão de duas partes) e WholeCard
(um cartão comum). PartialCard
possui dois métodos adicionais: PartialCard otherPart()
e boolean isFirstPart()
.
Representantes
Se eu tiver um deck, ele deve ser composto por WholeCard
s, não Card
s, como Card
poderia 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 WholeCard
ou dois PartialCard
s. Estou provisoriamente chamando este tipo Representative
, e Card
teria o método getRepresentative()
. A Representative
forneceria 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 getRepresentative
como return this;
.
Quanto a PartialCards
, eles não se representam, mas eles têm um externo Representative
que não é um Card
, mas fornecem métodos para acessar os dois PartialCard
s.
Eu acho que essa hierarquia de tipos faz sentido, mas é complicado. Se pensarmos em Card
s como "cartões conceituais" Representative
es 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 PartialCard
s e WholeCards
são ambos Card
s, 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 PartialCard
s 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
, Representative
precisaria ser convertido para a WholeCard
ou Composite
para acessar os Card
s 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.
fonte
Respostas:
Parece-me que você deveria ter uma aula como
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.
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.
fonte
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
Card
entidade contenha N de cada atributo: nome jogável, custo de mana, tipo, raridade, texto, efeitos, etc.Opção 2. Nem tudo o que é diferente da Opção 1, modele-a segundo a realidade física. Você tem uma
Card
entidade que representa um cartão físico . E, seu objetivo é guardar NPlayable
coisas. EssesPlayable
's cada um pode ter um nome distinto, custo de mana, lista de efeitos, lista de habilidades, etc .. E o seu 'físico'Card
pode ter seu próprio identificador (ou nome) que é um composto de cadaPlayable
' s nome, bem como o banco de dados MTG parece funcionar.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.)
fonte
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.
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:
Card
interface, como você já fez.ConcreteCard
, que implementeCard
e defina um cartão de face simples. Não hesite em colocar o comportamento de uma carta normal nessa classe.CompositeCard
, que implementaCard
e tem dois adicionais (e a priori private)Card
s. Vamos chamá-losleftCard
erightCard
.A elegância da abordagem é que a
CompositeCard
contém dois cartões, que eles mesmos podem ser ConcreteCard ou CompositeCard. No seu jogo,leftCard
erightCard
provavelmente será sistematicamenteConcreteCard
s, 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.CompositeCard
deve implementar os métodos especificadosCard
, é 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 oCompositeCard
próprio cartão. Por exemplo, você pode seguinte implementação:Ao fazer isso, você pode usar
CompositeCard
exatamente o que você faz para qualquer umCard
, e o comportamento específico fica oculto graças ao polimorfismo.Se você tem certeza de que a
CompositeCard
sempre conterá doisCard
s normais , você pode manter a ideia e simplesmente usarConcreateCard
como um tipo paraleftCard
erightCard
.fonte
Card
emCompositeCard
que está a implementar o padrão Decorator . Eu também recomendo que o OP use esta solução, o decorador é o caminho a seguir!CompositeCard
que não exponha métodos adicionais,CompositeCard
é apenas um decorador.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.
fonte
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
Card
interface:Pode haver um pouco mais de comportamento na
Card
interface, mas a maioria dos getters de propriedades passa para uma nova classeCardProperties
:Agora podemos
SimpleCard
representar um cartão inteiro com um único conjunto de propriedades:Vemos como o
CardProperties
e o que ainda está para ser escritoCardVisitor
estão começando a se encaixar. Vamos fazer aCompoundCard
para representar um cartão com duas faces:O
CardVisitor
começa a emergir. Vamos tentar escrever essa interface agora:(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:
O tempo de execução manipulará o envio para a versão correta do
#visit
método por meio de polimorfismo, em vez de tentar quebrá-lo.Em vez de usar uma classe anônima, você pode até promover
CardVisitor
uma 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
CardVisitor
interface. Por exemplo, pode chegar um momento em queCard
s 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
CardVisitor
para 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 caraCard
). Também podemos adicionar novos métodos sem forçar todas as classes existentes a implementar esses métodos ou deixar de compilar.fonte