Qual é um exemplo do Princípio de Substituição de Liskov?

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?

Eu não
fonte
Mais exemplos de LSP aderência e violação aqui
StuartLC
1
Esta pergunta tem infinitamente boas respostas e, portanto, é muito ampla .
Raedwald

Respostas:

892

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é a Rectangle. 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ê Squarederivou Rectangle, a Squaredeve ser utilizável em qualquer lugar que você espera a Rectangle. Isso cria um comportamento estranho.

Imagine que você tinha SetWidthe SetHeightmétodos em sua Rectangleclasse base; isso parece perfeitamente lógico. No entanto, se sua Rectanglereferência apontou para a Square, então SetWidthe SetHeightnão faz sentido, pois definir uma mudaria a outra para corresponder a ela. Nesse caso, Squarefalha no teste de substituição de Liskov Rectanglee a abstração de Squareherdar Rectangleé ruim.

insira a descrição da imagem aqui

Vocês devem conferir os outros pôsteres motivacionais de inestimáveis princípios do SOLID .

m-sharp
fonte
19
@ m-sharp E se for um retângulo imutável, que, em vez de SetWidth e SetHeight, tenhamos os métodos GetWidth e GetHeight?
Pacerier
140
Moral da história: modele suas aulas com base em comportamentos e não em propriedades; modele seus dados com base em propriedades e não em comportamentos. Se ele se comporta como um pato, certamente é um pássaro.
Sklivvz
193
Bem, um quadrado é claramente um tipo de retângulo no mundo real. A possibilidade de modelar isso em nosso código depende das especificações. O que o LSP indica é que o comportamento do subtipo deve corresponder ao comportamento do tipo base, conforme definido na especificação do tipo base. Se a especificação do tipo base do retângulo indicar que altura e largura podem ser definidas independentemente, o LSP diz que o quadrado não pode ser um subtipo de retângulo. Se a especificação do retângulo indicar que um retângulo é imutável, um quadrado pode ser um subtipo de retângulo. É tudo sobre subtipos, mantendo o comportamento especificado para o tipo base.
SteveT
63
@ Pacerier, não há problema se for imutável. O problema real aqui é que não estamos modelando retângulos, mas sim "retângulos retocáveis", isto é, retângulos cuja largura ou altura podem ser modificados após a criação (e ainda o consideramos o mesmo objeto). Se olharmos para a classe retângulo dessa maneira, fica claro que um quadrado não é um "retângulo reshapable", porque um quadrado não pode ser remodelado e ainda pode ser um quadrado (em geral). Matematicamente, não vemos o problema porque a mutabilidade nem faz sentido em um contexto matemático.
asmeurer
14
Eu tenho uma pergunta sobre o princípio. Por que o problema seria se Square.setWidth(int width)fosse implementado assim this.width = width; this.height = width;:? Nesse caso, é garantido que a largura seja igual à altura.
MC Emperor
488

O princípio da substituição de Liskov (LSP, ) é um conceito na programação orientada a objetos que declara:

Funções que usam ponteiros ou referências a classes base devem poder usar objetos de classes derivadas sem conhecê-lo.

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:

Diagrama de Classes

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 estende Board.

À primeira vista, isso parece ser uma boa decisão. Boardfornece tanto a Heighte Widthpropriedades e ThreeDBoardfornece o eixo Z.

O ponto em que ocorre é quando você olha para todos os outros membros herdados Board. Os métodos para a AddUnit, GetTile, GetUnitse assim por diante, todos levar ambos os parâmetros X e Y na Boardclasse mas o ThreeDBoardprecisa 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 Boardclasse e os métodos herdados da Boardclasse perdem seu significado. Uma unidade de código que tenta usar a ThreeDBoardclasse como sua classe base Boardseria muito azarada.

Talvez devêssemos encontrar outra abordagem. Em vez de estender Board, ThreeDBoarddeve ser composto de Boardobjetos. Um Boardobjeto 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.

