O Princípio da Substituição de Liskov

Liskov Substitution Principle — LSP

Ricardo Dias
Contexto Delimitado
9 min readOct 19, 2019

--

Conforme o leitor for avançando nessa sequência de artigos, perceberá que os princípios SOLID se complementam e um princípio acaba ajudando o outro a ser cumprido. No último artigo, o Princípio da Responsabilidade Única auxiliou o Princípio Aberto Fechado. O mesmo acontece com o Princípio da Substituição de Liskov, como será explicado a seguir.

Este princípio foi cunhado em 1988 por Barbara Liskov na obra sobre abstração de dados e teoria de tipos (LISKOV 1988). O princípio da substituição foi derivado do conceito de Design by Contracts (DBC), difundido em 1986 por Bertrand Meyer (MEYER 1986).

Um pouco sobre Contratos

Segundo MEYER (1986), a ideia central do Design by Contracts (Design por Contratos) pode ser entendida como uma metáfora, proveniente do mundo dos negócios.

Nos negócios

No âmbito empresarial, um contrato estabelece as obrigações mútuas entre um Cliente e um Fornecedor. Neste contrato, são definidas as obrigações e os benefícios de ambas as partes, como no seguinte exemplo:

  • É obrigação do Cliente pagar o valor;
  • É obrigação do Fornecedor prover o produto ao Cliente;
  • É benefício do Cliente obter o produto;
  • É benefício do Fornecedor receber o pagamento

Os envolvidos devem cumprir suas obrigações, através das regras e regulamentações, aplicadas no contrato.

Na programação

Como já dito, um contrato é uma metáfora e no âmbito do desenvolvimento de software, ele estabelece obrigações mútuas para um Cliente (rotina que utilizará a funcionalidade) e um Fornecedor (classe com funcionalidades).

Se uma classe provê funcionalidade (Fornecedor), ela deve definir uma fórmula preestabelecida (obrigação) de como a rotina utilizadora (Cliente) deve obter a funcionalidade.

Isso evita a utilização da funcionalidade por outros meios que não sejam os estabelecidos no contrato.

Os “benefícios” também devem ser garantidos pelo contrato, de forma que a rotina solicitante (Cliente) obtenha, de fato, a informação solicitada. Dessa maneira, a programação por contratos estabelece que:

  • É obrigação do Cliente implementar o método da maneira estabelecida;
  • É obrigação do Fornecedor prover as formas de se implementar;
  • É benefício do Fornecedor executar a funcionalidade sem imprevistos;
  • É benefício do Cliente obter o resultado desejado

Um contrato pode ser implementado utilizando classes abstratas ou interfaces, pois ambas proveem a possibilidade de impor obrigações.

Observação: a forma de se trabalhar com abstração e imposição de obrigações depende da linguagem de programação utilizada. Linguagens estaticamente tipadas como C#, C++, Java e PHP 7 com type hints dão mais poder para impor obrigações, já em linguagens dinamicamente tipadas essa garantia deve ser feita programaticamente através de asserções dentro do método em questão.

Observe abaixo o trecho de código escrito em PHP onde a classe Operário é obrigada a implementar o método calcularSalário, imposto pela interface IFuncionário:

interface IFuncionário // contrato
{
public function calcularSalário(): float; // assinatura
}
class Operário implements IFuncionário // fornecedor da funcionalidade
{
public function calcularSalário(): float // obrigação
{
//implementação do método
}
}
$objeto = new Operário(); // Cliente

No exemplo acima, de acordo com o Design by Contracts, a interface IFuncionário é chamada de contrato e o método calcularSalário é chamado de assinatura.

Quando a classe Operário for instanciada, se não possuir a implementação correta da assinatura calcularSalário”, uma exceção fatal será disparada e o programa será encerrado. Isso porque a classe Operário tem o contrato IFuncionário” que estabelece as seguintes “obrigações”:

  • o método calcularSalário deve ser implementado;
  • seu valor de retorno deve ser um número flutuante;
  • sua visibilidade deve ser pública.

Dessa forma, sempre que um novo tipo de funcionário precisar ser implementado, o contrato IFuncionário irá garantir que a nova classe estará de acordo com as regras estabelecidas.

O funcionamento do princípio

Uma vez entendido o funcionamento dos contratos, podemos abordar o Princípio da Substituição de Liskov, o qual defende que:

Classes derivadas devem ser substituíveis por suas classes base.

Em outras palavras, um objeto que utilize uma classe base deve continuar a funcionar corretamente quando um objeto derivado dessa classe base for passado para ele.

