Estou tentando modelar um jogo de cartas em que as cartas têm dois conjuntos importantes de recursos:
O primeiro é um efeito. Essas são as alterações no estado do jogo que acontecem quando você joga a carta. A interface para efeito é a seguinte:
boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);
E você pode considerar que a carta é jogável se, e somente se, você puder atingir o custo e todos os efeitos jogáveis. Igual a:
// in Card class
boolean isPlayable(Player p, GameState gs) {
if(p.resource < this.cost) return false;
for(Effect e : this.effects) {
if(!e.isPlayable(p,gs)) return false;
}
return true;
}
Ok, até agora, muito simples.
O outro conjunto de recursos do cartão são habilidades. Essas habilidades são alterações no estado do jogo que você pode ativar à vontade. Ao criar a interface para eles, percebi que eles precisavam de um método para determinar se eles podem ser ativados ou não, e um método para implementar a ativação. Acaba sendo
boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);
E eu percebo que, com exceção de chamá-lo "activar" em vez de "play", Ability
e Effect
tem exatamente a mesma assinatura.
É ruim ter várias interfaces com uma assinatura idêntica? Devo simplesmente usar um e ter dois conjuntos da mesma interface? Assim:
Set<Effect> effects;
Set<Effect> abilities;
Em caso afirmativo, que medidas de refatoração devo seguir adiante se elas se tornarem idênticas (à medida que mais recursos forem lançados), principalmente se forem divergentes (ou seja, ambas ganham algo que a outra não deveria, em oposição a apenas uma) e o outro sendo um subconjunto completo)? Estou particularmente preocupado que combiná-los não seja sustentável assim que algo mudar.
A boa impressão:
Reconheço que esta questão é gerada pelo desenvolvimento de jogos, mas acho que é o tipo de problema que poderia surgir com a mesma facilidade no desenvolvimento não relacionado a jogos, principalmente ao tentar acomodar os modelos de negócios de vários clientes em um aplicativo, como acontece com quase todos os projetos que já fiz com mais de uma influência nos negócios ... Além disso, os trechos usados são trechos de Java, mas isso poderia ser aplicado facilmente a uma infinidade de linguagens orientadas a objetos.
fonte
Respostas:
Só porque duas interfaces têm o mesmo contrato, não significa que sejam a mesma interface.
O Princípio da Substituição de Liskov declara:
Ou, em outras palavras: qualquer coisa que seja verdadeira em uma interface ou supertipo deve ser verdadeira em todos os seus subtipos.
Se estou entendendo sua descrição corretamente, uma habilidade não é um efeito e um efeito não é uma habilidade. Se um deles mudar de contrato, é improvável que o outro mude com ele. Não vejo boas razões para tentar vinculá-los um ao outro.
fonte
Da Wikipedia : "a interface é frequentemente usada para definir um tipo abstrato que não contém dados, mas expõe comportamentos definidos como métodos ". Na minha opinião, uma interface é usada para descrever um comportamento; portanto, se você tem comportamentos diferentes, faz sentido ter interfaces diferentes. Ao ler sua pergunta, a impressão que tive é que você está falando sobre comportamentos diferentes, de modo que interfaces diferentes podem ser a melhor abordagem.
Outro ponto que você disse é que, se um desses comportamentos mudar. Então o que acontece quando você tem apenas uma interface?
fonte
Se as regras do seu jogo de cartas fazem uma distinção entre "efeitos" e "habilidades", você precisa ter certeza de que são interfaces diferentes. Isso impedirá que você use acidentalmente um deles, onde o outro é necessário.
Dito isto, se eles são extremamente semelhantes, pode fazer sentido derivá-los de um ancestral comum. Considere com cuidado: você tem motivos para acreditar que "efeitos" e "habilidades" sempre terão necessariamente a mesma interface? Se você adicionar um elemento à
effect
interface, esperaria precisar do mesmo elemento adicionado àability
interface?Nesse caso, você pode colocar esses elementos em uma
feature
interface comum da qual eles são derivados. Caso contrário, não tente unificá-los - você perderá seu tempo movendo coisas entre as interfaces base e derivadas. No entanto, como você não pretende realmente usar a interface base comum para nada, mas "não se repita", isso pode não fazer muita diferença. E, se você seguir essa intenção, meu palpite é que, se você fizer a escolha errada no início, refatorar para corrigi-la mais tarde pode ser relativamente simples.fonte
O que você está vendo é basicamente um artefato da expressividade limitada dos sistemas de tipos.
Teoricamente, se o seu sistema de tipos permitir que você especifique com precisão o comportamento dessas duas interfaces, as assinaturas deles serão diferentes porque os comportamentos deles são diferentes. Na prática, a expressividade dos sistemas de tipos é limitada por limitações fundamentais, como o Problema de Halting e o Teorema de Rice, de modo que nem todas as facetas do comportamento podem ser expressas.
Agora, sistemas de tipos diferentes têm graus diferentes de expressividade, mas sempre haverá algo que não pode ser expresso.
Por exemplo: duas interfaces com comportamento de erro diferente podem ter a mesma assinatura em C #, mas não em Java (porque em Java as exceções fazem parte da assinatura). Duas interfaces cujo comportamento apenas difere em seus efeitos colaterais podem ter a mesma assinatura em Java, mas terão assinaturas diferentes em Haskell.
Portanto, é sempre possível que você acabe com a mesma assinatura para diferentes comportamentos. Se você acha importante poder distinguir esses dois comportamentos em mais do que apenas um nível nominal, precisará mudar para um sistema de tipo expressivo mais (ou diferente).
fonte