Eu não
fonte
10
Veja também Problema com elipse em círculo na Wikipedia para um exemplo semelhante, porém mais simples.
Brian
Cite @NotMySelf: "Acho que o exemplo é simplesmente demonstrar que herdar do board não faz sentido no contexto do ThreeDBoard e que todas as assinaturas de método não fazem sentido com o eixo Z".
Contango5
1
Portanto, se adicionarmos outro método a uma classe Child, mas toda a funcionalidade do Parent ainda fizer sentido na classe Child, isso estaria quebrando o LSP? Como, por um lado, modificamos a interface para usar o Child um pouco, por outro lado, se convertermos o Child em um Parent, o código que espera que um Parent funcione bem.
Nickolay Kondratyev
5
Este é um exemplo anti-Liskov. Liskov nos faz derivar retângulo da praça. Classe mais parâmetros da classe menos parâmetros. E você mostrou bem que é ruim. É realmente uma boa piada ter marcado como resposta e ter sido votada 200 vezes como resposta anti-liskov para a pergunta liskov. O princípio de Liskov é realmente uma falácia?
Gangnus
3
Eu já vi a herança funcionar da maneira errada. Aqui está um exemplo. A classe base deve ser 3DBoard e a classe derivada Board. A placa ainda tem um eixo Z de Max (Z) = Min (Z) = 1
Paulustrious 5/17
169

A substituibilidade é um princípio na programação orientada a objetos, afirmando que, em um programa de computador, se S é um subtipo de T, objetos do tipo T podem ser substituídos por objetos do tipo S

vamos fazer um exemplo simples em Java:

Mau exemplo

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

O pato pode voar por causa de um pássaro, mas e quanto a isso:

public class Ostrich extends Bird{}

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

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 
Maysara Alhindi
fonte
3
Belo exemplo, mas o que você faria se o cliente tivesse Bird bird. Você tem que lançar o objeto no FlyingBirds para usar o fly, o que não é legal, certo?
Moody
17
Não. Se o cliente possui Bird bird, significa que não pode usá-lo fly(). É isso aí. Passar a Ducknão altera esse fato. Se o cliente tiver FlyingBirds bird, mesmo que seja aprovado Duck, sempre funcionará da mesma maneira.
Steve Chamaillard
9
Isso também não seria um bom exemplo para a segregação de interface?
Saharsh 5/03/19
Excellent example Thanks Man
Abdelhadi Abdo 28/05/19
6
Que tal usar a Interface 'Flyable' (não consigo pensar em um nome melhor). Dessa forma, não nos comprometemos com essa hierarquia rígida. A menos que saibamos que realmente precisamos dela.
Thirdy
132

O LSP diz respeito aos invariantes.

O exemplo clássico é dado pela seguinte declaração de pseudo-código (implementações omitidas):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

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 Rectangledeve satisfazer o seguinte invariante:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

No entanto, esse invariante deve ser violado por uma implementação correta de Square, portanto, não é um substituto válido de Rectangle.

Konrad Rudolph
fonte
35
E, portanto, a dificuldade de usar "OO" para modelar qualquer coisa que possamos realmente modelar.
DrPizza
9
@DrPizza: Absolutamente. No entanto, duas coisas. Primeiro, esses relacionamentos ainda podem ser modelados no POO, embora incompletamente ou usando desvios mais complexos (escolha o que melhor se adequa ao seu problema). Em segundo lugar, não há alternativa melhor. Outros mapeamentos / modelagens têm os mesmos problemas ou problemas semelhantes. ;-)
Konrad Rudolph
7
@NickW Em alguns casos (mas não no acima), você pode simplesmente inverter a cadeia de herança - logicamente falando, um ponto 2D é um ponto 3D, onde a terceira dimensão é desconsiderada (ou 0 - todos os pontos estão no mesmo plano). Espaço 3D). Mas é claro que isso não é realmente prático. Em geral, esse é um dos casos em que a herança realmente não ajuda e não existe um relacionamento natural entre as entidades. Modele-os separadamente (pelo menos não conheço uma maneira melhor).
precisa saber é o seguinte
7
OOP destina-se a modelar comportamentos e não dados. Suas aulas violam o encapsulamento mesmo antes de violar o LSP.
21312 Sklivvz
2
@AustinWBryan Yep; quanto mais tempo trabalho neste campo, mais tendem a usar a herança apenas para interfaces e classes básicas abstratas e composição para o resto. Às vezes, é um pouco mais trabalhoso (digitação), mas evita um monte de problemas e é amplamente aceito por outros programadores experientes.
Konrad Rudolph
77

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):

Um exemplo simples de violação do LSP

Uma das violações mais flagrantes desse princípio é o uso de informações de tipo de tempo de execução do C ++ (RTTI) para selecionar uma função com base no tipo de um objeto. ou seja:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Claramente, a DrawShapefunção está mal formada. Ele deve conhecer todas as derivadas possíveis da Shapeclasse e deve ser alterado sempre que novas derivadas de Shapesão criadas. De fato, muitos vêem a estrutura dessa função como um anátema para o Design Orientado a Objetos.

