Pergunta

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?

Foi útil?

Solução 2

O Princípio da Substituição de Liskov (LSP, ) é um conceito em Programação Orientada a Objetos que afirma:

Funções que usam ponteiros ou As referências às classes base devem ser capaz de usar objetos de classes derivadas sem saber.

Em sua essência, o LSP trata de interfaces e contratos, bem como de como decidir quando estender uma classe vs.use outra estratégia, como composição, para atingir seu objetivo.

A maneira mais eficaz que encontrei para ilustrar esse ponto foi Use a cabeça primeiro OOA&D.Eles apresentam um cenário em que você é desenvolvedor de um projeto para construir uma estrutura para jogos de estratégia.

Eles apresentam uma classe que representa um quadro parecido com este:

Class Diagram

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

O livro prossegue alterando os requisitos para dizer que a estrutura do jogo também deve suportar tabuleiros de jogos 3D para acomodar jogos que voam.Então um ThreeDBoard é introduzida uma classe que estende Board.

À primeira vista, esta parece ser uma boa decisão. Board fornece tanto o Height e Width propriedades e ThreeDBoard fornece o eixo Z.

O problema é quando você olha para todos os outros membros herdados de Board.Os métodos para AddUnit, GetTile, GetUnits e assim por diante, todos usam os parâmetros X e Y no Board classe, mas o ThreeDBoard precisa de um parâmetro Z também.

Então você deve implementar esses métodos novamente com um parâmetro Z.O parâmetro Z não tem contexto para o Board classe e os métodos herdados da Board classe perde o sentido.Uma unidade de código que tenta usar o ThreeDBoard class como sua classe base Board estaria muito sem sorte.

Talvez devêssemos encontrar outra abordagem.Em vez de estender Board, ThreeDBoard deverá ser composto por Board objetos.Um Board 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.

Outras dicas

Um ótimo exemplo que ilustra o LSP (dado pelo tio Bob em um podcast que ouvi recentemente) foi como às vezes algo que soa bem em linguagem natural não funciona bem no código.

Em matemática, um Square é um Rectangle.Na verdade, é uma especialização de um retângulo.O "é um" faz você querer modelar isso com herança.No entanto, se no código você fez Square derivado de Rectangle, então uma Square deve ser utilizável em qualquer lugar onde você espera um Rectangle.Isso causa algum comportamento estranho.

Imagine que você tivesse SetWidth e SetHeight métodos em seu Rectangle classe base;isso parece perfeitamente lógico.No entanto, se o seu Rectangle referência apontou para um Square, então SetWidth e SetHeight não faz sentido porque definir um alteraria o outro para corresponder a ele.Nesse caso Square falha no teste de substituição de Liskov com Rectangle e a abstração de ter Square herdar de Rectangle é ruim.

enter image description here

Vocês deveriam conferir o outro valor inestimável Cartazes motivacionais dos princípios SOLID.

LSP diz respeito a invariantes.

O exemplo clássico é dado pela seguinte declaração de pseudocó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.A razão é que violamos invariantes decorrentes da definição matemática de quadrados e retângulos.A maneira como getters e setters funcionam, um Rectangle deve satisfazer o seguinte invariante:

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

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

A substituibilidade é um princípio da programação orientada a objetos que afirma que, em um programa de computador, se S for um subtipo de T, então os 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 porque é um pássaro, mas e isto:

public class Ostrich extends Bird{}

Avestruz é um pássaro, mas não pode voar, a classe Avestruz é um subtipo da classe Bird, mas não pode usar o método fly, o que 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{} 

