Posts SOLID - Liskov Substitution Principle
Post
Cancel

SOLID - Liskov Substitution Principle

LSP: Liskov Substitution Principle

O nome "Liskov" vem de "Barbara Liskov", a autora da famosa frase que ajuda a definir subtipos na orientação a objetos.

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

A frase é um pouco complexa, mas podemos fragmentar para entender seu sentido. Faremos um exercício em java.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TypeS extends TypeT {
  // Construtor, atributos, etc
}

public class TypeT {
  // Construtor, atributos, etc
}

public class TypeP {
  private TypeT obj;

  public TypeP(TypeT obj) {
    this.obj = obj;
  }

  public void doSomething() {
    // faz algo usando obj
  }
}

public class Main {
  public static void main(String[] args) {
    TypeS o1 = new TypeS("string");
    TypeT o2 = new TypeT("string");
    
    TypeP pType1 = new TypeP(o1);
    TypeP pType2 = new TypeP(o2);
    
    // Os dois métodos abaixo deveriam ter o mesmo comportamento, pois,
    // a classe TypeS herda da classe TypeT.
    pType1.doSomething();
    pType2.doSomething();
  }
}

Com o exemplo acima, conseguimos entender que, devemos garantir que subtipos devem ser intercambiáveis e não alterar o comportamento de quem usa os subtipos.

SRP_example_violation

Exemplo de violação de LSP

Para ficar mais claro, usaremos como exemplo o famigerado problema do "Retângulo".

Abaixo classe que define o comportamento para cálculo de area de um retângulo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Rectangle {

  private double width;
  private double height;

  public double getWidth() {
    return width;
  }

  public void setWidth(double width) {
    this.width = width;
  }

  public double getHeight() {
    return height;
  }

  public void setHeight(double height) {
    this.height = height;
  }

  public Double getArea() {
    return width * height;
  }
}

Agora, iremos propositamente violar LSP e definer a classe Square (quadrado) herdando de Rectangle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Square extends Rectangle {

  @Override
  public void setWidth(double width) {
    super.setWidth(width);
    super.setHeight(width);
  }

  @Override
  public void setHeight(double height) {
    super.setHeight(height);
    super.setWidth(height);
  }
}

Para utilização da classe, vemos que não é possível substituir o Square pelo Rectangle sem o assert falhar, pois, um quadrado possui todos os lados iguais, ao contrário do retângulo.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
  public static void main(String[] args) {
    Rectangle rec = new Rectangle();
    rec.setWidth(12);
    rec.setHeight(22);
    assert rec.getArea() == 264.0 : "Wrong Value";

    Rectangle square = new Square();
    square.setWidth(12);
    square.setHeight(22);
    assert square.getArea() == 264.0 : "Wrong Value"; // irá falhar
  }
}

A falha no segundo assert poderia acontecer com qualquer cliente da classe, pois, além de ser possível passar um Square para um programa que espera Rectangle, não é claro que quando setamos os lados individualmente, todos os lados serão iguais, pois pode se tratar de um quadrado.

A quabre do LSP ocorre devido ao comportamento inesperado da classe Square, que não obedece de forma concisa o contrato definido na classe base (Rectangle), onde o setWidth e setHeight sobrescrevem um único atributo referente a todos os lados de um quadrado.

Solução

Para resolver o problema do quadrado e não confundir o cliente da classe, deveríamos subir o nível de abstração de Retângulo e Quadrado. Ambos são "formas", portanto, as duas poderiam herdar de uma classe Shape (forma).

1
2
3
public interface Shape <V> {
  V getArea();
}

Utilizando uma interface com um único método, definimos um comportamento para Shape de obter a area. Usando Generics, deixamos aberta a implementação do tipo, podendo trocar entre tipos Int e Float (e seus derivados).

Implementação de Rectangle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Rectangle implements Shape<Double> {

  private double width;
  private double height;

  public double getWidth() {
    return width;
  }

  public void setWidth(double width) {
    this.width = width;
  }

  public double getHeight() {
    return height;
  }

  public void setHeight(double height) {
    this.height = height;
  }

  public Double getArea() {
    return width * height;
  }
}

Implementação de Square

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Square implements Shape<Double> {

  private double side;

  public void setSide(double side) {
    this.side = side;
  }

  @Override
  public Double getArea() {
    return Math.pow(side, 2);
  }
}

Utilização de Square e Rectangle

Com essa alteração, estamos deixando claro que tanto Square e Rectangle são formas e sua utilização depende de implementação. O cliente da classe não precisa saber os detalhes de implementação, apenas que Shape tem o método getArea.

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
  public static void main(String[] args) {
    Rectangle rec = new Rectangle();
    rec.setWidth(12);
    rec.setHeight(22);
    assert(rec.getArea() == 264.0) : "Wrong Value";

    Square square = new Square();
    square.setSide(12);
    assert square.getArea() == 144.0 : "Wrong Value";
  }
}

SRP_example_rightimpl

Conclusão

Embora LSP seja um pouco mais difícil de compreender, seu entendimento, aliado ao entendimento dos pilares de POO, pode trazer diversos benefícios a manutenibilidade a longo prazo, como em uma utilização mais claro e concisa de classes e modulos. Esse conceito pode ser elevados ao nível arquitetural. Em um contexto de API Rest, por exemplo, a hierarquia de URL deve ser seguida de modo a evitar confusões e exceções em seu uso.

This post is licensed under CC BY 4.0 by the author.