Quadrado e retângulo, uma violação mais sutil.

No entanto, existem outras maneiras, muito mais sutis, de violar o LSP. Considere um aplicativo que use a Rectangleclasse conforme descrito abaixo:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Imagine que um dia os usuários exijam a capacidade de manipular quadrados além de retângulos. [...]

Claramente, um quadrado é um retângulo para todas as intenções e propósitos normais. Como o relacionamento ISA é válido, é lógico modelar a Square classe como sendo derivada Rectangle. [...]

Squareherdará as funções SetWidthe SetHeight. Essas funções são totalmente inapropriadas para a Square, pois a largura e a altura de um quadrado são idênticas. Isso deve ser uma pista significativa de que há um problema com o design. No entanto, existe uma maneira de contornar o problema. Poderíamos substituir SetWidthe SetHeight[...]

Mas considere a seguinte função:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Se passarmos uma referência a um Squareobjeto para essa função, o Squareobjeto será corrompido porque a altura não será alterada. Esta é uma clara violação do LSP. A função não funciona para derivadas de seus argumentos.

[...]

Phillip Wells
fonte
14
Muito tarde, mas achei que essa era uma citação interessante nesse artigo: 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.
user2023861
@ user2023861 Você está perfeitamente certo. Vou escrever uma resposta com base nisso.
Inf3rno
40

O LSP é necessário quando algum código pensa que está chamando os métodos de um tipo Te pode, sem saber, chamar os métodos de um tipo S, onde S extends T(ou seja S, herda, deriva ou é um subtipo do supertipo T).

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 tipo S. Ou, onde um identificador de tipo T, recebe um valor do tipo S.

val id : T = new S() // id thinks it's a T, but is a S

O LSP exige que as expectativas (ou seja, invariantes) de métodos do tipo T(por exemplo Rectangle), não sejam violadas quando os métodos do tipo S(por exemplo Square) são chamados.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

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.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

O LSP exige que cada método do subtipo Stenha 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 subtipo S, deve ser o mesmo ou um supertipo do tipo Tido parâmetro de entrada correspondente do método correspondente do supertipo T.

Covariância significa que a variação está na mesma direção da herança, ou seja, o tipo Soda saída de cada método do subtipo S, deve ser o mesmo ou um subtipo do tipo Toda saída correspondente do método correspondente do supertipo T.

Isso ocorre porque se o chamador pensa que tem um tipo T, pensa que está chamando um método de T, ele fornece argumentos do tipo Tie atribui a saída ao tipo To. Quando, na verdade, ele está chamando o método correspondente de S, cada Tiargumento de entrada é atribuído a um Siparâmetro de entrada e a Sosaída é atribuída ao tipo To. Assim, se Sinão houvesse contravenção Ti, então um subtipo Xi- que não seria um subtipo de - Sipoderia ser atribuído Ti.

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 Tdeve ser oposta ou na mesma direção respectivamente para cada parâmetro de entrada ou saída (de todo método de T) 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:

  • Teoremas da incompletude de Gödel : qualquer teoria formal, na qual todas as verdades aritméticas possam ser provadas, é inconsistente.
  • Paradoxo de Russell : toda regra de associação para um conjunto que pode conter um conjunto, enumera o tipo específico de cada membro ou se contém. Assim, os conjuntos não podem ser estendidos ou são uma recursão ilimitada. Por exemplo, o conjunto de tudo que não é um bule de chá inclui a si mesmo, o que inclui a si mesmo, o que inclui a si mesmo, etc. Portanto, uma regra é inconsistente se (pode conter um conjunto e) não enumera os tipos específicos (ou seja, permite todos os tipos não especificados) e não permite a extensão ilimitada. Este é o conjunto de conjuntos que não são membros de si mesmos. Essa incapacidade de ser consistente e completamente enumerada em toda extensão possível é o teorema da incompletude de Gödel.
  • Princípio da Substituição de Liskov : geralmente é um problema indecidível se um conjunto é o subconjunto de outro, ou seja, a herança é geralmente indecidível.
  • Referenciação de Linsky : é indecidível o que é o cálculo de algo, quando é descrito ou percebido, ou seja, a percepção (realidade) não tem ponto de referência absoluto.
  • Teorema de Coase : não há ponto de referência externo; portanto, qualquer barreira às possibilidades externas ilimitadas fracassará.
  • Segunda lei da termodinâmica : o universo inteiro (um sistema fechado, ou seja, tudo) tende à máxima desordem, ou seja, ao máximo de possibilidades independentes.
