Ouvi dizer que o Princípio de Substituição de Liskov (LSP) é um princípio fundamental do design orientado a objetos. O que é e quais são alguns exemplos de seu uso?
908
Ouvi dizer que o Princípio de Substituição de Liskov (LSP) é um princípio fundamental do design orientado a objetos. O que é e quais são alguns exemplos de seu uso?
Respostas:
Um ótimo exemplo de ilustração do LSP (dado pelo tio Bob em um podcast que ouvi recentemente) foi como às vezes algo que soa bem na linguagem natural não funciona muito bem no código.
Em matemática, a
Square
é aRectangle
. Na verdade, é uma especialização de um retângulo. O "é um" faz com que você queira modelar isso com herança. No entanto, se no código que vocêSquare
derivouRectangle
, aSquare
deve ser utilizável em qualquer lugar que você espera aRectangle
. Isso cria um comportamento estranho.Imagine que você tinha
SetWidth
eSetHeight
métodos em suaRectangle
classe base; isso parece perfeitamente lógico. No entanto, se suaRectangle
referência apontou para aSquare
, entãoSetWidth
eSetHeight
não faz sentido, pois definir uma mudaria a outra para corresponder a ela. Nesse caso,Square
falha no teste de substituição de LiskovRectangle
e a abstração deSquare
herdarRectangle
é ruim.Vocês devem conferir os outros pôsteres motivacionais de inestimáveis princípios do SOLID .
fonte
Square.setWidth(int width)
fosse implementado assimthis.width = width; this.height = width;
:? Nesse caso, é garantido que a largura seja igual à altura.O princípio da substituição de Liskov (LSP, lsp) é um conceito na programação orientada a objetos que declara:
No fundo, o LSP trata de interfaces e contratos, bem como de como decidir quando estender uma classe versus usar outra estratégia, como a composição, para atingir seu objetivo.
A maneira eficaz máximo que eu já vi para ilustrar este ponto estava no Head First OOA & D . Eles apresentam um cenário em que você é um desenvolvedor de um projeto para criar uma estrutura para jogos de estratégia.
Eles apresentam uma classe que representa um quadro semelhante a este:
Todos os métodos usam as coordenadas X e Y como parâmetros para localizar a posição do bloco na matriz bidimensional de
Tiles
. Isso permitirá que um desenvolvedor de jogos gerencie unidades no tabuleiro durante o decorrer do jogo.O livro continua alterando os requisitos para dizer que o trabalho da estrutura do jogo também deve suportar tabuleiros de jogos em 3D para acomodar jogos com vôo. Portanto,
ThreeDBoard
é introduzida uma classe que se estendeBoard
.À primeira vista, isso parece ser uma boa decisão.
Board
fornece tanto aHeight
eWidth
propriedades eThreeDBoard
fornece o eixo Z.O ponto em que ocorre é quando você olha para todos os outros membros herdados
Board
. Os métodos para aAddUnit
,GetTile
,GetUnits
e assim por diante, todos levar ambos os parâmetros X e Y naBoard
classe mas oThreeDBoard
precisa de um parâmetro Z, bem.Portanto, você deve implementar esses métodos novamente com um parâmetro Z O parâmetro Z não tem contexto para a
Board
classe e os métodos herdados daBoard
classe perdem seu significado. Uma unidade de código que tenta usar aThreeDBoard
classe como sua classe baseBoard
seria muito azarada.Talvez devêssemos encontrar outra abordagem. Em vez de estender
Board
,ThreeDBoard
deve ser composto deBoard
objetos. UmBoard
objeto por unidade do eixo Z.Isso nos permite usar bons princípios orientados a objetos, como encapsulamento e reutilização, e não viola o LSP.
fonte
vamos fazer um exemplo simples em Java:
Mau exemplo
O pato pode voar por causa de um pássaro, mas e quanto a isso:
Avestruz é um pássaro, mas não pode voar, a classe Ostrich é um subtipo da classe Bird, mas não pode usar o método fly, isso significa que estamos quebrando o princípio do LSP.
Bom exemplo
fonte
Bird bird
. Você tem que lançar o objeto no FlyingBirds para usar o fly, o que não é legal, certo?Bird bird
, significa que não pode usá-lofly()
. É isso aí. Passar aDuck
não altera esse fato. Se o cliente tiverFlyingBirds bird
, mesmo que seja aprovadoDuck
, sempre funcionará da mesma maneira.O LSP diz respeito aos invariantes.
O exemplo clássico é dado pela seguinte declaração de pseudo-código (implementações omitidas):
Agora temos um problema, embora a interface corresponda. O motivo é que violamos os invariantes decorrentes da definição matemática de quadrados e retângulos. Da maneira que getters e setters funcionam, a
Rectangle
deve satisfazer o seguinte invariante:No entanto, esse invariante deve ser violado por uma implementação correta de
Square
, portanto, não é um substituto válido deRectangle
.fonte
Robert Martin tem um excelente artigo sobre o Princípio da Substituição de Liskov . Ele discute maneiras sutis e não tão sutis pelas quais o princípio pode ser violado.
Algumas partes relevantes do artigo (observe que o segundo exemplo está fortemente condensado):
fonte
Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
se uma pré-condição de classe filho for mais forte que uma pré-condição de classe pai, você não poderá substituir um filho por um pai sem violar a pré-condição. Daí o LSP.O LSP é necessário quando algum código pensa que está chamando os métodos de um tipo
T
e pode, sem saber, chamar os métodos de um tipoS
, ondeS extends T
(ou sejaS
, herda, deriva ou é um subtipo do supertipoT
).Por exemplo, isso ocorre onde uma função com um parâmetro de entrada do tipo
T
é chamada (ou seja, invocada) com um valor de argumento do tipoS
. Ou, onde um identificador de tipoT
, recebe um valor do tipoS
.O LSP exige que as expectativas (ou seja, invariantes) de métodos do tipo
T
(por exemploRectangle
), não sejam violadas quando os métodos do tipoS
(por exemploSquare
) são chamados.Mesmo um tipo com campos imutáveis ainda possui invariantes, por exemplo, os setters retangulares imutáveis esperam que as dimensões sejam modificadas independentemente, mas os setters quadrados imutáveis violam essa expectativa.
O LSP exige que cada método do subtipo
S
tenha parâmetros de entrada contravariáveis e uma saída covariante.Contravariante significa que a variação é contrária à direção da herança, ou seja, o tipo
Si
, de cada parâmetro de entrada de cada método do subtipoS
, deve ser o mesmo ou um supertipo do tipoTi
do parâmetro de entrada correspondente do método correspondente do supertipoT
.Covariância significa que a variação está na mesma direção da herança, ou seja, o tipo
So
da saída de cada método do subtipoS
, deve ser o mesmo ou um subtipo do tipoTo
da saída correspondente do método correspondente do supertipoT
.Isso ocorre porque se o chamador pensa que tem um tipo
T
, pensa que está chamando um método deT
, ele fornece argumentos do tipoTi
e atribui a saída ao tipoTo
. Quando, na verdade, ele está chamando o método correspondente deS
, cadaTi
argumento de entrada é atribuído a umSi
parâmetro de entrada e aSo
saída é atribuída ao tipoTo
. Assim, seSi
não houvesse contravençãoTi
, então um subtipoXi
- que não seria um subtipo de -Si
poderia ser atribuídoTi
.Além disso, para idiomas (por exemplo, Scala ou Ceilão) que possuem anotações de variação do local da definição nos parâmetros do polimorfismo de tipo (ou seja, genéricos), a co-ou a contra-direção da anotação de variação para cada parâmetro do tipo
T
deve ser oposta ou na mesma direção respectivamente para cada parâmetro de entrada ou saída (de todo método deT
) que tenha o tipo do parâmetro de tipo.Além disso, para cada parâmetro de entrada ou saída que possui um tipo de função, a direção de variação necessária é invertida. Esta regra é aplicada recursivamente.
A subtipagem é apropriada onde os invariantes podem ser enumerados.
Há muita pesquisa em andamento sobre como modelar invariantes, para que eles sejam impostos pelo compilador.
Typestate (na página 3) declara e aplica os invariantes de estado ortogonais ao tipo. Como alternativa, os invariantes podem ser aplicados convertendo asserções em tipos . Por exemplo, para afirmar que um arquivo está aberto antes de fechá-lo, File.open () pode retornar um tipo OpenFile, que contém um método close () que não está disponível em File. Uma API do jogo da velha pode ser outro exemplo de emprego de digitação para impor invariantes em tempo de compilação. O sistema de tipos pode até ser completo para Turing, por exemplo, Scala . Provedores de linguagens e teoremas de tipo dependente formalizam os modelos de digitação de ordem superior.
Devido à necessidade de a semântica abstrair sobre a extensão , espero que o emprego da digitação para modelar invariantes, ou seja, semântica denotacional de ordem superior unificada, seja superior ao Typestate. «Extensão», a composição permutada sem limites do desenvolvimento modular não coordenado. Porque me parece ser a antítese da unificação e, portanto, os graus de liberdade, ter dois modelos mutuamente dependentes (por exemplo, tipos e Typestate) para expressar a semântica compartilhada, que não pode ser unificada entre si para composição extensível . Por exemplo, a extensão semelhante ao Problema de Expressão foi unificada nos domínios de subtipagem, sobrecarga de função e digitação paramétrica.
Minha posição teórica é que, para que o conhecimento exista (consulte a seção “A centralização é cega e imprópria”), nunca haverá um modelo geral que possa impor 100% de cobertura de todos os invariantes possíveis em uma linguagem de computador completa em Turing. Para que o conhecimento exista, existem muitas possibilidades inesperadas, ou seja, desordem e entropia devem sempre estar aumentando. Essa é a força entrópica. Para provar todos os cálculos possíveis de uma extensão em potencial, é calcular a priori toda extensão possível.
É por isso que o Teorema Halting existe, ou seja, é indecidível se todos os programas possíveis em uma linguagem de programação completa de Turing terminam. Pode-se provar que algum programa específico termina (um em que todas as possibilidades foram definidas e computadas). Mas é impossível provar que toda a extensão possível desse programa termina, a menos que as possibilidades de extensão desse programa não sejam completas de Turing (por exemplo, via digitação dependente). Como o requisito fundamental para a completitude de Turing é a recursão ilimitada , é intuitivo entender como os teoremas da incompletude de Gödel e o paradoxo de Russell se aplicam à extensão.
Uma interpretação desses teoremas os incorpora em um entendimento conceitual generalizado da força entrópica:
fonte
Vejo retângulos e quadrados em todas as respostas e como violar o LSP.
Gostaria de mostrar como o LSP pode ser conformado com um exemplo do mundo real:
Esse design está em conformidade com o LSP porque o comportamento permanece inalterado, independentemente da implementação que escolhemos usar.
E sim, você pode violar o LSP nesta configuração, fazendo uma alteração simples como esta:
Agora, os subtipos não podem ser usados da mesma maneira, pois não produzem mais o mesmo resultado.
fonte
Database::selectQuery
para suportar apenas o subconjunto de SQL suportado por todos os mecanismos de banco de dados. Isso não é prático ... Dito isso, o exemplo ainda é mais fácil de entender do que a maioria dos outros usados aqui.Há uma lista de verificação para determinar se você está violando Liskov ou não.
Lista de controle:
Restrição do histórico : ao substituir um método, você não tem permissão para modificar uma propriedade não modificável na classe base. Dê uma olhada nesses códigos e você poderá ver que Name está definido como não modificável (conjunto privado), mas o SubType introduz um novo método que permite modificá-lo (através da reflexão):
Existem outros 2 itens: Contravariância de argumentos de método e Covariância de tipos de retorno . Mas não é possível em C # (eu sou desenvolvedor de C #), então não me importo com eles.
Referência:
fonte
O LSP é uma regra sobre o contrato das classes: se uma classe base satisfaz um contrato, as classes derivadas do LSP também devem satisfazer esse contrato.
Em pseudo-python
satisfaz o LSP se toda vez que você chama Foo em um objeto Derivado, ele fornece exatamente os mesmos resultados que chamar Foo em um objeto Base, desde que arg seja o mesmo.
fonte
2 + "2"
). Talvez você confunda "fortemente tipado" com "estaticamente digitado"?Para encurtar a história, vamos deixar retângulos retângulos e quadrados, exemplo prático ao estender uma classe pai, você precisa PRESERVAR a API pai exata ou EXTENDÊ-LO.
Digamos que você tenha um ItemsRepository básico .
E uma subclasse estendendo-a:
Em seguida, você pode ter um cliente trabalhando com a API Base ItemsRepository e confiando nela.
O LSP é interrompido quando a substituição da classe pai por uma subclasse quebra o contrato da API .
Você pode aprender mais sobre como escrever software sustentável em meu curso: https://www.udemy.com/enterprise-php/
fonte
Quando li pela primeira vez sobre o LSP, supus que isso fosse feito em um sentido muito estrito, essencialmente igualando-o à interface de implementação e à conversão de tipo seguro. O que significa que o LSP é garantido ou não pelo próprio idioma. Por exemplo, nesse sentido estrito, o ThreeDBoard certamente é substituível pelo Board, no que diz respeito ao compilador.
Depois de ler mais sobre o conceito, descobri que o LSP geralmente é interpretado de maneira mais ampla que isso.
Em resumo, o que significa para o código do cliente "saber" que o objeto atrás do ponteiro é de um tipo derivado, e não o tipo de ponteiro, não está restrito à segurança de tipo. A adesão ao LSP também pode ser testada através da investigação do comportamento real dos objetos. Ou seja, examinar o impacto dos argumentos de estado e método de um objeto nos resultados das chamadas de método ou os tipos de exceções geradas pelo objeto.
Voltando ao exemplo novamente, em teoria os métodos da placa podem funcionar perfeitamente no ThreeDBoard. Na prática, porém, será muito difícil evitar diferenças no comportamento que o cliente pode não lidar adequadamente, sem prejudicar a funcionalidade que o ThreeDBoard pretende adicionar.
Com esse conhecimento em mãos, avaliar a aderência ao LSP pode ser uma ótima ferramenta para determinar quando a composição é o mecanismo mais apropriado para estender a funcionalidade existente, em vez de herança.
fonte
Acho que todo mundo meio que abordou o que é LSP tecnicamente: você basicamente quer abstrair dos detalhes do subtipo e usar supertipos com segurança.
Então Liskov tem três regras subjacentes:
Regra de assinatura: deve haver uma implementação válida de todas as operações do supertipo no subtipo sintaticamente. Algo que um compilador poderá verificar por você. Há uma pequena regra sobre lançar menos exceções e ser pelo menos tão acessível quanto os métodos de supertipo.
Métodos Regra: A implementação dessas operações é semanticamente correta.
Regra de propriedades: Isso vai além das chamadas de função individuais.
Todas essas propriedades precisam ser preservadas e a funcionalidade extra de subtipo não deve violar as propriedades de supertipo.
Se essas três coisas foram resolvidas, você abstraiu o material subjacente e está escrevendo código fracamente acoplado.
Fonte: Desenvolvimento de Programa em Java - Barbara Liskov
fonte
Um exemplo importante do uso do LSP está nos testes de software .
Se eu tiver uma classe A que seja uma subclasse de B compatível com LSP, poderei reutilizar o conjunto de testes de B para testar A.
Para testar completamente a subclasse A, provavelmente preciso adicionar mais alguns casos de teste, mas no mínimo posso reutilizar todos os casos de teste da superclasse B.
Uma maneira de perceber isso é criando o que McGregor chama de "Hierarquia paralela para teste": Minha
ATest
classe herdaráBTest
. É necessária alguma forma de injeção para garantir que o caso de teste funcione com objetos do tipo A, e não do tipo B (um padrão simples de método de modelo funcionará).Observe que a reutilização do conjunto de super testes para todas as implementações de subclasses é de fato uma maneira de testar se essas implementações de subclasses são compatíveis com LSP. Assim, também se pode argumentar que se deve executar o conjunto de testes da superclasse no contexto de qualquer subclasse.
Consulte também a resposta à pergunta Stackoverflow " Posso implementar uma série de testes reutilizáveis para testar a implementação de uma interface? "
fonte
Vamos ilustrar em Java:
Não há nenhum problema aqui, certo? Um carro é definitivamente um dispositivo de transporte, e aqui podemos ver que ele substitui o método startEngine () de sua superclasse.
Vamos adicionar outro dispositivo de transporte:
Tudo não está indo como planejado agora! Sim, uma bicicleta é um dispositivo de transporte, no entanto, não possui um mecanismo e, portanto, o método startEngine () não pode ser implementado.
A solução para esses problemas é uma hierarquia de herança correta e, no nosso caso, resolveríamos o problema diferenciando classes de dispositivos de transporte com e sem motores. Mesmo que uma bicicleta seja um dispositivo de transporte, ela não tem um motor. Neste exemplo, nossa definição de dispositivo de transporte está errada. Não deve ter um motor.
Podemos refatorar nossa classe TransportationDevice da seguinte maneira:
Agora podemos estender o TransportationDevice para dispositivos não motorizados.
E estenda o TransportationDevice para dispositivos motorizados. Aqui é mais apropriado adicionar o objeto Engine.
Assim, nossa classe Car se torna mais especializada, ao mesmo tempo em que adere ao Princípio de Substituição de Liskov.
E nossa classe de bicicleta também está em conformidade com o Princípio de Substituição de Liskov.
fonte
Essa formulação do LSP é muito forte:
O que basicamente significa que S é outra implementação completamente encapsulada da mesma coisa que T. E eu poderia ser ousado e decidir que o desempenho faz parte do comportamento de P ...
Portanto, basicamente, qualquer uso de ligação tardia viola o LSP. O objetivo principal do OO é obter um comportamento diferente quando substituímos um objeto de um tipo por outro de outro!
A formulação citada pela wikipedia é melhor, pois a propriedade depende do contexto e não inclui necessariamente todo o comportamento do programa.
fonte
Em uma frase muito simples, podemos dizer:
A classe filho não deve violar suas características de classe base. Deve ser capaz com isso. Podemos dizer que é o mesmo que subtipagem.
fonte
Exemplo:
Abaixo está o exemplo clássico para o qual o Princípio de Substituição de Liskov é violado. No exemplo, 2 classes são usadas: Retângulo e Quadrado. Vamos supor que o objeto Rectangle seja usado em algum lugar do aplicativo. Estendemos o aplicativo e adicionamos a classe Square. A classe square é retornada por um padrão de fábrica, com base em algumas condições e não sabemos exatamente o tipo de objeto que será retornado. Mas sabemos que é um retângulo. Obtemos o objeto retângulo, definimos a largura para 5 e a altura para 10 e obtemos a área. Para um retângulo com largura 5 e altura 10, a área deve ser 50. Em vez disso, o resultado será 100
Veja também: Abrir Fechar Princípio
Alguns conceitos semelhantes para melhor estrutura: Convenção sobre configuração
fonte
O princípio da substituição de Liskov
fonte
Algum adendo:
Gostaria de saber por que ninguém escreveu sobre o Invariant, pré-condições e pós-condições da classe base que devem ser obedecidas pelas classes derivadas. Para que uma classe D derivada seja completamente sustentável pela classe Base B, a classe D deve obedecer a certas condições:
Portanto, o derivado deve estar ciente das três condições acima impostas pela classe base. Portanto, as regras de subtipagem são pré-decididas. O que significa que o relacionamento 'IS A' deve ser obedecido somente quando certas regras forem obedecidas pelo subtipo. Essas regras, na forma de invariantes, pré-condições e pós-condição, devem ser decididas por um ' contrato de projeto ' formal .
Outras discussões sobre isso estão disponíveis no meu blog: Princípio de substituição de Liskov
fonte
O LSP, em termos simples, afirma que objetos da mesma superclasse devem poder ser trocados entre si sem quebrar nada.
Por exemplo, se tivermos
Cat
umaDog
classe e uma derivada de umaAnimal
classe, qualquer função que utilize a classe Animal deverá poder usarCat
ouDog
comportar-se normalmente.fonte
A implementação do ThreeDBoard em termos de um conjunto de placas seria tão útil?
Talvez você queira tratar fatias do ThreeDBoard em vários planos como uma placa. Nesse caso, convém abstrair uma interface (ou classe abstrata) para o Board para permitir várias implementações.
Em termos de interface externa, você pode considerar uma interface de placa para o TwoDBoard e o ThreeDBoard (embora nenhum dos métodos acima se encaixe).
fonte
Um quadrado é um retângulo em que a largura é igual à altura. Se o quadrado definir dois tamanhos diferentes para a largura e a altura, ele violará o invariante do quadrado. Isso é contornado com a introdução de efeitos colaterais. Mas se o retângulo tiver um setSize (altura, largura) com a pré-condição 0 <altura e 0 <largura. O método do subtipo derivado requer height == width; uma pré-condição mais forte (e que viola a lsp). Isso mostra que, embora quadrado seja um retângulo, ele não é um subtipo válido porque a pré-condição é reforçada. A solução alternativa (geralmente uma coisa ruim) causa um efeito colateral e isso enfraquece a condição pós (que viola lsp). setWidth na base tem a condição de postagem 0 <width. O derivado o enfraquece com a altura == largura.
Portanto, um quadrado redimensionável não é um retângulo redimensionável.
fonte
Este princípio foi introduzido por Barbara Liskov em 1987 e estende o Princípio Aberto-Fechado, concentrando-se no comportamento de uma superclasse e seus subtipos.
Sua importância se torna óbvia quando consideramos as consequências de violá-la. Considere um aplicativo que usa a seguinte classe.
Imagine que um dia o cliente exija a capacidade de manipular quadrados além de retângulos. Como um quadrado é um retângulo, a classe quadrada deve ser derivada da classe Rectangle.
No entanto, ao fazer isso, encontraremos dois problemas:
Um quadrado não precisa das variáveis de altura e largura herdadas do retângulo e isso pode gerar um desperdício significativo de memória se tivermos que criar centenas de milhares de objetos quadrados. As propriedades do setter de largura e altura herdadas do retângulo são inadequadas para um quadrado, pois a largura e a altura de um quadrado são idênticas. Para definir altura e largura para o mesmo valor, podemos criar duas novas propriedades da seguinte maneira:
Agora, quando alguém definir a largura de um objeto quadrado, sua altura mudará de acordo e vice-versa.
Vamos seguir em frente e considerar esta outra função:
Se passarmos uma referência a um objeto quadrado para essa função, violaremos o LSP porque a função não funciona para derivadas de seus argumentos. As propriedades width e height não são polimórficas porque não são declaradas virtuais em retângulo (o objeto quadrado será corrompido porque a altura não será alterada).
No entanto, ao declarar as propriedades do setter como virtuais, enfrentaremos outra violação, o OCP. De fato, a criação de um quadrado de classe derivado está causando alterações no retângulo da classe base.
fonte
A explicação mais clara para o LSP que encontrei até agora foi "O Princípio de Substituição de Liskov diz que o objeto de uma classe derivada deve ser capaz de substituir um objeto da classe base sem causar erros no sistema ou modificar o comportamento da classe base " daqui . O artigo fornece um exemplo de código para violar o LSP e corrigi-lo.
fonte
Digamos que usamos um retângulo em nosso código
Em nossa aula de geometria, aprendemos que um quadrado é um tipo especial de retângulo, porque sua largura tem o mesmo comprimento que sua altura. Vamos fazer uma
Square
aula também com base nesta informação:Se substituirmos
Rectangle
porSquare
no nosso primeiro código, ele será quebrado:Isso ocorre porque o
Square
tem um novo pré-requisito que não tínhamos naRectangle
classe:width == height
. De acordo com o LSP, asRectangle
instâncias devem ser substituíveis pelasRectangle
instâncias da subclasse. Isso ocorre porque essas instâncias passam na verificação de tipo paraRectangle
instâncias e, portanto, causam erros inesperados no seu código.Este foi um exemplo para a parte "pré-condições não podem ser reforçadas em um subtipo" no artigo da wiki . Então, para resumir, violar o LSP provavelmente causará erros no seu código em algum momento.
fonte
O LSP diz que "Os objetos devem ser substituíveis por seus subtipos". Por outro lado, esse princípio aponta para
e o exemplo a seguir ajuda a entender melhor o LSP.
Sem LSP:
Fixação por LSP:
fonte
Convido você a ler o artigo: Violando o princípio de substituição de Liskov (LSP) .
Você pode encontrar uma explicação sobre o Princípio da Substituição de Liskov, dicas gerais para adivinhar se você já o violou e um exemplo de abordagem que o ajudará a tornar sua hierarquia de classes mais segura.
fonte
O PRINCÍPIO DE SUBSTITUIÇÃO DE LISKOV (do livro de Mark Seemann) afirma que deveríamos ser capazes de substituir uma implementação de uma interface por outra sem quebrar o cliente ou a implementação.É esse princípio que permite atender aos requisitos que ocorrerem no futuro, mesmo que possamos ' não os prevejo hoje.
Se desconectarmos o computador da parede (Implementação), nem a tomada (Interface) nem o computador (Cliente) quebram (na verdade, se for um laptop, ele pode funcionar com as baterias por um período de tempo) . Com o software, no entanto, um cliente geralmente espera que um serviço esteja disponível. Se o serviço foi removido, obtemos uma NullReferenceException. Para lidar com esse tipo de situação, podemos criar uma implementação de uma interface que não faz "nada". Este é um padrão de design conhecido como Objeto Nulo, [4] e corresponde aproximadamente a desconectar o computador da parede. Como estamos usando acoplamentos soltos, podemos substituir uma implementação real por algo que não faz nada sem causar problemas.
fonte
O Princípio de Substituição da Likov afirma que, se um módulo de programa estiver usando uma classe Base, a referência à classe Base poderá ser substituída por uma classe Derived sem afetar a funcionalidade do módulo de programa.
Intenção - Os tipos derivados devem ser completamente substitutos para seus tipos de base.
Exemplo - Tipos de retorno de co-variante em java.
fonte
Aqui está um trecho deste post que esclarece bem as coisas:
[..] para compreender alguns princípios, é importante perceber quando isso foi violado. É isso que vou fazer agora.
O que significa a violação deste princípio? Isso implica que um objeto não cumpre o contrato imposto por uma abstração expressa com uma interface. Em outras palavras, significa que você identificou suas abstrações incorretas.
Considere o seguinte exemplo:
Isso é uma violação do LSP? Sim. Isso ocorre porque o contrato da conta nos diz que uma conta seria retirada, mas esse nem sempre é o caso. Então, o que devo fazer para corrigi-lo? Acabei de modificar o contrato:
Voilà, agora o contrato está satisfeito.
Essa violação sutil geralmente impõe ao cliente a capacidade de diferenciar objetos concretos empregados. Por exemplo, dado o primeiro contrato da conta, ele pode se parecer com o seguinte:
E isso viola automaticamente o princípio de aberto / fechado [isto é, para requisito de retirada de dinheiro. Porque você nunca sabe o que acontece se um objeto que viola o contrato não tiver dinheiro suficiente. Provavelmente não retorna nada, provavelmente uma exceção será lançada. Então você tem que verificar se
hasEnoughMoney()
- o que não faz parte de uma interface. Portanto, essa verificação forçada dependente da classe de concreto é uma violação do OCP].Este ponto também aborda um equívoco que encontro com frequência sobre a violação do LSP. Ele diz que "se o comportamento de um pai mudou em um filho, isso viola o LSP". No entanto, isso não acontece - desde que uma criança não viole o contrato de seus pais.
fonte