É certo violar o LSP?

10

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 classe duas classes Sworde Reloadable. Se Reloadablecontiver um específico method, chamado Reload(), eu teria que fazer o downcast para acessá-lo methode, 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 Reloadablearma pode obviamente ser recarregada, mas Swordnã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 Swordestar 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
    }
}

fonte
2
Você pode fazer 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.
usr
3
Se você deixa em reload()branco ou se standardActionsnã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.
usr
27
Escrevi uma série de artigos explorando uma variedade de problemas com várias técnicas para resolver esse problema. A conclusão: não tente capturar as regras do seu jogo no sistema de tipos do idioma . Capture as regras do jogo em objetos que representem e imponham regras no nível da lógica do jogo, não no nível do sistema de tipos . Não há razão para acreditar que qualquer sistema de tipos que você esteja usando seja sofisticado o suficiente para representar sua lógica de jogo. ericlippert.com/2015/04/27/wizards-and-warriors-part-one
Eric Lippert
2
@ EricLippert - Obrigado pelo seu link. Eu me deparei com este blog tantas vezes, mas alguns dos pontos apresentados não são bem compreendidos, mas a culpa não é sua. Estou aprendendo OOP sozinho e me deparei com os diretores do SOLID. A primeira vez que me deparei com o seu blog, eu não o entendi, mas aprendi um pouco mais e li o seu blog novamente, e lentamente comecei a entender partes do que estava sendo dito. Um dia, vou entender completamente tudo nessa série. Espero que: D
6
@SR "se não fizer nada ou der uma exceção, você está violando" - acho que você interpretou mal a mensagem desse artigo. O problema não era diretamente que setAltitude não fez nada, foi que não cumpriu a pós-condição "o pássaro será desenhado na altitude definida". Se você definir a pós-condição de "recarregar" como "se houver munição suficiente disponível, a arma poderá atacar novamente", não fazer nada será uma implementação perfeitamente válida para uma arma que não use munição.
Sebastian Redl 23/10

Respostas:

16

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 privateherança.

  • Herança usada para modelar um tipo / união de soma, por exemplo: a Baseé CaseAou CaseB. 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 a CaseC, então o código assume que a Basepode ser apenas a CaseAou CaseBestá incorreto. A Scala pode fazer isso com segurança com seu case classconceito. Em Java, isso pode ser modelado quando Baseé 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 Weaponclasse base Gune Swordherdar” 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 talvez reload()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 Gune Swordpodem ser totalmente independentes. Considerando uma Gunlata fire()e reload()uma Swordúnica pode strike(). 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 uma Attackestratégia para a qual você fornece myGun::fireou () -> 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 OnDamageReceivedou Attack. Com suas armas, nós podemos ter MeleeAttack, RangedAttacke Reloadslots. 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().

amon
fonte
Mais ou menos como o SP de uma maneira. Por que o slot precisa estar vazio? Se o dicionário não contiver a ação, simplesmente não faça nada
@SR Se um slot está vazio ou não existe realmente não importa, e depende do mecanismo usado para implementar esses slots. Eu escrevi esta resposta com as suposições de uma linguagem bastante estática em que os slots são campos de instância e sempre existem (ou seja, design de classe normal em Java). Se escolher um modelo mais dinâmico em que os slots sejam entradas em um dicionário (como usar um HashMap em Java ou um objeto Python normal), os slots não precisarão existir. Observe que abordagens mais dinâmicas oferecem muito tipo de segurança, o que geralmente não é desejável.
Am
Concordo que os objetos do mundo real não modelam bem. Se eu entendo sua postagem, você está dizendo que posso usar o Padrão de Estratégia?
2
@ SR Sim, o Padrão de Estratégia, de alguma forma, é provavelmente uma abordagem sensata. Compare também o Padrão de Objeto de Tipo relacionado: gameprogrammingpatterns.com/type-object.html
amon
3

Porque ter uma estratégia para attacknã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 swordherdam weaponé 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.

Telastyn
fonte
Eu acho que isso é perfeito. Eu posso usar o SP, mas eles são compensadores, só preciso estar ciente deles. Veja minha edição, pelo que tenho em mente.
1
Fwiw: uma espada tem munição infinita: você pode continuar usando-a sem ler para sempre; recarregar não faz nada porque você tem um uso infinito para começar; um alcance de um / corpo a corpo: é uma arma corpo a corpo. Não é impossível pensar em todas as estatísticas / ações de uma maneira que funcione tanto para o corpo a corpo quanto para o alcance. Ainda assim, à medida que envelheci, uso cada vez menos herança em favor de interfaces, competição e seja qual for o nome para usar uma única Weaponclasse com uma instância de espada e arma.
quer
Fwiw nas espadas de Destiny 2 usa munição por algum motivo!
@ CAD97 - Esse é o tipo de pensamento que eu já vi sobre esse problema. Tenha espada com munição infinita, então não recarregue. Isso apenas empurra o problema ou o oculta. E se eu introduzir uma granada, e então? Granadas não possuem munição ou tiro e não devem estar cientes de tais métodos.
1
Estou com CAD97 nisso. E criaria um WeaponBuilderque pudesse construir espadas e armas, compondo uma arma de estratégias.
precisa
3

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:

if (isEquipped(weapon)) {
   reload();
}

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,

if (canReload(weapon)) {
   reload();
}
else if (canSharpen(weapon)) {
  sharpen();
}
else if (canPollish(weapon)) {
  polish();
}

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.

Batavia
fonte
Digamos que eu tenha um dicionário interno de armas que detenha as ações das armas e, quando o usuário passa em "Recarregar", ele verifica o dicionário, por exemplo, gunActions.containsKey (action), selecione o objeto associado a ele e faça isto. Ao invés de uma classe arma com múltiplos if
Veja a edição acima. Isto é o que eu tinha em mente quando se utiliza o SP
0

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.

gnasher729
fonte
0

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

Ewan
fonte
Eu acho que o SP pode lidar com tudo isso (veja a edição acima), a arma teria SafteyOn()e Swordteria wipeOffBlood(). Cada arma não tem conhecimento de outros métodos (e eles não devem ser)
o SP é bom, mas é equivalente a downcasting sem segurança de tipo. Eu acho que eu estava respondendo meio uma pergunta diferente, deixe-me atualizar
Ewan
2
Por si só, o padrão de estratégia não implica a pesquisa dinâmica de uma estratégia em uma lista ou dicionário. Ou seja, ambos weapon.do("attack")e o tipo seguro weapon.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.
amon
que não vai funcionar nesta situação, pois há duas ações separadas atacar e recarga, o que você precisa vincular a alguma entrada do usuário
Ewan