Shelby Moore III
fonte
17
@ Shelyby: Você misturou muitas coisas. As coisas não são tão confusas quanto você as indica. Muitas de suas afirmações teóricas se baseiam em argumentos frágeis, como 'Para que o conhecimento exista, existem muitas possibilidades inesperadas, .........' AND 'geralmente é um problema indecidível se um conjunto é o subconjunto de outro, ou seja, herança é geralmente indecidível '. Você pode iniciar um blog separado para cada um desses pontos. De qualquer forma, suas asserções e premissas são altamente questionáveis. Não se deve usar coisas das quais não se tem conhecimento!
aknon
1
@aknon Eu tenho um blog que explica esses assuntos com mais profundidade. Meu modelo TOE de espaço-tempo infinito é frequências ilimitadas. Não é confuso para mim que uma função indutiva recursiva tenha um valor inicial conhecido com um limite final infinito, ou uma função coindutora tenha um valor final desconhecido e um limite inicial conhecido. A relatividade é o problema quando a recursão é introduzida. É por isso que Turing complete é equivalente a recursão ilimitada .
Shelby Moore III
4
@ShelbyMooreIII Você está indo em muitas direções. Esta não é uma resposta.
Soldalma
1
@Soldalma é uma resposta. Você não vê isso na seção Resposta. O seu é um comentário porque está na seção de comentários.
Shelby Moore III
1
Como sua mistura com o mundo scala!
Ehsan M. Kermani
24

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:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

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:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Agora, os subtipos não podem ser usados ​​da mesma maneira, pois não produzem mais o mesmo resultado.

Steve Chamaillard
fonte
6
O exemplo não viola o LSP apenas enquanto restringimos a semântica Database::selectQuerypara 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.
Palec
5
Achei a resposta mais fácil de entender do resto.
Malcolm Salvador
23

Há uma lista de verificação para determinar se você está violando Liskov ou não.

  • Se você violar um dos seguintes itens -> você viola Liskov.
  • Se você não violar nada -> não posso concluir nada.

Lista de controle:

  • Nenhuma nova exceção deve ser lançada na classe derivada : se a sua classe base lançou ArgumentNullException, suas subclasses só tiveram permissão para lançar exceções do tipo ArgumentNullException ou quaisquer exceções derivadas de ArgumentNullException. Lançar IndexOutOfRangeException é uma violação de Liskov.
  • As pré-condições não podem ser fortalecidas : suponha que sua classe base funcione com um membro int. Agora, seu subtipo exige que o int seja positivo. Isso é pré-condições reforçadas, e agora qualquer código que funcionou perfeitamente bem antes com ints negativos é quebrado.
  • As pós-condições não podem ser enfraquecidas : suponha que sua classe base exigida todas as conexões com o banco de dados devem ser fechadas antes do retorno do método. Na sua subclasse, você substituiu esse método e deixou a conexão aberta para reutilização. Você enfraqueceu as pós-condições desse método.
  • Os invariantes devem ser preservados : a restrição mais difícil e dolorosa a ser cumprida. Os invariantes estão ocultos na classe base e a única maneira de revelá-los é ler o código da classe base. Basicamente, você deve ter certeza de que, quando substituir um método, qualquer coisa imutável deve permanecer inalterada após a execução do método substituído. A melhor coisa que consigo pensar é impor essas restrições invariantes na classe base, mas isso não seria fácil.
  • 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):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

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:

Cù Đức Hiếu
fonte
Também sou desenvolvedor de C # e direi que sua última declaração não é verdadeira a partir do Visual Studio 2010, com a estrutura .Net 4.0. A covariância dos tipos de retorno permite um tipo de retorno mais derivado do que o definido pela interface. Exemplo: Exemplo: IEnumerable <T> (T é covariante) IEnumerator <T> (T é covariante) IQueryable <T> (T é covariante) IGrouping <TKey, TElement> (TKey e TElement são covariantes) IComparer <T> (T é incompatível) IEqualityComparer <T> (T é contravariante) IComparable <T> (T é contravariante) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter
1
Resposta ótima e focada (embora as perguntas originais tratassem mais de exemplos do que de regras).
Mike
22

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

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

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.

