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