Robert Martin tem um excelente artigo sobre o Princípio da Substituição de Liskov.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 gritantes desse princípio é o uso do C++ Informações de tipo de tempo de execução (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 o DrawShape a função está mal formada.Deve saber sobre todos os derivados possíveis do Shape classe, e deve ser mudada sempre que novos derivados de Shape são criados.Na verdade, muitos vêem a estrutura desta 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 formas, muito mais sutis, de violar o LSP.Considere uma aplicação que usa o Rectangle classe 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 todos os efeitos e propósitos normais.Como o relacionamento ISA é válido, é lógico modelar o Squareclasse como sendo derivada de Rectangle. [...]

Square herdará o SetWidth e SetHeight funções.Estes funções são totalmente inadequadas para um Square, uma vez que a largura e altura de um quadrado são idênticos.Esta deve ser uma pista significativa que há um problema com o design.No entanto, há uma maneira de contorne o problema.Poderíamos substituir SetWidth e SetHeight [...]

Mas considere a seguinte função:

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

Se passarmos uma referência a um Square objeto nesta função, o Square o objeto 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.

[...]

LSP é necessário onde algum código pensa que está chamando os métodos de um tipo T, e pode inadvertidamente 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 quando uma função com um parâmetro de entrada do tipo T, é chamado (ou seja,invocado) com um valor de argumento do tipo S.Ou, onde um identificador do tipo T, é atribuído um valor do tipo S.

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

LSP requer as expectativas (ou seja,invariantes) para métodos do tipo T (por exemplo. Rectangle), não deve ser violado quando os métodos do tipo S (por exemplo. Square) são chamados em vez disso.

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 tem invariantes, por ex.o imutável Os configuradores de retângulos esperam que as dimensões sejam modificadas de forma independente, mas o imutável Os setters quadrados 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)
}

LSP requer que cada método do subtipo S deve ter parâmetro(s) de entrada contravariante(s) 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 Ti do parâmetro de entrada correspondente do método correspondente do supertipo T.

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

Isso ocorre porque se o chamador achar que tem um tipo T, pensa que está chamando um método de T, então ele fornece argumentos do tipo Ti e atribui a saída ao tipo To.Quando ele está realmente chamando o método correspondente de S, então cada Ti argumento de entrada é atribuído a um Si parâmetro de entrada e o So a saída é atribuída ao tipo To.Assim se Si não eram contravariantes w.r.t.para Ti, então um subtipo Xi- o que não seria um subtipo de Si-poderia ser atribuído a Ti.

Além disso, para idiomas (por ex.Scala ou Ceilão) que possuem anotações de variação de local de definição em parâmetros de polimorfismo de tipo (ou seja,genéricos), a co- ou contra-direção da anotação de variação para cada parâmetro de tipo do tipo T devemos ser oposto ou na mesma direção, respectivamente, para cada parâmetro de entrada ou saída (de cada método de T) que possui o tipo do parâmetro type.

Além disso, para cada parâmetro de entrada ou saída que possui um tipo de função, a direção de variância necessária é invertida.Esta regra é aplicada recursivamente.


A subtipagem é apropriada onde os invariantes podem ser enumerados.

Há muitas pesquisas em andamento sobre como modelar invariantes, para que sejam aplicados pelo compilador.

Estado de tipo (consulte a página 3) declara e impõe invariantes de estado ortogonais ao tipo.Alternativamente, os invariantes podem ser aplicados por convertendo asserções em tipos.Por exemplo, para afirmar que um arquivo está aberto antes de fechá-lo, File.open() poderia retornar um tipo OpenFile, que contém um método close() que não está disponível em File.A 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 em Turing, por ex. escala.Linguagens de tipo dependente e provadores de teoremas formalizam os modelos de digitação de ordem superior.

Devido à necessidade da semântica resumo sobre extensão, espero que o emprego da digitação para modelar invariantes, ou seja,semântica denotacional unificada de ordem superior, é superior ao Typestate.‘Extensão’ significa a composição ilimitada e permutada de desenvolvimento modular e descoordenado.Porque me parece ser a antítese da unificação e, portanto, dos graus de liberdade, ter dois modelos mutuamente dependentes (por exemplo,types e Typestate) para expressar a semântica compartilhada, que não pode ser unificada entre si para composição extensível.Por exemplo, Problema de ExpressãoA extensão semelhante a foi unificada nos domínios de subtipagem, sobrecarga de funções e digitação paramétrica.

