DEV Community

Wali Queiroz
Wali Queiroz

Posted on

Princípios SOLID em GoLang - Liskov Substitution Principle (LSP)

Hoje vamos falar sobre o Liskov Substitution Principle. Para ver os artigos anteriores da série acesse:

  1. Princípios SOLID em GoLang - Single Responsability Principle (SRP)
  2. Princípios SOLID em GoLang - Open/Closed Principle (OCP)

Gopher - Liskov Substitution Principle

Dentre os princípios SOLID, o LSP é o que tem a definição formal mais complicada. Por outro lado, é o de mais simples execução, porque o conceito é intuitivo e você acaba aplicando sem muito esforço cognitivo na maioria das vezes.

O princípio foi definido por Barbara Liskov da seguinte forma:

"Se q(x) é uma propriedade demonstrável dos objetos x de tipo T. Então q(y) deve ser verdadeiro para objetos y de tipo S onde S é um subtipo de T."

Complicado, né?

Mas, no fim das contas, essa definição matemática pode ser traduzida na seguinte sentença:

"Se um ObjetoX é uma instância da ClasseX, e um ObjetoY é uma instância da ClasseY, e a ClasseY herda da ClasseX— se usarmos ObjetoY em vez de ObjetoX em algum lugar do código, a funcionalidade não deve ser interrompida."

Como podemos ver, esse é um princípio que parece estar diretamente ligado aos conceitos de classe e herança, e nenhum dos dois está presente no Go. Então como podemos aplicar?

Já vi alguns artigos explicando o LSP em GoLang utilizando os recursos de composição (embedding) da linguagem, mas, do meu ponto de vista, não é uma boa abordagem, pois a composição não permite substituir a estrutura pai pela estrutura filha. Observem:

A definição do princípio leva muitos (eu incluso por bastante tempo) a pensar que se trata de somente de herança, mas na verdade trata-se de subtipagem. Logo, em Go, o LSP é melhor expresso através do uso de interfaces e polimorfismo. Vamos ao próximo exemplo:

Neste cenário, fizemos o método Refuel() na struct ElectricCar lançar um panic indicando que carros elétricos não podem ser abastecidos com gasolina.

Ao chamar a função PerformVehicleActions() com uma instância de ElectricCar, ocorre uma quebra óbvia do princípio de substituição de Liskov. Embora ElectricCar implemente o método Refuel() definido pela interface Vehicle, a implementação específica do carro elétrico interrompe a execução do programa.

No exemplo, vimos a quebra da funcionalidade da interface em vez de seguir a expectativa. E essa é a sacada do LSP em Go: Uma struct não deve violar o propósito da interface.

Poderíamos alterar o design de modo que o cliente do método Refuel() tenha que estar ciente de um possível erro ao chamá-lo. No entanto, isso significaria que os clientes teriam que ter conhecimento especial do comportamento inesperado do subtipo. Isso começa a quebrar o Open/Closed Principle.

Em resumo, toda violação do LSP se torna uma violação do OCP. Então, como corrigir?

Uma possível modificação para respeitar o princípio seria mudar a interface Vehicle para ter um método mais genérico, como Recharge, em vez de Refuel. Assim, cada subtipo pode implementar esse método de acordo com a sua fonte de energia, seja gasolina ou eletricidade.

Outra possível modificação seria criar uma interface separada para os veículos elétricos, como ElectricVehicle, que tenha um método específico para recarregar a bateria, como RechargeBattery. Assim, o ElectricCar implementaria essa interface e não haveria conflito com o método Refuel.

Aqui está um exemplo de código usando a segunda modificação:

Depois disso tudo vocês podem estar pensando: "Ah, Wali, com código de mentirinha é tudo muito fácil, quero ver no mundo real." Então vejamos um exemplo de aplicação do LSP no mudo real:

Neste exemplo, temos a definição da interface Cache, com os métodos Get, Set e Delete, e as implementações RedisCache e MemoryCache.

O UserService utiliza a interface Cache como dependência, permitindo a troca entre o RedisCache e o MemoryCache sem problemas.

No cenário apresentado, o LSP poderia ser quebrado se a implementação específica de algum método de Cache na struct derivada não cumprisse as mesmas garantias e pré-condições definidas pela interface.

Por exemplo, se a implementação do método Get em MemoryCache lançasse um erro diferente ou não respeitasse a garantia de retornar um erro quando a chave não é encontrada, isso quebraria o LSP. Da mesma forma, se a implementação do método Set em RedisCache não armazenasse corretamente os valores no Redis, ou a implementação do método Delete não excluísse corretamente as chaves, isso também violaria o LSP.

Isso é tudo, pessoal! Até a próxima!

Referências:

Top comments (0)