Olá, pessoal! No meu primeiro artigo sobre Design By Contract, recebi questionamentos interessantes sobre os prós e contras dessa
abordagem, comparando-a com a forma tradicional de validações com
if?throw. Logo, o objetivo deste texto será evidenciar as diferenças
entre essas duas abordagens.
No último artigo lancei um desafio ao meu amigo Gustavo Badke (@guripunk) após ele ter escrito o seguinte comentário: “Pra min isso é firula, não vejo diferença nenhum do if(?) throw new Exception?“.
Alguns códigos que vou apresentar aqui foram escritos por ele, então,
considerem-no como uma participação especial nesse artigo. Vamos lá:
Pré-condições
Tradicionalmente, quando precisamos verificar se os parâmetros de um
método qualquer estão válidos para utilização, escrevemos algo
semelhante ao código abaixo:
public Cor(int pVermelho, int pVerde, int pAzul)<br />{<br /> if (pVermelho < 0 || pVermelho > 255)<br /> throw new ArgumentOutOfRangeException("O argumento 'pVermelho' deve ser entre 0 e 255");<br /> if (pVerde < 0 || pVerde > 255)<br /> throw new ArgumentOutOfRangeException("O argumento 'pVerde' deve ser entre 0 e 255");<br /> if (pAzul < 0 || pAzul > 255)<br /> throw new ArgumentOutOfRangeException("O argumento 'pAzul' deve ser entre 0 e 255");<br /> <br /> // Implementação...<br />}A abordagem acima resolve o nosso requisito de validação dos parâmetros, porém, o código utilizado para isso é extremamente “carregado” para a implementação de um requisito simples. Utilizando DbC, a implementação ficaria da seguinte forma:
public Cor(int pVermelho, int pVerde, int pAzul)<br />{<br /> Contract.Requires<ArgumentOutOfRangeException>(pVermelho >= 0 && pVermelho <= 255);<br /> Contract.Requires<ArgumentOutOfRangeException>(pVerde >= 0 && pVerde <= 255);<br /> Contract.Requires<ArgumentOutOfRangeException>(pAzul >= 0 && pAzul <= 255);<br /> <br /> // Implementação...<br />}A abordagem acima não só resolve o nosso requisito de validação dos
parâmetros, como também deixa o código mais legível e você ainda conta
com a validação dos contratos em tempo de compilacão. O método Requires
ainda possibilita a configuração de uma mensagem personalizada caso
seja necessário.
Também poderíamos usar AOP para
resolver nosso problema, contudo, seria utilizar um recurso muito
poderoso e complexo para uma necessidade muito simples. Se você não vai
usar AOP para nenhuma outra necessidade do seu projeto, não recomendo utilizar nessa situação.
Não devemos avaliar o poder do DbC analisando somente suas
pré-condições. Precisamos avaliar se todos os aspectos dessa abordagem
serão realmente úteis no nosso projeto, isso inclui também as
pós-condições e invariantes.
Pós-condições
É comum, após a execução de um método, realizar a verificação de
estado da classe nos membros que foram afetados pelo mesmo. Nessa
situação, usando a abordagem tradicional, faríamos uma verificação
qualquer no final do corpo do método. Algo como mostra o código abaixo:
public void AdicionarVermelho(int pValor)<br />{<br /> this.Vermelho += pValor;<br /> <br /> if (this.Vermelho > 255)<br /> throw new Exception("A propriedade Vermelho não deve ser maior que 255");<br />}Um dos principais problemas nessa abordagem é a falta de legibilidade no método. Veja a solução usando DbC:
public void AdicionarVermelho(int pValor)<br />{<br /> Contract.Ensures(this.Vermelho <= 255);<br /> <br /> this.Vermelho += pValor;<br />}Quando trabalhamos com contratos, sempre inserimos os mesmos no topo
dos métodos, o que facilita muito a leitura dos códigos. Mesmo com
exemplos simples como esses, podemos perceber que a curva de
aprendizagem é muito baixa.
Invariantes
Como você garantiria que um objeto estará válido sempre, independentemente
de quais e quantos métodos forem executados? Provavelmente, você
precisará construir algum método de validação e chamá-lo ao final de
cada método público de sua classe. Nesse ponto, percebemos uma grande
diferença entre as abordagens, pois usando DbC, só precisaríamos
construir o método Invariant com as validações necessárias e pronto,
vejam o exemplo:
[ContractInvariantMethod]<br />private void Invariant()<br />{<br /> Contract.Invariant(this.Vermelho <= 255);<br />}Em resumo, como disse anteriormente, é necessário analisar todos os
recursos da abordagem e não somente um. Apesar das pré-condições serem
facilmente substituídas por if?throw, isso não acontece da mesma forma
com as invariantes.
DbC não é para todos os projetos, DbC não é regra, DbC não é melhor
ou pior que ninguém. Use em seus projetos quando precisar de todos os
recursos que a abordagem dispõe, e mesmo assim não deixe de analisar
alternativas como AOP.
Abraços!







