@rodbv

Arquivo para março 23rd, 2009

Em busca de uma arquitetura sólida – Parte 3

fazer um comentário »

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:

Console 2b

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.


Escrito por rodbv

23/03/2009 em 20:39

Publicado em Programação

Um mês de TDD

com um comentário

No mês passado lançamos a nova versão do produto da nossa empresa, Confirmit, e depois de um ou dois dias respirando (ou seja, lendo blogs, experimentando com novas tecnologias) começamos a trabalhar na próxima versão.

Uma das decisões que tomei ao começarmos a versão 15.0(!) do nosso sistema foi adotar TDD (test-driven-development) em período integral. Eu já vinha escrevendo testes unitários há alguns anos, mas sempre escrevendo os testes ao fim do processo, pra checar o que eu já tinha desenvolvido. Com certeza isso é melhor do que nada mas não traz muitos dos benefícios do verdadeiro TDD, aquele em que a gente escreve um teste primeiro (sem código nenhum na aplicação), vê ele falhar e aí sim escreve o código que faz o teste passar, ou seja, o padrão vermelho-verde (o padrão tem esse nome em referência aos ícones vermelho e verde que costuma indicar o fracasso e sucesso da execução dos testes).

Algumas coisas que notei nesse período de 1 mês seguindo TDD:

  • No começo é difícil lembrar de escrever os testes antes: me peguei algumas vezes escrevendo um monte de código nas classes de regras de negócio antes de me tocar que eu deveria antes estar escrevendo testes para essas novas funções. O que fiz nesse caso foi comentar o código, escrever o teste, rodá-lo pra ver o teste falhar e aí retirar os comentários pro teste passar.
  • Minhas funções tornaram-se menores, mais atômicas: como a gente deve testar uma coisa de cada vez, em cada teste, eu me vi escrevendo funções menores que fazem somente o necessário para passar tais testes. Então ao invés de ter uma função que faz 3 coisas, passei a ter 3 funções que fazem 1 coisa, cada uma com um ou mais testes.
  • Passei a pensar mais em termos de API: como os testes são, de certa forma, o primeiro cliente de nossas novas funções, a gente se vê obrigado a pensar mais com o ponto de vista de um desenhista de APIs: funções com nomes mais descritivos, correspondendo aos testes específicos, com parâmetros e valores de retorno mais bem-pensados.
  • ASP.NET Webforms continua sendo horrível pra testar, mas com TDD fica mais fácil: como eu escrevo os testes antes de sequer abrir os projetos Web do ASP.NET, eu tive mais facilidade em realmente deixar toda a lógica de negócios na camada certa, e minhas páginas ASP.NET passaram a fazer mais da única coisa que elas devem fazer, que é chamar funções das camadas de negócio e mostrar os dados na tela. Quando eu não escrevia testes unitários, ou os escrevia a posteriori, era muito fácil acabar deixando minhas páginas ASP.NET “espertas” demais, fazendo coisas demais. Nesse aspecto, o uso do padrão de desenho de repositórios e serviços também têm sido muito úteis, mas isso fica pra outra postagem.
  • TDD é divertido! Essa talvez tenha sido a grande surpresa. Sinceramente eu não me lembro de me sentir tão motivado a começar o dia na frente do computador nos últimos anos. Ao seguir o padrão vermelho-verde eu me vi encarando a programação como uma espécie de jogo, onde eu me coloco uma tarefa/desafio pequena e a resolvo; coloco outra em cima dessa e a resolvo. A gente passa a quebrar os problemas em pedaços menores, mais digeríveis, e com isso a gente se sente menos sobrecarregado. Também é muito legal ver o relatório do servidor de CI ao fim do dia dizendo que todas minhas dezenas de testes passaram, dá uma sensação de dever cumprido. Como muitas vezes a gente leva meses pra ter feedback dos clientes quanto ao nosso trabalho, com TDD eu proporciono feedback instantâneo que me mantém motivado a continuar.
  • Confiança: sempre ouvi dizer isso de TDD, e é verdade: a gente se sente muito mais confiante. Posso fazer um grande refactoring no meu código, envolvendo ou não código de terceiros, e eu sei que, desde que todos os (milhares de) testes estejam verdes, eu não cometi nenhum erro extremamente estúpido. Ajuda muito a dormir tranquilo depois de um check-in envolvendo 100 mudanças de nome de variável ou de provedor de dados.
  • No começo a tentação a “pular” testes vai ser grande, mas persista: não tem muito a adicionar aqui, como toda mudança de hábito, no começo vai rolar uma vontade de dizer “ah não, dessa vez vou mandar ver sem testes, depois eu escrevo” mas a verdade é que a grande chance é que “depois” nunca aconteça, e esse código fique sem testes.

Eu sou meio cético a novas “ondas” que aparecem de vez em quando no mundo da programação, e confesso que quando ouvi falar em TDD há alguns anos também desconfiei que isso seria mais uma dessas ondas. Mas agora eu vejo que não é o caso, eu realmente acredito que, assim como programação OO, TDD veio pra ficar. Faz simplesmente muito sentido, e ao mesmo tempo é algo muito fácil de se adotar. Eu acho que dificilmente contrataria hoje em dia um desenvolvedor que se recusasse a escrever testes unitários. E você?


Escrito por rodbv

23/03/2009 em 20:13

Publicado em ASP.NET, Programação

Etiquetado com

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.