Charlie Martin
fonte
9
Mas ... se você sempre obtém o mesmo comportamento, qual é o sentido de ter a classe derivada?
Leonid
2
Você perdeu um ponto: é o mesmo comportamento observado . Você pode, por exemplo, substituir algo pelo desempenho O (n) por algo funcionalmente equivalente, mas pelo desempenho O (lg n). Ou você pode substituir algo que acesse dados implementados com o MySQL e substituí-lo por um banco de dados na memória.
Charlie Martin
@ Charlie Martin, codificando para uma interface em vez de uma implementação - eu gosto disso. Isso não é exclusivo do OOP; linguagens funcionais como o Clojure também promovem isso. Mesmo em termos de Java ou C #, acho que usar uma interface em vez de usar uma classe abstrata mais hierarquias de classes seria natural para os exemplos que você fornece. Python não é fortemente tipado e realmente não possui interfaces, pelo menos não explicitamente. Minha dificuldade é que eu faço OOP há vários anos sem aderir ao SOLID. Agora que me deparei com isso, parece limitante e quase contraditório.
Hamish Grubijan 06/07/12
Bem, você precisa voltar e conferir o artigo original de Barbara. reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Isso não é realmente afirmado em termos de interfaces, e é uma relação lógica que mantém (ou não) em qualquer linguagem de programação que possui alguma forma de herança.
Charlie Martin
1
@HamishGrubijan Não sei quem lhe disse que o Python não é fortemente digitado, mas eles estavam mentindo para você (e se você não acredita em mim, ligue um intérprete de Python e tente 2 + "2"). Talvez você confunda "fortemente tipado" com "estaticamente digitado"?
asmeurer
21

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 .

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

E uma subclasse estendendo-a:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Em seguida, você pode ter um cliente trabalhando com a API Base ItemsRepository e confiando nela.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

O LSP é interrompido quando a substituição da classe pai por uma subclasse quebra o contrato da API .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Você pode aprender mais sobre como escrever software sustentável em meu curso: https://www.udemy.com/enterprise-php/

Lukas Lukac
fonte
20

Funções que usam ponteiros ou referências a classes base devem poder usar objetos de classes derivadas sem conhecê-lo.

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.

Chris Ammerman
fonte
19

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:

  1. 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.

  2. Métodos Regra: A implementação dessas operações é semanticamente correta.

    • Pré-condições mais fracas: As funções de subtipo devem levar pelo menos o que o supertipo tomou como entrada, se não mais.
    • Pós-condições mais fortes: eles devem produzir um subconjunto da saída produzida pelos métodos do supertipo.
  3. Regra de propriedades: Isso vai além das chamadas de função individuais.

    • Invariantes: Coisas sempre verdadeiras devem permanecer verdadeiras. Por exemplo. O tamanho de um conjunto nunca é negativo.
    • Propriedades evolutivas: geralmente algo a ver com imutabilidade ou com o tipo de estados em que o objeto pode estar. Ou talvez o objeto apenas cresça e nunca encolha, para que os métodos do subtipo não o façam.

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

snagpaul
fonte
18

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 ATestclasse 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? "

avandeursen
fonte
14

Vamos ilustrar em Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

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:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

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.

Esses são os tipos de problemas aos quais a violação do Princípio de Substituição de Liskov leva, e geralmente podem ser reconhecidos por um método que não faz nada, ou mesmo 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:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Agora podemos estender o TransportationDevice para dispositivos não motorizados.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

E estenda o TransportationDevice para dispositivos motorizados. Aqui é mais apropriado adicionar o objeto Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

Assim, nossa classe Car se torna mais especializada, ao mesmo tempo em que adere ao Princípio de Substituição de Liskov.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

E nossa classe de bicicleta também está em conformidade com o Princípio de Substituição de Liskov.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
Khaled Qasem
fonte
9

Essa formulação do LSP é muito forte:

Se para cada objeto o1 do tipo S existe um objeto o2 do tipo T, de modo que, para todos os programas P definidos em termos de T, o comportamento de P permanece inalterado quando o1 é substituído por o2, então S é um subtipo de T.

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.

