Aqui vamos nós, outra vez. Tudo certin?
Olhando o código do EasyMock, percebi que negligenciei completamente algumas boas práticas de programação defensiva. Percebi que, embora validasse alguns parâmetros, muitas condições inválidas ainda podem ocorrer sem que uma excessão adequada seja lançada. Essa série começa mostrando as técnicas que pretendo adotar que nosso “pequeno” framework tenha um aspecto mais sólido.
Como desenvolver um código sólido? Como garantir que nosso sistema se manterá saudável durante toda sua execução? Uma alternativa para obter algo próximo a isso (programa infalível, ao meu ver, não é um objetivo alcançavel) é pela adoção do paradigma de design por contratos.
Esse post é o primeiro de uma série que se propõe a apresentar a alternativa .net para essa abordagem.
Design by Contracts (DbC), o que é isso?
É uma abordagem para design (e codificação) de programas de computador. Basicamente, assume que serão apontadas pelo desenvolvedor interfaces formais, precisas e verificáveis para todos os componetes de software. Esse comportamento é obtido pela definição de pré-condições, pós-condições e invariantes (definirei mais adiante). A soma dessas especificações é definida como contrato e devem estar em conformidade com as condições e obrigações das regras de negócio.
Por ser uma marca registrada para a Eiffel (detentora dos direitos da linguagem, com o mesmo nome, onde o conceito surgiu), muitas vezes é mencionado como “Programação por Contratos” ou Contract-first development.
Limitação das linguagens de programação para estabelecer um “Contrato”
Como desenvolvedores, escrevemos códigos que “firmam contratos” todos os dias. Observe:
Nosso método simples, em C#, expõe um “contrato”. Se pudesse falar, diria algo mais ou menos assim:
- Prometo aceitar apenas um argumento, um vetor, do tipo string;
- Prometo que retornarei um inteiro como resultado.;
Essas duas afirmações até podem ser vistas como “obrigações contratuais”. Entretanto há algumas “cláusulas” que estão claramente em aberto:
- Nosso método aceitaria uma entrada nula?
- Nosso método aceitaria um array de strings vazias?
- Afinal, quantas strings são esperadas pelo nosso método?
E isso é só o começo! Se quisermos realmente “forçar a barra” poderíamos ver uma série de outras “cláusulas” que não firmamos. Para contornar essa “limitação”, escrevemos um código de verificação. Então, obtemos algo assim:
Chamamos esse tipo de técnica de “programação defensiva”. Checando se o array não é nulo e se ele tem, pelo menos, um elemento, acreditamos que o código restante conseguirá rodar “liso” sem nenhuma Exception. Estes testes permitem que tenhamos certeza de que os valores de entrada atendem o que esperamos. Poderíamos dizer que essas são as “pré-condições” para que o método execute.
Essas “pré-condições” são boas. Código como esse está presente em quase todo código que escrevemos. Mas, entretanto, falta alguma coisa, não acha? Observando a assinatura do método, clientes de nossa biblioteca não têm como saber dessas restrições… Se usássemos Eiffel, teríamos condições de deixar mais claras essas pré-condições. Mas, em C#, definitivamente não… Pelo menos, até agora.
A regra dos três – definindo um contrato
Segundo o DbC, cada método pode falhar em três posições:
- Primeiro, um método pode falhar se for “chamado” com uma entrada inválida (como um null, em um objeto que deve ser especificado);
- Segundo, um método pode falhar se o retorno do método for inválido (a lógica do método está conduzindo a um resultado errado, por exemplo, um valor fora do intervalo 0-255 para um componente de cor)
- Terceiro, por último mas não menos importante, um método pode falhar por deixar o objeto host (onde está implementado) em estádo inválido (exemplo, um count negativo para uma coleção).
Sabendo onde um método falha, como evitar que isso ocorra? A teoria que suporta o DbC nos dá dicas valiosas para indicar onde devemos fazer correções. Observe:
- Se um método obtem uma entrada inválida, geralmente, a fonte do “bug” será o metodo que está chamando;
- Se está sendo retornado um estado inválido, então, a fonte de erro é o próprio método;
- Se, por outro lado, o método recebe entrada válida e está fazendo a saída adequada, mesmo assim, deixa o objeto host em estádo inválido, então a fonte de erro será também o próprio método.
Então, para encontrar bugs… A teoria básica de DbC diz que podemos identificar a origem de um bug, em um dado programa, posicionando assertivas (assertions) em alguns (ou, preferencialmente todos) os três pontes de falha. Ou seja, para “pegar” esses bugs, tudo que precisamos que fazer é escrever assertivas válidas nesses pontos. Se o programa falhar, essas assertivas mostraram claramente isso.
As assertivas que precisamos escrever podem ser:
- pré-condições – destinadas a checar a validade das entradas;
- pós-condições – destinadas a checar a validade das saídas;
- invariantes – destianadas a verificar o estado do objeto.
Ao conjunto dessas assertivas, chamamos contrato.
No post de hoje, foco, intencionalmente, apenas em pré-condições.
A abordagem do .net, Code Contracts
E não é que a Microsoft oferece ferramentas para nos ajudar a programar por contratos?
Todo o modelo de objetos que vou mencionar nesse post está incluído, nativamente, no Framework 4 (e no silverlight 4). Para usar recursos de apoio do Visual Studio, você precisa baixar o conteúdo disponível na página de pesquisa da Microsoft para Code Contracts . Observe que há diferenças conforme a versão do Visual Studio que está sendo utilizado.
Basicamente, Code Contracts é uma biblioteca e um conjunto de ferramentas que apóiam o DbC em linguagens .net. O princípio é simples: Explicitamos as pré-condições, pós-condições e invariantes, e deixamos que o sistema nos ajude em um de dois modos:
- verificação estática – quando o VS checa nosso código e nossos contratos alertando-nos de eventuais desvios (infelizmente, apenas nas versões premium e ultimate). Observe que esses desvios ficam evidentes por verificações que não fizemos, ou ainda por contratos que não estamos obedecendo;
- verificação em runtime – quando nosso programa “executa” o contrato em tempo de execução, gerando Exceptions quando algo “violação” acontece. Seria equivalente a adicionarmos código defensivo no nosso código.
Nesse primeiro post, tratarei apenas da validação estática.
Ativando a verificação estática de contratos no VS 2010
Para ativar a verificação estática de contratos, acesse as propriedades do projeto que deseja verificar e selecione a aba “Code Contracts”
Aqui, definimos como queremos que nosso código seja verificado marcando as opções que desejamos. Onde:
- Perform Static Contract Checking – ativa o verificador estático (mais uma vez, observe que essa funcionalidade está disponível apenas para versões Premium e Ultimate)
- Implicit Non-Null Obligations – faz com que o VS nos recomende a adição, em nossos contratos, de pré-condições para checagem de entradas nulas onde essas parecerem necessárias;
- Implicit Array Bounds Obligations – faz com que o VS nos recomende a adição, em nossos contratos, de pré/pós-condições para checagem de limtes de arrays onde essas parecerem necessárias;
Ative essas três opções, mais a opção para execução em background.
Exemplo prático
Voltando ao nosso pequeno código:
Se você habilitou a verificação estática, nesse momento, está encontrando o seguinte quadro na nossa janela de “erros”:
Olha só! O visual studio está me mostrando o que há de “fraco” no meu código ! É bom esse VS!
Repare que os dois warnings estão nos informando que o código pode cair por causa de uma referência nula e por causa de um possível acesso acima dos limites do vetor.
Além disso, repare que o sistema também recomenda que coloquemos algumas pré-condições. Mais que isso, mostra-nos exatamente como devemos escrever as mesmas. Alteremos então nosso código:
Repare que a adoção de pré-condições foi feita através do método estático Requires da classe Contract (que está em System.Diagnostics.Contracts). Facin e bonitin! Me empolguei com a ideia e também adicionei verificação de nulo para o primeiro elemento .
Ctrl+Shift+B ….
Agora, o sistema nos dá mais uma outra recomendação, observe a primeira linha! Checamos nulo, mas não chegamos string vazio. Ok, respeito, adiciono a verificação!
Ctrl + Shift + B…
Visual Studio começou a “complicar” com nosso método Main. Está recomendando adição de verificações para Args. Entenda, ele está fazendo isso porque explicitamos o contrato do nosso método SayHelloToFirstContact! Podemos até adicionar as verificações que estão sendo recomendadas, mas, isso só faria sentido se ativassemos a verificação em runtime. Por hora, resolvemos a coisa na moda antiga: explicitamos a verificação de args. Observe:
Ctrl+Shift+B… VS feliz
Bem, por hoje, é só isso!
Nelson Bassetto
28/08/2010
Achei bem legal, mas acho que acaba “sujando” o código com todas essas “verificações”. Há um tempo atrás estudei o EntLib e achei block validation bastante interessante. Ele permite que você especifique todas as regras de uma classe entidade através de atributos ou especificado em arquivo de configurações. Fica show de bola! Abraços!
v
31/08/2010
Não gosto muito do Validation do App Block por que se você precisa ter uma regra de validação mais complexa, é necessário criar classes de validação. Ai imagina, se pra cada método fosse necessário criar validadores específicos, ou pior, metade da validação fica por metaatributos e metade dentro do código.
Code Contracts parece a melhor alternativa.