SOLID: Um assunto necessário | Parte 3 — Princípio de Substituição de Liskov

Aline Souza
Aline Souza
Published in
4 min readMar 3, 2021

Chegamos em um dos, se não o, princípio que mais causa confusão em seu entendimento. O princípio de substituição de Liskov, na teoria, diz que:

Seja q(x) uma propriedade que se pode provar do objeto x do tipo T. Então, q(y) também é possível provar para o objeto y do tipo S, sendo S um subtipo de T.

Ou seja, este princípio, como a própria definição diz, define que sendo uma classe B uma extensão de A, então, presume-se que se eu substituir a classe A por B, o comportamento esperado é o mesmo. Ainda ficou confuso, né? Mas calma que vou tentar clarear.

A gente aprende nos livros e na faculdade que se temos comportamentos em comum, logo, uma classe pode estender da outra. O problema desse tipo de conceito é que acabamos usando a herança puramente para aproveitamento de código, para não ter que implementar novamente os mesmos comportamentos.

Porém, se formos considerar o princípio de substituição de Liskov, esse tipo de pensamento é incorreto, ao menos na visão de orientação a objetos. Por exemplo, nesse caso:

Digamos que eu tenha uma classe “cachorro”. Um cachorro faz o quê? Late, anda, abana o rabo, por exemplo. E agora eu quero construir uma classe de cachorro de brinquedo. Olha só! Por quê não utilizar a classe cachorro como pai e estender dela? Até por que, teremos que implementar os mesmos métodos, correto? NÃO.

O problema, é que apesar da extrema semelhança, um cachorro de brinquedo precisa de pilhas para funcionar. Ou seja, antes de você chamar a função “andar”, por exemplo, terá que checar se ele possui bateria, certo?

Agora vamos tentar aplicar o princípio da substituição. Conseguimos substituir a classe cachorro? Não. Pois assim que precisarmos que o cachorro “ande”, ele vai checar se tem pilha, ou seja, emitirá um erro, se formos considerar que um cachorro de verdade não usa pilha.

Vamos ver este exemplo codificado?

Como dito, vamos considerar a classe "Dog" abaixo:

open class Dog {
private var state: DogState = DogState.STOPPED

open fun walk() {
state = DogState.WALKING
}

open fun bark() {
state = DogState.BARKING
}
}
// Fiz um ENUM simples com os possíveis estados do cachorro
enum class DogState {
STOPPED,
WALKING,
RUNNING,
LYING,
BARKING
}

E agora, vamos criar uma classe chamada ToyDog() para estender da classe Dog():

class ToyDog(
private val checkBatteryUseCase: CheckBatteryUseCase
) : Dog() {

private var state: DogState = DogState.STOPPED
override fun walk() {
if(checkBatteryUseCase()) {
state = DogState.WALKING
} else {
Timber.d("THIS TOY HASN'T BATTERY")
}
}

override fun bark() {
if(checkBatteryUseCase()) {
state = DogState.BARKING
} else {
Timber.d("THIS TOY HASN'T BATTERY")
}
}
}

Certo, vamos por parte:

class ToyDog(
private val checkBatteryUseCase: CheckBatteryUseCase
) : Dog()

Neste trecho, eu criei a nova classe chamada ToyDog() estendo de Dog(). Mas perceba que eu injetei um UseCase no meu construtor. Por qual motivo? Pois eu não quero atribuir a responsabilidade à classe ToyDog() de saber como/onde checar se possui ou não bateria. Ou seja, eu só quero saber se tem ou não bateria. De onde o UseCase vai tirar essa informação, não me importa (se será de uma API, de um SharedPreferences ou de um banco de dados local, por exemplo).

Já nesse ponto abaixo, eu fiz um override dos métodos de ação da classe pai Dog(). Por qual motivo? Pois agora, eu não posso simplesmente mudar o estado do cachorro para "walking", por exemplo. Afinal, antes de fazer um cachorro de brinquedo andar, temos que saber se tem bateria/pilha para isso.

override fun walk() {
if(checkBatteryUseCase()) {
state = DogState.WALKING
} else {
Timber.d("THIS TOY HASN'T BATTERY")
}
}

Perceba que, se não tiver bateria/pilha, estou emitindo um erro específico.

Aparentemente, nada de errado nessa minha implementação.

Agora façamos o teste: Nos pontos em que eu tiver chamado a classe Dog(), por algum motivo, eu necessite substituir pela classe ToyDog(). O que aconteceria? Inicialmente, nada. Mas assim que eu chamasse uma das funções (walk() por exemplo), eu bateria no meu UseCase para checar se há bateria. Mas como trata-se de um cachorro real, eu receberia uma resposta negativa (sendo como FALSE, ou um erro).

Deu pra perceber que apesar de ambos terem as mesmas funções, não é possível substituir a classe pai Dog() pela classe filha ToyDog() e manter o mesmo comportamento?

Ou seja, esse tipo de herança nunca deveria ter sido feita!

Devemos evitar associar herança com reaproveitamento de código, por puro comodismo. Sempre que ficar na dúvida se aquela classe deve ou não estender de outra, pense: Eu consigo substituir a classe filha pela pai, sem haver algum tipo de modificação de comportamento? Se a resposta for sim, use herança.

Se a resposta for não, como no exemplo acima em que modificamos o comportamento, então não faz sentido usar herança.

Sim, eu sei que isso vai contra tudo que aprendemos desde a época da faculdade e em livros, de que se possui comportamentos em comum (como no caso dos cachorros acima, que ambos andam e latem), mas lembre-se que orientação a objetos vai muito além de reaproveitamento de código.

Neste momento, estamos aliando o poderoso uso de orientação a objetos com os princípios SOLID, que buscam te ajudar a melhorar a legibilidade do seu código, manutenibilidade, testabilidade e flexibilidade de expandir seu código sem grandes alterações.

Escrever menos código não significa ter o melhor código!

Nota: O código acima é totalmente didático.

Lembrando que esta é uma série de 5 postagens em que abordo os princípios de SOLID de forma mais objetiva possível. Perdeu os 2 primeiros? Veja aqui:

Parte 1: Princípio da Responsabilidade Única

Parte 2: Princípio do Aberto/Fechado

Me siga no LinkedIn para ficar por dentro dos próximos!

Code Like a Girl 👧

--

--

Aline Souza
Aline Souza

Desenvolvedora Android, apaixonada por tecnologia, e aprendendo todo dia um pouco mais! Code like a girl :)