Minha posição teórica é que para conhecimento para existir (ver seção “A centralização é cega e inadequada”), haverá nunca ser um modelo geral que pode impor 100% de cobertura de todos os invariantes possíveis em uma linguagem de computador Turing-completa.Para que o conhecimento exista, existem muitas possibilidades inesperadas, ou seja,a desordem e a entropia devem estar sempre aumentando.Esta é a força entrópica.Provar todos os cálculos possíveis de uma extensão potencial é calcular a priori todas as extensões possíveis.

É por isso que existe o Teorema da Halting, ou seja,é indecidível se todos os programas possíveis em uma linguagem de programação Turing-completa terminam.Pode-se comprovar que algum programa específico termina (aquele em que todas as possibilidades foram definidas e computadas).Mas é impossível provar que toda extensão possível desse programa termina, a menos que as possibilidades de extensão desse programa não sejam Turing completas (por exemplo,via digitação dependente).Uma vez que o requisito fundamental para a completude de Turing é recursão ilimitada, é intuitivo compreender 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 uma compreensão conceitual generalizada 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 contém a si mesmo.Portanto, os conjuntos não podem ser estendidos ou são recursivos ilimitados.Por exemplo, o conjunto de tudo o que não é bule, inclui-se, que se inclui, que se inclui, etc….Assim, uma regra é inconsistente se (pode conter um conjunto e) não enumerar os tipos específicos (ou seja,permite todos os tipos não especificados) e não permite extensão ilimitada.Este é o conjunto de conjuntos que não são membros de si mesmos.Esta incapacidade de ser consistente e completamente enumerada em todas as extensões possíveis é o teorema da incompletude de Gödel.
  • Princípio da substituição de Liskov:geralmente é um problema indecidível se algum conjunto é subconjunto de outro, ou seja,a herança é geralmente indecidível.
  • Referência 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 existe um ponto de referência externo, portanto qualquer barreira às possibilidades externas ilimitadas falhará.
  • Segunda lei da termodinâmica:todo o universo (um sistema fechado, ou seja,tudo) tende à desordem máxima, ou seja,possibilidades independentes máximas.

O LSP é uma regra sobre o contrato das aulas:se uma classe base satisfaz um contrato, então, pelo LSP, as classes derivadas 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ê chamar Foo em um objeto Derived, ele fornecer exatamente os mesmos resultados que chamar Foo em um objeto Base, desde que arg seja o mesmo.

Funções que usam ponteiros ou referências para classes base devem ser capazes de usar objetos de classes derivadas sem saber disso.

Quando li pela primeira vez sobre LSP, presumi que isso se referia a um sentido muito estrito, essencialmente equiparando-o à implementação de interface e à conversão de tipo seguro.O que significaria que o LSP é garantido ou não pela própria linguagem.Por exemplo, neste sentido estrito, ThreeDBoard é certamente substituível por Board, no que diz respeito ao compilador.

Depois de ler mais sobre o conceito, descobri que o LSP geralmente é interpretado de forma mais ampla do que isso.

Resumindo, o que significa para o código do cliente "saber" que o objeto por trás do ponteiro é de um tipo derivado, e não do tipo 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 do estado de um objeto e dos argumentos do método nos resultados das chamadas de método ou nos tipos de exceções geradas pelo objeto.

Voltando ao exemplo novamente, em teoria os métodos do Board podem funcionar perfeitamente no ThreeDBoard.Na prática, porém, será muito difícil evitar diferenças de comportamento que o cliente pode não tratar adequadamente, sem prejudicar a funcionalidade que o ThreeDBoard pretende adicionar.

Com esse conhecimento em mãos, avaliar a adesão ao LSP pode ser uma ótima ferramenta para determinar quando a composição é o mecanismo mais apropriado para estender a funcionalidade existente, em vez da herança.

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

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

