Em busca de uma arquitetura sólida – Parte 3
Continuando nossa série sobre os princípios S.O.L.I.D. de programação orientada a objetos, hoje vamos ver a terceira letra da sopa, L. Caso você tenha acabado de achar esse blog, eu recomendo que leia antes a primeira e segunda partes.
O L em SOLID tem o nome mais esotérico de todos: Liskov Substitution Principle (LSP). Agora você já sabe o que falar amanhã no escritório pra impressionar seus colegas!
O enunciado oficial desse princípio também é meio complicado, pois foi expresso de forma meio matemática:
Se, para cada objeto o1 de tipo S houver um objeto o2 de tipo T tal que para todos programas P definidos em termos de T, o comportamento de P é inalterado quando o1 é substituído por o2, então S é um supertipo de T.
OK, agora vamos traduzir isso pra português:
Um programa que usa uma determinada classe ou interface T deve poder usar objetos derivados de T sem ter conhecimento do tipo concreto desse objeto.
Talvez você já tenha ouvido o conselho de que é melhor programar em cima de abstrações, interfaces ou super-classes, pois isso nos permite maior flexibilidade, além de ajudar bastante na hora de testar o código. Um ótimo conselho, com certeza, mas se o LSP não for seguido, você pode encontrar bugs inesperados ou ser obrigado a escrever código que “cheira mal”, e que muito provavelmente também quebra os dois princípios que vimos antes, SRP e OCP.
Vamos ilustrar com um exemplo. Suponha que estejamos implementando um sistema para um banco, e como a gente sabe que terá que lidar com vários tipos de contas diferentes, vamos criar uma super-classe chamada ContaBase com a funcionalidade que você espera suportar pra qualquer tipo de conta:
public abstract class ContaBase { public decimal SaldoAtual { get; private set; } public DateTime DataAbertura { get; private set; }</p><p style="clear: both"> </p><pre style="clear: both"><code>public virtual void AbreConta(decimal saldoInicial)
{
SaldoAtual = saldoInicial;
DataAbertura = DateTime.Now;
}
public virtual void EfetuaDeposito(decimal valor)
{
SaldoAtual += valor;
}
public virtual void EfetuaSaque(decimal valor)
{
SaldoAtual -= valor;
}
</code></pre><p style="clear: both"> </p><p style="clear: both">}
A primeira classe concreta que você cria é para conta-corrente:
public class ContaCorrente : ContaBase { //nada de especial nessa classe, vamos usar os metodos da superclasse }
Como por enquanto a gente não vê nada de particular com essa classe, vamos deixar essa classe executar o código da super-classe sem mudanças.
Vamos então começar a usar essas classes em nosso código, num programa para o console
class Program { public static void Main(string[] args) { //busca a conta bancaria Console.Write("Entre o numero da nova conta: "); string numeroConta = Console.ReadLine(); ContaBase conta = new ContaCorrente();</p><p style="clear: both"> </p><pre style="clear: both"><code> //abre conta
conta.AbreConta(1000);
//efetua algumas operacoes...
conta.EfetuaDeposito(25);
conta.EfetuaSaque(319);
//mostra o saldo
Console.WriteLine("Saldo atual: " + conta.SaldoAtual);
Console.ReadKey();
}
</code></pre><p style="clear: both"> </p><p style="clear: both">}
Rodando o programa, parece que tudo funciona como esperado:

Ótimo, agora vamos começar a segunda iteração de desenvolvimento, e nosso cliente nos informa que precisa de uma conta especial, de investimento a médio prazo, e essa conta não pode aceitar saques caso tenha menos de 1 ano de criação.
Aqui está nossa nova classe:
public class ContaInvestimento : ContaBase { public override void EfetuaSaque(decimal valor) { if (DateTime.Now.AddYears(-1) < DataAbertura) throw new Exception("Essa conta nao permite saque antes de " + DataAbertura.AddYears(1).ToShortDateString()); } }
Vamos ajustar nosso programa pra lidar com esse tipo de conta. Vamos supor que contas-investimento sempre comecem com dígito 9 (o código que cria contas, claro, deveria estar numa factory ou algo do gênero, mas vamos manter o exemplo simples).
class Program { public static void Main(string[] args) { //busca a conta bancaria Console.Write("Entre o numero da nova conta: "); string numeroConta = Console.ReadLine(); ContaBase conta; if (numeroConta.StartsWith("9")) conta = new ContaInvestimento(); else conta = new ContaCorrente();</p><p style="clear: both"> </p><pre style="clear: both"><code> //abre conta
conta.AbreConta(1000);
//efetua algumas operacoes...
conta.EfetuaDeposito(25);
conta.EfetuaSaque(319);
//mostra o saldo
Console.WriteLine("Saldo atual: " + conta.SaldoAtual);
Console.ReadKey();
}
</code></pre><p style="clear: both"> </p><p style="clear: both">}
Tudo funciona bem com uma conta corrente, mas quando entramos uma conta de investimento, um erro ocorre:

Bem, nosso cliente não fica muito feliz com o “bang” saindo da caixa de som dele, e a gente se apressa em fazer um ajuste ali pela linha 18:
conta.EfetuaDeposito(25); if (conta.GetType() == typeof(ContaInvestimento)) //checar aqui se a conta ja permite saque... conta.EfetuaSaque(319);
Programadores iniciantes (ou distraídos, ou sob pressão) podem dormir com esse tipo de código (e vemos esse tipo de código aos montes), mas dá pra sentir que isso vai nos trazer problemas no futuro. O problema é que apesar de estarmos lidando com um objeto do tipo ContaBase, agora temos que checar se esse objeto é de um tipo específico para podermos efetuar saques. Ou seja, estamos seguindo o princípio de programar em cima de abstrações só pra “inglês ver”. Basta imaginar esse programa ampliado para 10 tipos de conta, cada um com suas particularidades, pra imaginar a dificuldade que vai ser manter esse código. A raiz do problema é que esse programa está violando o LSP: arquitetamos as classes de forma que agora a classe base não pode mais se substituída por qualquer subclasse, pois os resultados variam dependendo do tipo de subclasse que temos em mãos.
A forma mais fácil de detectar violações ao LSP, em C#, é procurarmos por código do tipo if(…typeof…). Um bom programa não deve ter que ficar checando tipos específicos: ou você programa diretamente em cima de uma classe final (logo, já sabe qual o tipo dela), ou se você está programando em cima de super-classes ou interfaces, o tipo não deve importar.
No nosso exemplo, uma forma simples de evitarmos tal problema e seguirmos o LSP seria criar um método PodeEfetuarSaque(), que, no caso de contas-investimento, checaria a data. Outras contas teriam outras regras de negócio, e contas sem qualquer tipo de restrição poderiam simplesmente retornar true. Com isso a regra de negócio volta para o lugar que ela pertence, dentro da classe para a qual ela se aplica. Nosso programa principal simplesmente faz uso de abstrações.
Até a próxima.