Damien Pollet
fonte
2
Erm, essa formulação é própria de Barbara Liskov. Barbara Liskov, "Abstração de Dados e Hierarquia", SIGPLAN Notices, 23,5 (maio de 1988). Não é "muito forte", é "exatamente correto" e não tem a implicação que você acha que tem. É forte, mas tem a quantidade certa de força.
DrPizza 30/11/2009
Então, há muito poucos subtipos na vida real :)
Damien Pollet
3
"O comportamento não é alterado" não significa que um subtipo fornecerá exatamente os mesmos valores concretos de resultado. Isso significa que o comportamento do subtipo corresponde ao esperado no tipo base. Exemplo: tipo base Shape pode ter um método draw () e estipular que esse método deve renderizar a forma. Dois subtipos de Shape (por exemplo, Square e Circle) implementariam o método draw () e os resultados pareceriam diferentes. Mas, desde que o comportamento (renderizando a forma) corresponda ao comportamento especificado de Shape, Square e Circle serão subtipos de Shape de acordo com o LSP.
SteveT
9

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.

Alireza Rahmani Khalili
fonte
9

Princípio da Substituição de Liskov (LSP)

O tempo todo projetamos um módulo de programa e criamos algumas hierarquias de classe. Em seguida, estendemos algumas classes, criando algumas classes derivadas.

Devemos garantir que as novas classes derivadas se estendam apenas sem substituir a funcionalidade das classes antigas. Caso contrário, as novas classes podem produzir efeitos indesejados quando usadas em módulos de programas existentes.

O Princípio de Substituição de Liskov 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.

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

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Conclusão:

Esse princípio é apenas uma extensão do Princípio de Fechamento Aberto e significa que devemos garantir que novas classes derivadas estendam as classes base sem alterar seu comportamento.

Veja também: Abrir Fechar Princípio

Alguns conceitos semelhantes para melhor estrutura: Convenção sobre configuração

GauRang Omar
fonte
8

O princípio da substituição de Liskov

  • O método substituído não deve permanecer vazio
  • O método substituído não deve gerar um erro
  • A classe base ou o comportamento da interface não devem ser modificados (retrabalhados), devido a comportamentos de classe derivados.
Rahamath
fonte
7

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:

  • As variantes internas da classe base devem ser preservadas pela classe derivada
  • As pré-condições da classe base não devem ser reforçadas pela classe derivada
  • As pós-condições da classe base não devem ser enfraquecidas pela classe derivada.

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

aknon
fonte
6

O LSP, em termos simples, afirma que objetos da mesma superclasse devem poder ser trocados entre si sem quebrar nada.

Por exemplo, se tivermos Catuma Dogclasse e uma derivada de uma Animalclasse, qualquer função que utilize a classe Animal deverá poder usar Catou Dogcomportar-se normalmente.

johannesMatevosyan
fonte
4

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).

Tom Hawtin - linha de orientação
fonte
1
Penso que o exemplo é simplesmente demonstrar que herdar da placa não faz sentido no contexto do ThreeDBoard e todas as assinaturas de método são sem sentido com um eixo Z.
NotMyself
4

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.

Wouter
fonte
4

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.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

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.

public class Square : 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:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Agora, quando alguém definir a largura de um objeto quadrado, sua altura mudará de acordo e vice-versa.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Vamos seguir em frente e considerar esta outra função:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

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.

Ivan Porta
fonte
3

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.

Prasa
fonte
1
Forneça os exemplos de código no stackoverflow.
sebenalern
3

Digamos que usamos um retângulo em nosso código

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

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 Squareaula também com base nesta informação:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Se substituirmos Rectanglepor Squareno nosso primeiro código, ele será quebrado:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Isso ocorre porque o Squaretem um novo pré-requisito que não tínhamos na Rectangleclasse: width == height. De acordo com o LSP, as Rectangleinstâncias devem ser substituíveis pelas Rectangleinstâncias da subclasse. Isso ocorre porque essas instâncias passam na verificação de tipo para Rectangleinstâ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.

inf3rno
fonte
3

O LSP diz que "Os objetos devem ser substituíveis por seus subtipos". Por outro lado, esse princípio aponta para

As classes filho nunca devem quebrar as definições de tipo da classe pai.

e o exemplo a seguir ajuda a entender melhor o LSP.

Sem LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Fixação por LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}
Zahra.HY
fonte
2

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.

Ryszard Dżegan
fonte
2

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.

Raghu Reddy Muttana
fonte
2

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.

Ishan Aggarwal
fonte
1

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:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

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:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

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:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

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 sehasEnoughMoney() - 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.

Vadim Samokhin
fonte