Lista de controle:

  • Nenhuma nova exceção deve ser lançada na classe derivada:Se sua classe base lançasse ArgumentNullException, então suas subclasses só poderiam 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 reforçadas:Suponha que sua classe base funcione com um membro int.Agora, seu subtipo exige que int seja positivo.Isso fortalece as pré-condições e agora qualquer código que funcionava perfeitamente bem antes com entradas negativas é quebrado.
  • As pós-condições não podem ser enfraquecidas:Suponha que sua classe base exija que todas as conexões com o banco de dados sejam fechadas antes que o método seja retornado.Na sua subclasse, você substituiu esse método e deixou a conexão aberta para reutilização adicional.Você enfraqueceu as pós-condições desse método.
  • Invariantes devem ser preservados:A restrição mais difícil e dolorosa de cumprir.Os invariantes ficam algum tempo ocultos na classe base e a única maneira de revelá-los é lendo o código da classe base.Basicamente, você precisa ter certeza de que, ao 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 histórica: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 neste código e você verá que Name está definido como não modificável (conjunto privado), mas SubType introduz um novo método que permite modificá-lo (por meio de 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 dos argumentos do método e Covariância de tipos de retorno.Mas não é possível em C# (sou desenvolvedor C#), então não me importo com eles.

Referência:

Um exemplo importante do usar do LSP está em teste de software.

Se eu tiver uma classe A que seja uma subclasse de B compatível com LSP, posso reutilizar o conjunto de testes de B para testar A.

Para testar completamente a subclasse A, provavelmente precisarei 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 é construir o que McGregor chama de "hierarquia paralela para testes":Meu ATest classe herdará de BTest.Alguma forma de injeção é então necessária para garantir que o caso de teste funcione com objetos do tipo A em vez do tipo B (um padrão de método de modelo simples servirá).

Observe que reutilizar o conjunto de supertestes para todas as implementações de subclasses é, na verdade, uma forma de testar se essas implementações de subclasses são compatíveis com LSP.Assim, pode-se também argumentar que deve execute o conjunto de testes da superclasse no contexto de qualquer subclasse.

Veja também a resposta à pergunta Stackoverflow "Posso implementar uma série de testes reutilizáveis ​​para testar a implementação de uma interface?"

Acho que todo mundo já entendeu o que é LSP tecnicamente:Basicamente, você deseja ser capaz de abstrair os detalhes dos subtipos e usar os supertipos com segurança.

Portanto, Liskov tem 3 regras subjacentes:

  1. Regra de assinatura:Deve haver uma implementação válida de cada operação do supertipo no subtipo sintaticamente.Algo que um compilador poderá verificar para você.Existe uma pequena regra sobre lançar menos exceções e ser pelo menos tão acessível quanto os métodos de supertipo.

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

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

    • Invariantes:Coisas que são sempre verdadeiras devem permanecer verdadeiras.Por exemplo.o tamanho de um conjunto nunca é negativo.
    • Propriedades Evolutivas:Geralmente algo relacionado à imutabilidade ou ao tipo de estado em que o objeto pode estar.Ou talvez o objeto apenas cresça e nunca diminua, então os métodos do subtipo não deveriam sobreviver.

Todas essas propriedades precisam ser preservadas e a funcionalidade extra do subtipo não deve violar as propriedades do supertipo.

Se essas três coisas forem cuidadas, você terá abstraído o material subjacente e estará escrevendo código fracamente acoplado.

Fonte:Desenvolvimento de Programas em Java - Barbara Liskov

Longo Resumindo a história, vamos deixar retângulos retângulos e quadrados quadrados, exemplo prático ao estender uma classe pai, você tem que PRESERVAR a API pai exata ou ESTENDÊ-LA.

Digamos que você tenha um base Repositório de itens.

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;
    }
}

Então você poderia ter um Cliente trabalhando com a API Base ItemsRepository e contando com ela.

