Sumário
Veja o índice completo do tópico “S.O.L.I.D”
Olá pessoas …
Continuando a nossa saga de artigos, vamos agora ver a letra L do acróstico que define a sigla LSP Liskov Substitution Principle (Princípio da Substituição de Liskov).
Definido o LSP
Não tem como falar deste princípio sem saber quem o criou, Barbara Liskov, em 1988.
A definição mais usada, e a mais simples, é:
![]() |
Classes derivadas podem ser substituídas por suas classes de base.
|
![]() |
Certo ou errado?E lá vamos nós novamente ver como fica o princípio ferido, os problemas que nos trazem e a solução do mesmo … |
Ferindo o princípio
![]() |
Isto está errado.Utilitário de atualização de saldos. /* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ using Solid.Errado.LSP.Abstract; namespace Solid.Errado.LSP { public static class AtualizarSaldo { #region Public Methods public static void AtualizarSaldos(params AtualizaSaldoBase[] saldos) { foreach(var saldo in saldos) { if(saldo is AtualizarSaldoAnual) ((AtualizarSaldoAnual)saldo).AtualizarAno(); else if(saldo is AtualizarSaldoMensal) ((AtualizarSaldoMensal)saldo).AtualizarMes(); else if(saldo is AtualizarSaldoDiario) ((AtualizarSaldoDiario)saldo).AtualizarDia(); } } #endregion Public Methods } } Abstração para as classes de saldo concretas /* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ namespace Solid.Errado.LSP.Abstract { public abstract class AtualizaSaldoBase { #region Public Properties public double SaldoAtual { get; set; } #endregion Public Properties } } Cálculo de saldo diário /* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ using Solid.Errado.LSP.Abstract; using System; namespace Solid.Errado.LSP { public class AtualizarSaldoDiario: AtualizaSaldoBase { #region Public Methods public void AtualizarDia() { Console.WriteLine("O saldo DIARIO foi atualizado."); } #endregion Public Methods } } Cálculo de saldo mensal /* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ using Solid.Errado.LSP.Abstract; using System; namespace Solid.Errado.LSP { public class AtualizarSaldoMensal: AtualizaSaldoBase { #region Public Methods public void AtualizarMes() { Console.WriteLine("O saldo MENSAL foi atualizado."); } #endregion Public Methods } } Cálculo de saldo anual /* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ using Solid.Errado.LSP.Abstract; using System; namespace Solid.Errado.LSP { public class AtualizarSaldoAnual: AtualizaSaldoBase { #region Public Methods public void AtualizarAno() { Console.WriteLine("O saldo ANUAL foi atualizado."); } #endregion Public Methods } } |
Problemas ao ferir o princípio
- Primeiro, já matamos o polimorfismo. Na classe de atualização de saldos “public static class AtualizarSaldo” no método “public static void AtualizarSaldos(params AtualizaSaldoBase[] saldos)” temos que fazer “if/else” para determinar que tipo de classe iremos usar para calcular o saldo.
- Na mesma classe citada acima, mesmo que utilizado a herança nas demais classes de atualização de saldo, eu não consigo o reaproveitamento do código de atualização de saldos, uma vez que eu tenho que identificar qual o tipo de classe e determinar o método que é chamado, veja no fragmento abaixo o “if/else”.
if(saldo is AtualizarSaldoAnual) ((AtualizarSaldoAnual)saldo).AtualizarAno(); else if(saldo is AtualizarSaldoMensal) ((AtualizarSaldoMensal)saldo).AtualizarMes(); else if(saldo is AtualizarSaldoDiario) ((AtualizarSaldoDiario)saldo).AtualizarDia();
- Violação do princípio OCP
- Dar manutenção neste código é ter dores de cabeça, uma vez que teremos que lembrar de alterar este método de cálculo de saldos sempre que um novo tipo de saldo for criado.
Corrigindo o princípio
Como o LSP anda de mãos dadas com o OCP, podemos perceber que as classes de saldos definidos no princípio OCP atendem 100% os requisitos deste princípio . Vamos relembrar estas classes:
Analisando as classes definidas acima, podemos perceber que:
- As classes de saldo herdam diretamente da classe “public abstract class AtualizaSaldoBase”;
- Passamos a ter acesso ao método “Atualizar()”, desta forma eu consigo chamar o método “Atualizar()” a partir da classe mais genérica (Veja: Generalização e Especialização), é utilizado o polimorfismo para evitar o uso de “if/ else”, como podemos ver no fragmento abaixo da classe de serviço de atualização de saldo.
public static class AtualizaSaldoService { #region Public Methods public static void AtualizarSaldos(List<AtualizaSaldoBase> saldos) { foreach(AtualizaSaldoBase saldo in saldos) { saldo.Atualizar(); } } #endregion Public Methods }
- Se necessário criar um novo tipo de saldo, basta criar a classe, herdar de “AtualizaSaldoBase” e implementar a chamada onde queremos que o novo saldo seja calculado sem termos que nos preocupar em fazer “if” para identificar o tipo de saldo que iremos calcular;
- A classe mais especializada pode ser facilmente convertida na classe mais genérica e atende ao princípio em questão;
Pegadinha e cuidados
![]() |
Que diabos O patinho feio; história infantil; está fazendo aqui?
Ora! É simples. Se nada como um pato, tem as habilidades de um pato, come igual ao pato mas é um |
Vamos ao exemplo mais clássico de erro de abstração Quadrado x Retângulo. Não poderíamos falar de LSP sem comentar sobre o Quadrado x Retângulo.
Todo quadrado é também um retângulo, pois ambos tem ângulos retos. Mas nem todo retângulo é um quadrado. Quem me disse foi o Professor Procópio do Matemática Rio, no vídeo Qual a Diferença entre Retângulo e Quadrado?
Vamos aos fontes:
Classe de retângulo.
/* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ namespace Solid.Errado.QuadradoRetangulo { public class Retangulo { #region Public Properties public virtual int Altura { get; set; } public virtual int Largura { get; set; } #endregion Public Properties } }
Classe do quadrado que herda de retângulo (Já falei sobre herança?)
/* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ namespace Solid.Errado.QuadradoRetangulo { public class Quadrado: Retangulo { } }
Utilitário para cálculo de área.
/* * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework. */ using Solid.Errado.QuadradoRetangulo; namespace Solid.Errado.Utility { public static class CalculaArea { #region Public Methods //Sim, este método poderia ser da classe retângulo. //Mas para efeito de exemplo, vou deixar ele aqui. public static int CalcularArea(Retangulo retangulo) { return retangulo.Altura * retangulo.Largura; } #endregion Public Methods } }
Chamada de exemplo para ambos os casos
int altura; int largura; Console.WriteLine("\r\nInforme a altura:"); altura = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("Informe a largura:"); largura = Convert.ToInt32(Console.ReadLine()); //aqui, podemos criar um quadrado normalmente. Quadrado quadrado = new Quadrado { Altura = altura, Largura = largura }; //aqui, criamos um retângulo com base em um quadrado, atendendo ao princípio do LSP Retangulo retanguloQuadrado = new Quadrado { Altura = altura, Largura = largura }; //Aqui criamos um retângulo normalmente. Retangulo retangulo = new Retangulo { Altura = altura, Largura = largura }; int quadradoAreaEsperada = altura * altura; int retanguloAreaEsperada = largura * altura; //o método CalcularArea() espera um retângulo, perceba que podemos passar tanto um quadrado quanto um retângulo. Atendendo ao princípio LSP. Console.WriteLine($"A área correta do retângulo é {retanguloAreaEsperada} e a calculada foi {Errado.Utility.CalculaArea.CalcularArea(retangulo)}"); Console.WriteLine($"A área correta do quadrado é {quadradoAreaEsperada} e a calculada foi {Errado.Utility.CalculaArea.CalcularArea(quadrado)}");
Vamos analisar os fontes e explicar o real problema de não tomarmos cuidado no momento de definirmos nossas abstrações.
As classes definidas, a primeira vista, não ferem o princípio LSP, uma vez que podem ser herdadas, terem seus métodos sobrescritos, são reutilizáveis e eu posso criar a mais genérica pela classe mais especializada, posso utilizar a classe mais genérica como parâmetro e passar a mais especializada, como diz o princípio da substituição.
No caso, como foi mostrado, eu posso criar um retângulo a partir de um quadrado. Veja o fragmento abaixo
//aqui, criamos um retângulo com base em um quadrado, atendendo ao princípio do LSP Retangulo retanguloQuadrado = new Quadrado { Altura = altura, Largura = largura };
Mas isso, tem um erro. Imagina que nossos sistema é usado para calcular as esquadrias em um prédio. E neste sistema eu preciso calcular a área para pedir que se corte uma esquadria quadrada. considerando que o quadrado tem seus lados iguais, e meu sistema aceita que eu passe altura x largura diferentes, mesmo para a classe “Quadrado”, eu tenho um erro de regra de negócio e de abstração.
Se eu crio uma classe retângulo a partir de uma classe “Quadrado” a área calculada ficará errada se eu passar os valores de “Altura” e “Largura” diferentes.
//aqui, criamos um retângulo com base em um quadrado, atendendo ao princípio do LSP Retangulo retanguloQuadrado = new Quadrado { Altura = 10, Largura = 5 }; //aqui acontece o erro, uma vez que eu passei 10x5=50. O meu calculo de área deveria considerar Altura X Largura igual para o calculo de quadrados. Console.WriteLine($"A área correta do quadrado é {Errado.Utility.CalculaArea.CalcularArea(retanguloQuadrado)}");
Pegando um exemplo real, nos artigos anteriores temos tratado as nossas classes de saldos como saldos de estoque. Imaginem que eu queira calcular o saldo financeiro. Em sua essência o cálculo é igual um menos o outro, entradas menos saídas. Só que de um lado eu estou falando de produtos, e de outro de valores monetários, cada qual com suas regras específicas.
Para terminar
Para atender ao princípio de Liskov, temos que garantir que as nossa classes genéricas, sejam substituíveis pelas nossas classes especializadas; Veja: Generalização x Especialização; Desta forma atendemos também o OCP.
Cuidado ao usar o princípio “É UM”, e preste muita atenção que este princípio se aplica ao comportamento da classe e não aos tipos ou subtipos. Valorize o polimorfismo.
![]() |
Para saber mais. |
O fonte utilizado aqui pode ser baixado pelo GITHub em https://github.com/desenvolvedores-net/ArtigoSOLID
É isso ai pessoal 🙂
Até o próximo
♦ Marcelo