Eu estava seguindo esta questão altamente votada sobre possível violação do princípio da substituição de Liskov. Sei qual é o princípio da Substituição de Liskov, mas o que ainda não está claro em minha mente é o que pode dar errado se eu, como desenvolvedor, não pensar sobre o princípio ao escrever código orientado a objetos.
27
Respostas:
Eu acho que está muito bem afirmado nessa questão, que é uma das razões pelas quais foi votado com tanta ênfase.
Imagine se você quiser:
Nesse método, ocasionalmente a chamada .Close () é acionada. Portanto, agora, com base na implementação concreta de um tipo derivado, é necessário alterar a maneira como esse método se comporta da maneira como esse método seria escrito se a Tarefa não tivesse subtipos que pudessem ser entregue a esse método.
Devido a violações de substituição liskov, o código que usa seu tipo precisará ter conhecimento explícito do funcionamento interno dos tipos derivados para tratá-los de maneira diferente. Isso une fortemente o código e geralmente dificulta o uso consistente da implementação.
fonte
Se você não cumprir o contrato que foi definido na classe base, as coisas podem falhar silenciosamente quando você obtém resultados que estão fora.
LSP nos estados da wikipedia
Se alguma dessas opções não for válida, o chamador poderá obter um resultado que não espera.
fonte
Considere um caso clássico dos anais das perguntas da entrevista: você derivou o Circle da Ellipse. Por quê? Porque um círculo é uma elipse, é claro!
Exceto ... ellipse tem duas funções:
Claramente, eles devem ser redefinidos para o Círculo, porque um Círculo tem um raio uniforme. Você tem duas possibilidades:
A maioria dos idiomas OO não suporta o segundo, e por uma boa razão: seria surpreendente descobrir que seu círculo não é mais um círculo. Portanto, a primeira opção é a melhor. Mas considere a seguinte função:
Imagine que alguma função_ chame e.set_alpha_radius. Mas como e era realmente um círculo, surpreendentemente também possui seu raio beta.
E aqui reside o princípio da substituição: uma subclasse deve ser substituída por uma superclasse. Caso contrário, coisas surpreendentes acontecem.
fonte
Nas palavras dos leigos:
Seu código terá muitas cláusulas CASE / switch por todo o lado.
Cada uma dessas cláusulas CASE / switch precisará de novos casos adicionados de tempos em tempos, o que significa que a base de código não é tão escalável e sustentável quanto deveria.
O LSP permite que o código funcione mais como hardware:
Você não precisa modificar o seu iPod porque comprou um novo par de alto-falantes externos, já que os antigos e os novos externos respeitam a mesma interface, eles são intercambiáveis sem que o iPod perca a funcionalidade desejada.
fonte
typeof(someObject)
para decidir o que "está autorizado a fazer", com certeza, mas isso é outro antipadrão.para dar um exemplo da vida real com o UndoManager do java
herda de
AbstractUndoableEdit
cujo contrato especifica que possui 2 estados (desfeitos e refeitos) e pode alternar entre eles com chamadas únicas paraundo()
eredo()
no entanto, o UndoManager possui mais estados e atua como um buffer de desfazer (cada chamada
undo
desfaz algumas, mas não todas as edições, enfraquecendo a pós-condição)isso leva à situação hipotética em que você adiciona um UndoManager a um CompoundEdit antes de chamar e
end()
depois desfazer o CompoundEdit fará com que ele chameundo()
cada edição depois de deixar suas edições parcialmente desfeitasEu rolei o meu próprio
UndoManager
para evitar isso (eu provavelmente deveria renomeá-lo paraUndoBuffer
embora)fonte
Exemplo: você está trabalhando com uma estrutura de interface do usuário e cria seu próprio controle de interface do usuário personalizado subclassificando a
Control
classe base. AControl
classe base define um métodogetSubControls()
que deve retornar uma coleção de controles aninhados (se houver). Mas você substitui o método para realmente retornar uma lista de datas de nascimento dos presidentes dos Estados Unidos.Então, o que pode dar errado com isso? É óbvio que a renderização do controle falhará, pois você não retorna uma lista de controles conforme o esperado. Provavelmente a interface do usuário falhará. Você está violando o contrato ao qual as subclasses de Controle devem aderir.
fonte
Você também pode vê-lo do ponto de vista da modelagem. Quando você diz que uma instância da classe
A
também é uma instância da classe,B
implica que "o comportamento observável de uma instância da classeA
também pode ser classificado como comportamento observável de uma instância da classeB
" (Isso só é possível se a classeB
for menos específica do que classeA
.)Portanto, violar o LSP significa que há alguma contradição no seu design: você está definindo algumas categorias para seus objetos e depois não as respeita em sua implementação, algo deve estar errado.
Como fazer uma caixa com uma etiqueta: "Esta caixa contém apenas bolas azuis" e depois jogar uma bola vermelha nela. Qual é o uso de uma etiqueta desse tipo se ela mostra informações incorretas?
fonte
Herdei recentemente uma base de código que contém alguns dos principais violadores de Liskov. Em aulas importantes. Isso me causou enormes quantidades de dor. Deixe-me explicar o porquê.
Eu tenho
Class A
, que derivaClass B
.Class A
eClass B
compartilhe várias propriedades queClass A
substituem sua própria implementação. Definir ou obter umaClass A
propriedade tem um efeito diferente de definir ou obter exatamente a mesma propriedadeClass B
.Deixando de lado o fato de que essa é uma maneira absolutamente terrível de fazer a tradução no .NET, existem vários outros problemas com esse código.
Nesse caso,
Name
é usado como um índice e uma variável de controle de fluxo em vários locais. As classes acima estão espalhadas por toda a base de código, tanto na forma bruta quanto na derivada. Violar o princípio de substituição de Liskov neste caso significa que eu preciso conhecer o contexto de cada chamada para cada uma das funções que levam a classe base.O código usa objetos de ambos
Class A
eClass B
, portanto, não posso simplesmente tornarClass A
abstrato para forçar as pessoas a usarClass B
.Existem algumas funções utilitárias muito úteis que operam
Class A
e outras funções utilitárias muito úteis que operamClass B
. Idealmente, eu gostaria de ser capaz de usar qualquer função de utilitário que pode operar emClass A
onClass B
. Muitas das funções que aceitam umClass B
poderiam facilmente aceitar um,Class A
se não fosse pela violação do LSP.O pior de tudo é que esse caso em particular é realmente difícil de refatorar, pois todo o aplicativo depende dessas duas classes, opera nas duas classes o tempo todo e quebraria de centenas de maneiras se eu mudar isso (o que vou fazer) de qualquer forma).
O que terei que fazer para corrigir isso é criar uma
NameTranslated
propriedade, que será aClass B
versão daName
propriedade e muito, muito cuidadosamente, alterar todas as referências àName
propriedade derivada para usar minha novaNameTranslated
propriedade. No entanto, ao errar uma dessas referências, o aplicativo inteiro pode explodir.Dado que a base de código não possui testes de unidade, é quase o cenário mais perigoso que um desenvolvedor pode enfrentar. Se eu não alterar a violação, tenho que gastar uma quantidade enorme de energia mental acompanhando que tipo de objeto está sendo operado em cada método e, se eu corrigir a violação, poderia fazer o produto inteiro explodir em um momento inoportuno.
fonte
BaseName
eTranslatedName
para o acesso tanto o estilo de classe-AName
e o significado de classe B? Qualquer tentativa de acessarName
uma variável do tipoB
seria rejeitada com um erro do compilador, para garantir que todas as referências fossem convertidas em um dos outros formulários.Se você quiser sentir o problema de violar o LSP, pense no que acontece se você tiver apenas .dll / .jar da classe base (sem código fonte) e precisar criar uma nova classe derivada. Você nunca pode concluir esta tarefa.
fonte