/**
 * 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 PSL está quebrado quando substituindo pai aula com um 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/

Esta formulação do LSP é demasiado forte:

Se para cada objeto o1 do tipo S existe um objeto o2 do tipo T tal 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 exatamente da mesma coisa que T.E eu poderia ser ousado e decidir que o desempenho faz parte do comportamento de P...

Então, basicamente, qualquer uso de late-binding viola o LSP.O objetivo do OO é obter um comportamento diferente quando substituímos um objeto de um tipo por outro de outro tipo!

A formulação citada por Wikipédia é melhor porque a propriedade depende do contexto e não inclui necessariamente todo o comportamento do programa.

Alguns adendos:
Eu me pergunto por que ninguém escreveu sobre Invariant , pré-condições e pós-condições da classe base que devem ser obedecidas pelas classes derivadas.Para que uma classe derivada D seja completamente substituível pela classe Base B, a classe D deve obedecer a certas condições:

  • In-variantes da classe base devem ser preservados 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, a derivada 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 'É A' será obedecido apenas quando certas regras forem obedecidas pelo subtipo.Estas regras, na forma de invariantes, pré-condições e pós-condições, devem ser decididas por uma decisão formal.contrato de projeto'.

Mais discussões sobre isso disponíveis em meu blog: Princípio de substituição de Liskov

Numa frase muito simples, podemos dizer:

A classe filha não deve violar as características da classe base.Deve ser capaz com isso.Podemos dizer que é o mesmo que subdigitar.

Vejo retângulos e quadrados em todas as respostas e como violar o LSP.

Gostaria de mostrar como o LSP pode ser compatível 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 escolhermos 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 forma, pois não produzem mais o mesmo resultado.

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

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

Devemos garantir que as novas classes derivadas apenas se estendam sem substituindo a funcionalidade de classes antigas.Caso contrário, as novas classes pode produzir efeitos indesejados quando eles são usados em programa existente Módulos.

O Princípio de Substituição de Liskov afirma que se um módulo de programa é usando uma classe Base, então a referência à classe Base pode ser substituído por uma classe derivada sem afetar a funcionalidade de o módulo do programa.

Exemplo:

Abaixo está o exemplo clássico em que o Princípio da 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 a aplicação e adicionamos a classe Square.A classe square é retornada por um padrão de fábrica, baseado em algumas condições e não sabemos exatamente que tipo de objeto será retornado.Mas sabemos que é um retângulo.Pegamos o objeto retângulo, definimos a largura como 5 e a altura como 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:

Este princípio é apenas uma extensão do Princípio do Fechamento Aberto e significa que devemos nos certificar de que novas classes derivadas estão se estendendo as classes base sem mudar seu comportamento.

Veja também: Princípio Abrir Fechar

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

A implementação do ThreeDBoard em termos de um conjunto de Board seria tão útil?

Talvez você queira tratar fatias do ThreeDBoard em vários planos como uma placa.Nesse caso, você pode querer abstrair uma interface (ou classe abstrata) para Board para permitir múltiplas implementações.

Em termos de interface externa, você pode querer fatorar uma interface Board para TwoDBoard e ThreeDBoard (embora nenhum dos métodos acima sirva).

Um quadrado é um retângulo onde a largura é igual à altura.Se o quadrado definir dois tamanhos diferentes para largura e altura, ele violará a invariante quadrada.Isso é contornado com a introdução de efeitos colaterais.Mas se o retângulo tivesse um setSize(height, width) com 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 o lsp).Isto mostra que embora quadrado seja um retângulo, não é um subtipo válido porque a pré-condição é reforçada.A solução alternativa (em geral, uma coisa ruim) causa um efeito colateral e enfraquece a pós-condição (que viola o lsp).setWidth na base tem pós-condição 0 <largura.A derivada enfraquece com altura == largura.

Portanto, um quadrado redimensionável não é um retângulo redimensionável.

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

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

Se substituirmos o Rectangle com Square em nosso primeiro código, ele irá quebrar:

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

Isso ocorre porque o Square tem uma nova pré-condição que não tínhamos no Rectangle aula: width == height.De acordo com o LSP o Rectangle instâncias devem ser substituíveis por Rectangle instâncias de subclasse.Isso ocorre porque essas instâncias passam na verificação de tipo para Rectangle instâncias e, portanto, causarão erros inesperados em seu código.

Este foi um exemplo para o "as pré-condições não podem ser fortalecidas em um subtipo" parte no artigo wiki.Resumindo, violar o LSP provavelmente causará erros no seu código em algum momento.

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á 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!*/
}