Segundo LISKOV (1988), “o que se deseja aqui é algo como a seguinte propriedade de substituição: se para cada objeto O1 do tipo S existe um objeto O2 do tipo T, tal que, para todos os programas P definidos em termos de T, o comportamento de P fica inalterado quando O1 é substituído por O2, então S é um subtipo de T.”

A primeira vista parece complicado, mas basicamente LISKOV (1988) está dizendo que se uma classe S herda de uma classe T, então a classe S deve se comportar igual a classe T. Ou seja, a T é a classe base e ela deve responder por todas as classes filhas dela, inclusive a S.

Outra especificação do princípio, define que:

O subtipo deve ser utilizado como seu tipo base sem nenhuma surpresa.

Surpresas são muito boas em outros contextos, mas na programação especificamente é sinônimo de dor de cabeça.

Suponha que uma parte do sistema está utilizando uma determinada funcionalidade. Se essa funcionalidade precisar ser trocada por outra (dinâmica ou estaticamente), a outra deverá devolver o mesmo tipo de informação, caso contrário, o sistema quebrará.

Neste contexto, para garantir que a classe S tenha o mesmo comportamento que a classe base T, é imprescindível a utilização de um contrato (interface ou classe abstrata), contendo as assinaturas (definições de métodos) necessárias para que as classes que a herdarem sejam obrigadas a implementá-las.

Cenário das mil maravilhas

O cenário demonstrado no artigo anterior é um bom exemplo do Princípio da Substituição de Liskov em ação, por isso podemos utilizar o mesmo estudo de caso:

Modelo utilizando três princípios: SRP, OCP, e LSP

Perceba que as classes Operário, Gerente e Vendedor recebem a “obrigação” de implementar o “contrato IFuncionário” que, por sua vez, possui a “assinatura calcularSalário” exigindo o retorno de um número do tipo float.

Qualquer classe que herdar IFuncionário deverá implementar o método calcularSalário para cumprir a exigência da assinatura.

Modelo cumprindo o Princípio da Substituição de Liskov usando interface

Para facilitar o entendimento, observe o código abaixo escrito em PHP, onde o método pagarFuncionário invoca o método calcularSalário e fica esperando um valor do tipo flutuante (float):

class Financeiro
{
public function pagarFuncionário(IFuncionário $cargo)
{
$salárioFloat = $cargo->calcularSalário();

// restante da rotina que usa o
// número flutuante para efetuar o pagamento
...
}
}
$funcionário = new Operário();$transação = new Financeiro();
$transação->pagarFuncionário($funcionário);

No cenário acima, qualquer um dos três objetos derivados de IFuncionário (Operário, Gerente ou Vendedor) irão ser compatíveis com a invocação do método pagarFuncionário, pois calcularSalário sempre será obrigado a retornar um número flutuante.

Da mesma forma, se uma classe qualquer for derivada de Operário, Gerente ou Vendedor, ao fornecê-la para o método pagarFuncionário, o comportamento deverá ser o mesmo, ou seja, retornar um número flutuante. Isso é demonstrado no diagrama abaixo, na implementação da classe Ajudante, que estende a classe Operário:

Modelo cumprindo o Princípio da Substituição de Liskov usando abstração

Tudo funcionará bem, pois as implementações estão cumprindo o Princípio da Substituição de Liskov. Note que ao cumprir este princípio, acaba-se cumprindo também o Princípio Aberto Fechado, abordado no artigo anterior.

Cenário da dor de cabeça

Vamos mudar um pouco o modelo da implementação para criar um cenário mais permissivo, de forma que o leitor possa entender como o princípio pode ser ferido e ter uma noção das consequências desastrosas dessa falha na implementação.

Não será preciso mudar muita coisa! Basta remover a exigência do contrato IFuncionário, ignorando a obrigação de retornar um número flutuante:

Remoção da exigência de retornar um número flutuante na assinatura calcularSalário

Parece inofensivo… até que num belo dia, um programador desavisado decida implementar o método calcularSalário sem o conhecimento exato do que deve ser retornado.

Importante lembrar: na classe Financeiro, o método pagarFuncionário continua esperando um número flutuante para prosseguir com o restante da rotina.

Digamos que um programador deduza, por algum motivo, que na classe Operário, o método calcularSalário deva retornar um valor booleano, ou pior, que deva retornar uma string contendo a frase “O salário do Ricardo deve ser de R$ 16.000”. Neste caso, o programa irá quebrar, pois cálculos matemáticos precisam de números!

