Dev (Back & Front)ARTIGO

Design by contract (DbC) em .NET: comparando soluções

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!

Outro cara que um dia vai mudar o mundo.

Ver perfil