Nem tudo está indo como planejado agora!Sim, a bicicleta é um meio de transporte, porém não possui motor e, portanto, o método startEngine() não pode ser implementado.

Estes são os tipos de problemas que violam a substituição de Liskov O princípio leva a, e eles geralmente podem ser reconhecidos por um método que não faz nada, ou mesmo não pode ser implementado.

A solução para estes 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.Embora a bicicleta seja um meio de transporte, ela não tem motor.Neste exemplo, a nossa definição de dispositivo de transporte está errada.Não deveria ter 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 Carro torna-se mais especializada, ao mesmo tempo que adere ao Princípio de Substituição de Liskov.

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

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

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

Eu encorajo você a ler o artigo: Violando o Princípio da Substituição de Liskov (LSP).

Você pode encontrar lá uma explicação do que é o Princípio de Substituição de Liskov, pistas gerais que ajudam você a adivinhar se você já o violou e um exemplo de abordagem que o ajudará a tornar sua hierarquia de classes mais segura.

A explicação mais clara para o LSP que encontrei até agora foi "O Princípio da 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 " de aqui.O artigo fornece um exemplo de código para violar o LSP e corrigi-lo.

O PRINCÍPIO DE SUBSTITUIÇÃO DE LISKOV (do livro de Mark Seemann) afirma que devemos ser capazes de substituir uma implementação de uma interface por outra sem quebrar o cliente ou a implementação. 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 até funcionar com baterias por um período de tempo) .Com o software, entretanto, um cliente geralmente espera que um serviço esteja disponível.Se o serviço foi removido, obteremos 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 Null Object,[4] e corresponde aproximadamente a desconectar o computador da parede.Como estamos usando acoplamento fraco, podemos substituir uma implementação real por algo que não faz nada sem causar problemas.

O Princípio da Substituição de 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 Derivada sem afetar a funcionalidade do módulo do programa.

Intenção - Os tipos derivados devem ser completamente substituíveis por seus tipos básicos.

Exemplo – Tipos de retorno covariantes em java.

LSP diz que ''Os objetos devem ser substituídos por seus subtipos''.Por outro lado, este princípio aponta para

As classes filhas 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();
}

Correçã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();
}

Deixe-me tentar, considere uma interface:

interface Planet{
}

Isso é implementado por classe:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Você usará a Terra como:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Agora considere mais uma classe que estende a Terra:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

Agora, de acordo com o LSP, você poderá usar o LiveablePlanet no lugar da Terra e isso não deverá danificar seu sistema.Como:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Exemplos retirados de aqui

Aqui está um trecho de esta postagem isso esclarece bem as coisas:

[..] para compreender alguns princípios, é importante perceber quando eles foram violados.Isso é o que farei agora.

O que significa a violação deste princípio?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 incorretamente suas abstrações.

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.Isto ocorre porque o contrato da conta nos diz que uma conta seria retirada, mas nem sempre é esse o caso.Então, o que devo fazer para consertar isso?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 foi cumprido.

Essa violação sutil muitas vezes impõe ao cliente a capacidade de diferenciar os objetos concretos empregados.Por exemplo, dado o contrato da primeira conta, poderia ser assim:

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 aberto-fechado [ou seja, para exigência 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 retornará nada, provavelmente uma exceção será lançada.Então você tem que verificar se hasEnoughMoney() - que não faz parte de uma interface.Portanto, esta verificação forçada dependente da classe concreta é uma violação do OCP].

Este ponto também aborda um equívoco que encontro com frequência sobre a violação do LSP.Diz que "se o comportamento de um pai mudou em uma criança, então, isso viola o PSL". No entanto, isso não acontece – desde que a criança não viole o contrato de seus pais.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top