Parece um exemplo simplório, mas se o leitor já tiver experiência com programação, saberá que acontece de tudo neste universo :)

No caso acima, imagine se fosse um sistema real: os gerentes e os vendedores iriam receber seus salários normalmente. No entanto, por causa da falha no programa (que nesse caso só aconteceria no dia do pagamento), o pagamento de todos os operários seria comprometido, forçando o pessoal do departamento financeiro a efetuá-lo de forma manual, o que certamente iria gerar bastante transtorno.

O LSP e sua relação com testes

Olhando para os exemplos propostos, parece simples implementar o princípio, no entanto, conforme um programa começa a crescer, isso não fica tão simples.

O grande objetivo de um bom design de código é propiciar a refatoração. Sistemas mudam o tempo todo e flexibilidade é a joia mais preciosa que uma equipe de desenvolvedores pode almejar.

Quando muitas classes herdam de um mesmo contrato, ao precisarmos fazer alguma manutenção que exija uma mudança mais profunda (como a mudança do nome de métodos), teríamos que mudar todas as classes filhas. Isso geralmente causa medo e os programadores preferem deixar do jeito que está.

Um grande aliado nesse cenário é a implementação de Testes de Unidade e Testes de Integração para as classes baseadas em contratos. Isso garante que suas assinaturas sejam cumpridas e facilita muito na hora de mudar as coisas no sistema. Com Testes de Unidade, qualquer alteração errada será rapidamente apontada e, consequentemente, rapidamente corrigida.

Segundo MEYER (1994), um design baseado em contratos facilita a implementação destes testes. O leitor que nunca programou Testes de Unidade, ao começar, não conseguirá mais desenvolver sem eles, por causa da grande facilidade de manutenção que agregam ao projeto.

Enquanto o Teste de Unidade verifica uma classe de forma isolada e certifica que os contratos foram implementados conforme a exigência das assinaturas, o Teste de Integração avalia o funcionamento das classes de forma conjunta e certifica que todas estão operando corretamente entre si.

Além de tudo, os testes dão mais confiança ao programador, que não terá medo de alterar o código na preocupação de quebrar alguma coisa e demorar para descobrir onde ocorreu.

Conclusão

O poder obtido pela substituição de subtipos permite que um módulo, ao implementar um tipo base, seja extensível sem necessidade de modificação. Ou seja, a flexibilidade obtida com o Princípio Aberto Fechado só é realmente possível se o Princípio da Substituição de Liskov for aplicado conjuntamente.

O uso de contratos adiciona grande segurança para o design de um programa, mas deve ser bem compreendido. Os programadores devem aprender a depender implicitamente dos contratos impostos pelo código. Isso irá evitar implementações equivocadas, muito recorrentes em softwares legados.

Além disso, os contratos darão ao programador valiosas pistas de como se deve implementar e o que pode ser utilizado, bastando olhar para o contrato em questão.

Por enquanto é isso. Espero que o conteúdo esteja sendo útil. Até a próxima!

Leia também o próximo artigo sobre o assunto:

Leia todos os artigos desta série:

Referências para Aprofundamento

LISKOV, Barbara. Data Abstraction and Hierarchy. SIGPLAN Notices, 23, 1988. Disponível em <https://pdfs.semanticscholar.org/36be/babeb72287ad9490e1ebab84e7225ad6a9e5.pdf>. Acesso em 05/10/2019.

LISKOV, Barbara; WING, Jeannette. Family Values: A Behavioral Notion of Subtyping. Laboratory for Computer Science Massachusetts Institute of Technology MIT, Cambridge, MA, 1994. Disponível em <https://www.cs.cmu.edu/~wing/publications/LiskovWing94.pdf>. Acesso em 03/08/2019.

MARTIN, Robert C. Design Principles and Design Patterns. Disponível em <https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf>. Acesso em 03/08/2019.

MARTIN, Robert C. Princípios, Padrões e Práticas Ágeis em C#. Bookman, Porto Alegre, RS, 2011.

MARTIN, Robert C. Arquitetura Limpa: O guia do artesão para estrutura e design de software. Alta Books Editora, Rio de Janeiro, RJ, 2019.

Meyer, Bertrand: Design by Contract, Technical Report TR-EI-12/CO, Interactive Software Engineering Inc., 1986

--

--

Ricardo Dias
Contexto Delimitado

Apaixonado por padrões, programação clara, elegante e principalmente manutenível. Trabalha como desenvolvedor deste 2000, incrementando a cada ano este loop…