Como verificar o princípio de substituição de Liskov em uma hierarquia de herança?

14

Inspirado por esta resposta:

O princípio da substituição de Liskov exige que

  • As pré-condições não podem ser reforçadas em um subtipo.
  • As pós-condições não podem ser enfraquecidas em um subtipo.
  • Invariantes do supertipo devem ser preservados em um subtipo.
  • Restrição do histórico (a "regra do histórico"). Os objetos são considerados modificáveis ​​apenas por meio de seus métodos (encapsulamento). Como os subtipos podem introduzir métodos que não estão presentes no supertipo, a introdução desses métodos pode permitir alterações de estado no subtipo que não são permitidas no supertipo. A restrição de história proíbe isso.

Eu esperava que alguém publicasse uma hierarquia de classes que viole esses 4 pontos e como resolvê-los adequadamente.
Estou procurando uma explicação elaborada para fins educacionais sobre como identificar cada um dos 4 pontos na hierarquia e a melhor maneira de corrigi-lo.

Nota:
Eu esperava publicar um exemplo de código para as pessoas trabalharem, mas a pergunta em si é sobre como identificar as hierarquias defeituosas :)

Songo
fonte
Há alguns outros exemplos de violações LSP nas respostas a esta pergunta SO
StuartLC

Respostas:

17

É muito mais simples do que essa citação faz parecer, por mais exata que seja.

Quando você olha para uma hierarquia de herança, imagine um método que recebe um objeto da classe base. Agora, pergunte a si mesmo: existem suposições que alguém editando esse método possa fazer que seriam inválidas para essa classe.

Por exemplo ( originalmente visto no site do tio Bob ):

public class Square : Rectangle
{
    public Square(double width) : base(width, width)
    {
    }

    public override double Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Width;
        }
    }

    public override double Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Height;
        }
    }
}

Parece justo, certo? Eu criei um tipo especializado de retângulo chamado Square, que mantém que Width deve ser igual a Height o tempo todo. Um quadrado é um retângulo, então se encaixa nos princípios OO, não é?

Mas espere, e se alguém agora escrever este método:

public void Enlarge(Rectangle rect, double factor)
{
    rect.Width *= factor;
    rect.Height *= factor;
}

Não é legal. Mas não há razão para que o autor desse método saiba que pode haver um problema em potencial.

Sempre que derivar uma classe de outra, pense na classe base e no que as pessoas podem assumir (como "ela tem Largura e Altura e ambas seriam independentes"). Então pense "essas suposições permanecem válidas na minha subclasse?" Caso contrário, repensar seu design.

pdr
fonte
Exemplo muito bom e sutil. +1. O que você pode fazer é transformar o método da classe Rectangle em Ampliar e substituí-lo na classe Square.
marco-Fiset
@ marco-fiset: Eu prefiro ver o Square e o Retangular dissociados, o Square com apenas uma dimensão, mas cada uma implementando IResizable. É verdade que, se houvesse um método Draw, eles seriam semelhantes, mas prefiro que ambos encapsulem uma classe RectangleDrawer, que inclui o código comum.
Pd
1
Eu não acho que este seja um bom exemplo. O problema é que um quadrado não tem largura ou altura. Ele só tem um comprimento de seus lados. O problema não estaria lá se a largura e a altura fossem apenas legíveis, mas elas são graváveis ​​neste caso. Ao introduzir um estado modificável, é sempre muito mais difícil manter o LSP.
SpaceTrucker
@pdr Obrigado pelo exemplo, mas em relação às quatro condições que mencionei no meu post, qual parte da Squareclasse as viola?
Songo
1
@Ongo: é a restrição da história. Melhor explicado aqui: blackwasp.co.uk/LSP.aspx "Por sua natureza, as subclasses incluem todos os métodos e propriedades de suas superclasses. Eles também podem adicionar outros membros. A restrição de histórico diz que membros novos ou modificados não devem modificar o estado de um objeto de uma maneira que não seria permitida pela classe base . Por exemplo, se a classe base representar um objeto com um tamanho fixo, a subclasse não deve permitir que esse tamanho seja modificado. "
Pd