Estou acompanhando essa pergunta , mas estou mudando meu foco do código para um princípio.
Pelo meu entendimento do princípio de substituição de Liskov (LSP), quaisquer que sejam os métodos da minha classe base, eles devem ser implementados na minha subclasse e, de acordo com esta página, se você substituir um método na classe base e ele não fizer nada ou lançar um exceção, você está violando o princípio.
Agora, meu problema pode ser resumido assim: eu tenho um resumo Weapon
class
e duas classes Sword
e Reloadable
. Se Reloadable
contiver um específico method
, chamado Reload()
, eu teria que fazer o downcast para acessá-lo method
e, idealmente, você desejaria evitar isso.
Eu então pensei em usar o Strategy Pattern
. Dessa forma, cada arma estava ciente das ações que é capaz de executar; portanto, por exemplo, uma Reloadable
arma pode obviamente ser recarregada, mas Sword
não pode e nem sequer sabe de uma Reload class/method
. Como afirmei na postagem Stack Overflow, não preciso fazer downcast e posso manter uma List<Weapon>
coleção.
Em outro fórum , a primeira resposta sugerida para permitir Sword
estar ciente Reload
, simplesmente não faça nada. Essa mesma resposta foi dada na página Estouro de pilha à qual vinculei acima.
Eu não entendo completamente o porquê. Por que violar o princípio e permitir que a Sword esteja ciente Reload
, e deixe em branco? Como eu disse no meu post Stack Overflow, o SP praticamente resolveu meus problemas.
Por que não é uma solução viável?
public final Weapon{
private final String name;
private final int damage;
private final List<AttackStrategy> validactions;
private final List<Actions> standardActions;
private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
{
this.name = name;
this.damage = damage;
standardActions = new ArrayList<Actions>(standardActions);
validAttacks = new ArrayList<AttackStrategy>(validActions);
}
public void standardAction(String action){} // -- Can call reload or aim here.
public int attack(String action){} // - Call any actions that are attacks.
public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
return new Weapon(name, damage,standardActions, attacks) ;
}
}
Interface e Implementação de Ataques:
public interface AttackStrategy{
void attack(Enemy enemy);
}
public class Shoot implements AttackStrategy {
public void attack(Enemy enemy){
//code to shoot
}
}
public class Strike implements AttackStrategy {
public void attack(Enemy enemy){
//code to strike
}
}
class Weapon { bool supportsReload(); void reload(); }
. Os clientes testariam se suportado antes de recarregar.reload
é definido contratualmente para lançar iff!supportsReload()
. Isso adere ao LSP se as classes dirigidas aderem ao protocolo que acabei de descrever.reload()
branco ou sestandardActions
não contém uma ação de recarga é apenas um mecanismo diferente. Não há diferença fundamental. Você pode fazer as duas coisas. => Sua solução é viável (qual foi sua pergunta) .; O Sword não precisa saber sobre recarregar se o Weapon contém uma implementação padrão em branco.Respostas:
O LSP está preocupado com subtipagem e polimorfismo. Nem todo o código realmente usa esses recursos; nesse caso, o LSP é irrelevante. Dois casos de uso comuns de construções de linguagem de herança que não são um caso de subtipagem são:
Herança usada para herdar a implementação de uma classe base, mas não sua interface. Em quase todos os casos, a composição deve ser preferida. Idiomas como Java não podem separar herança de implementação e interface, mas, por exemplo, C ++ tem
private
herança.Herança usada para modelar um tipo / união de soma, por exemplo: a
Base
éCaseA
ouCaseB
. O tipo base não declara nenhuma interface relevante. Para usar suas instâncias, você deve convertê-las no tipo de concreto correto. A transmissão pode ser feita com segurança e não é o problema. Infelizmente, muitos idiomas OOP não conseguem restringir os subtipos da classe base apenas aos subtipos pretendidos. Se o código externo pode criar aCaseC
, então o código assume que aBase
pode ser apenas aCaseA
ouCaseB
está incorreto. A Scala pode fazer isso com segurança com seucase class
conceito. Em Java, isso pode ser modelado quandoBase
é uma classe abstrata com um construtor privado, e as classes estáticas aninhadas são herdadas da base.Alguns conceitos, como hierarquias conceituais de objetos do mundo real, mapeiam muito mal em modelos orientados a objetos. Pensamentos como “A arma é uma arma e uma espada é uma arma, por isso eu vou ter uma
Weapon
classe baseGun
eSword
herdar” são enganosas: real-palavra é-um relacionamento não implicam uma tal relação em nosso modelo. Um problema relacionado é que os objetos podem pertencer a várias hierarquias conceituais ou podem alterar sua afiliação hierárquica durante o tempo de execução, que a maioria dos idiomas não pode modelar, pois a herança geralmente é por classe e não por objeto, e definida em tempo de design e não em tempo de execução.Ao projetar modelos de POO, não devemos pensar na hierarquia ou em como uma classe “estende” a outra. Uma classe base não é um lugar para fatorar as partes comuns de várias classes. Em vez disso, pense em como seus objetos serão usados, ou seja, que tipo de comportamento os usuários desses objetos precisam.
Aqui, os usuários podem precisar de
attack()
armas e talvezreload()
deles. Se quisermos criar uma hierarquia de tipos, ambos os métodos devem estar no tipo base, embora armas não recarregáveis possam ignorar esse método e não façam nada quando chamadas. Portanto, a classe base não contém as partes comuns, mas a interface combinada de todas as subclasses. As subclasses não diferem em sua interface, mas apenas na implementação dessa interface.Não é necessário criar uma hierarquia. Os dois tipos
Gun
eSword
podem ser totalmente independentes. Considerando umaGun
latafire()
ereload()
umaSword
única podestrike()
. Se você precisar gerenciar esses objetos polimorficamente, poderá usar o Padrão do Adaptador para capturar os aspectos relevantes. No Java 8, isso é possível de maneira bastante conveniente com interfaces funcionais e referências lambdas / método. Por exemplo, você pode ter umaAttack
estratégia para a qual você fornecemyGun::fire
ou() -> mySword.strike()
.Finalmente, às vezes é sensato evitar subclasses, mas modele todos os objetos através de um único tipo. Isso é particularmente relevante nos jogos porque muitos objetos do jogo não se encaixam muito bem em nenhuma hierarquia e podem ter muitos recursos diferentes. Por exemplo, um RPG pode ter um item que é um item de missão, aumenta suas estatísticas com +2 de força quando equipado, tem 20% de chance de ignorar qualquer dano recebido e fornece um ataque corpo a corpo. Ou talvez uma espada recarregável, porque é * mágica *. Quem sabe o que a história exige.
Em vez de tentar descobrir uma hierarquia de classes para essa bagunça, é melhor ter uma classe que forneça slots para vários recursos. Esses slots podem ser alterados em tempo de execução. Cada slot seria uma estratégia / retorno de chamada como
OnDamageReceived
ouAttack
. Com suas armas, nós podemos terMeleeAttack
,RangedAttack
eReload
slots. Esses slots podem estar vazios; nesse caso, o objeto não fornece esse recurso. Os slots são então chamado condicionalmente:if (item.attack != null) item.attack.perform()
.fonte
Porque ter uma estratégia para
attack
não é suficiente para suas necessidades. Claro, ele permite abstrair quais ações o item pode executar, mas o que acontece quando você precisa saber o alcance da arma? Ou a capacidade de munição? Ou que tipo de munição é necessária? Você voltou ao downcasting para conseguir isso. E ter esse nível de flexibilidade tornará a UI um pouco mais difícil de implementar, pois precisará ter um padrão de estratégia semelhante para lidar com todos os recursos.Tudo isso dito, eu particularmente não concordo com as respostas para suas outras perguntas. Tendo
sword
herdamweapon
é terrível OO, ingénuo que conduz invariavelmente a métodos não-op ou controlos de tipo espalhados sobre o código.Mas, na raiz do problema, nenhuma solução está errada . Você pode usar as duas soluções para criar um jogo divertido e divertido de jogar. Cada um vem com seu próprio conjunto de compromissos, assim como qualquer solução que você escolher.
fonte
Weapon
classe com uma instância de espada e arma.WeaponBuilder
que pudesse construir espadas e armas, compondo uma arma de estratégias.Claro que é uma solução viável; é apenas uma péssima ideia.
O problema não é se você tiver essa instância única em que você recarrega sua classe base. O problema é que você também precisa colocar o "balanço", "atirar" "aparar", "bater", "polir", "desmontar", "afiar" e "substituir os pregos da extremidade pontiaguda do clube". método em sua classe base.
O ponto do LSP é que seus algoritmos de nível superior precisam funcionar e fazer sentido. Então, se eu tiver um código como este:
Agora, se isso gera uma exceção não implementada e causa uma falha no programa, é uma péssima idéia.
Se seu código estiver assim,
então seu código pode ficar cheio de propriedades muito específicas que nada têm a ver com a idéia abstrata de 'arma'.
No entanto, se você estiver implementando um jogo de tiro em primeira pessoa e todas as suas armas puderem disparar / recarregar, exceto que uma faca então (no seu contexto específico), faz muito sentido que a recarga da faca não faça nada, já que essa é a exceção e as probabilidades. de ter sua classe base cheia de propriedades específicas é baixo.
Atualização: Tente pensar no caso / termos abstratos. Por exemplo, talvez toda arma tenha uma ação de "preparação", que é uma recarga para armas e um invólucro para espadas.
fonte
Obviamente, tudo bem se você não criar uma subclasse com a intenção de substituir uma instância da classe base, mas se você criar uma subclasse usando a classe base como um repositório conveniente de funcionalidades.
Agora, se é uma boa ideia ou não, é muito discutível, mas se você nunca substituir a subclasse pela classe básica, o fato de ela não funcionar não é problema. Você pode ter problemas, mas o LSP não é o problema neste caso.
fonte
O LSP é bom porque permite que o código de chamada não se preocupe com o funcionamento da classe.
por exemplo. Posso ligar para Weapon.Attack () em todas as armas montadas no meu BattleMech e não me preocupar que algumas delas possam lançar uma exceção e travar meu jogo.
Agora, no seu caso, você deseja estender seu tipo de base com novas funcionalidades. Attack () não é um problema, porque a classe Gun pode acompanhar sua munição e parar de disparar quando acabar. Mas Reload () é algo novo e não faz parte de ser uma arma.
A solução mais fácil é fazer o downcast, acho que você não precisa se preocupar muito com o desempenho, pois não fará isso em todos os quadros.
Como alternativa, você pode reavaliar sua arquitetura e considerar que, em resumo, todas as armas são recarregáveis e algumas armas nunca precisam ser recarregadas.
Então você não está mais estendendo a classe por armas ou violando o LSP.
Mas é problemático a longo prazo, porque você deve pensar em casos mais especiais, Gun.SafteyOn (), Sword.WipeOffBlood () etc e se você os colocar todos em Arma, terá uma classe base generalizada super complicada que mantém tendo que mudar.
editar: por que o padrão de estratégia é ruim (tm)
Não é, mas considere a configuração, o desempenho e o código geral.
Eu tenho que ter alguma configuração em algum lugar que me diga que uma arma pode ser recarregada. Quando instancia uma arma, tenho que ler essa configuração e adicionar dinamicamente todos os métodos, verificar se não há nomes duplicados etc.
Quando chamo um método, tenho que percorrer essa lista de ações e fazer uma correspondência de cadeias para ver qual chamar.
Quando eu compilo o código e chamo Weapon.Do ("atack") em vez de "attack", não recebo um erro na compilação.
Pode ser uma solução adequada para alguns problemas, digamos que você tem centenas de armas, todas com diferentes combinações de métodos aleatórios, mas perde muitos dos benefícios do OO e da digitação forte. Realmente não economiza nada em downcasting
fonte
SafteyOn()
eSword
teriawipeOffBlood()
. Cada arma não tem conhecimento de outros métodos (e eles não devem ser)weapon.do("attack")
e o tipo seguroweapon.attack.perform()
podem ser exemplos do padrão de estratégia. Procurar estratégias por nome é necessário apenas ao configurar o objeto a partir de um arquivo de configuração, embora o uso de reflexão seja igualmente seguro para o tipo.