domingo, 10 de dezembro de 2006

Design by Contract (DbC)

João deseja um guarda-roupa sob-medida para o seu quarto, mas teme que um marceneiro inábil não consiga fazê-lo como ele anseia. Então ele resolve deixar tudo anotado no papel para que não haja equívocos e o marceneiro entregue o guarda-roupa como planejado. Assim, ele diminui o risco de problemas e não precisa confiar tanto nas habilidades de improviso do marceneiro.

João então anota tudo e faz uma espécie de contrato onde ele - o cliente - descreve tudo que deseja obter. O marceneiro - seu fornecedor - irá acordar em entregar o pedido sob o preço e tempo determinados.

Assim, foi estabelecido um contrato, uma forma de tentar assegurar que ambas as partes se beneficiem.

Geralmente, vemos nos contratos as seguintes características:

  • Cada parte se beneficiará de alguma forma;
  • Para ter tais benefícios, terão de cumprir certas obrigações;
  • Esses benefícios e obrigações estarão documentados num Documento de Contrato;

O Documento de Contrato protegerá ambos os lados:

  • Ele protege o cliente especificando o quanto deve ser feito: o cliente espera um determinado resultado.
  • Ele protege o fornecedor especificando um mínimo aceitável: o fornecedor está ciente do escopo requisitado.

Aquilo que não está no contrato obviamente não pode ser cobrado ou imposto. Na verdade, nem sempre. Podem haver problemas neste ponto. Sempre há coisas que estão implícitas e que são esperadas por ambos os lados. Por exemplo, João obviamente espera que a porta de seu guarda-roupa feche sem problemas e que a tranca idem, mas isso não foi colocado em seu contrato pois acredita estar implícito na fabricação de qualquer guarda-roupa. Seu marceneiro, por sua vez, espera que o cheque não volte... :)

Como visto, há certas questões que são sempre esperadas de estar contidas num contrato. Seja por práticas comuns do mercado, regras, leis ou quaisquer outras que sejam largamente conhecidas. A questão é que estas regras implícitas são esperadas em todos os contratos e portanto não necessitam ser repetidas em todos eles.

Contratos e escrita de código

A primeira vista pode parecer um pouco estranho, mas toda esta bagagem de contrato é aplicável a escrita de código. Cliente, fornecedor, contrato, benefícios, obrigações e regras implícitas estão permeados em nosso código fonte de uma forma ou outra e é facilmente possível identificá-los, formalizá-los e verificá-los. O melhor de tudo é que esta formalização da escrita de código em espécies de contratos pode nos auxiliar a garantir um ótimo nível de qualidade do código. Ela ajuda a assegurar que o código fará aquilo que desejamos, que poderemos verificá-lo e atestar que cumpre suas obrigações.

Aplicando contratos à escrita de código

Ao escrever código, espressamos a lógica do software em linhas e agrupamos estas linhas em rotinas para ficarem mais fáceis de gerenciar, memorizar (diminuir a carga mental), evitar duplicações, etc. Estas rotinas muitas vezes fazem referências à outras que por sua vez à outras mais, indo de um alto nível de abstração para um baixo nível de abstração, em relação aos detalhes de implementação.

Quando uma rotina chama outra, ela espera um determinado comportamento e resultado desta - e falando em termos de contrato, um determinado benefício. Por sua vez ela tem uma obrigação com a outra rotina, que seria a de passar informações corretas para a mesma.

Então uma rotina A que chama outra, B, espera um benefício ao custo de uma obrigação. Podemos dizer que A, a rotina chamadora, seria o cliente e B, a rotina chamada, seria o fornecedor. Agora, podemos tentar estabelecer o contrato entre as partes criando suas cláusulas.

Como fazemos isso no código ?

Através de afirmações do tipo "assegure que", os asserts. Praticamente toda a linguagem de programação tem uma biblioteca que possui um método capaz de verificar se uma condição está sendo cumprida e caso ela não esteja, interromper a execução do programa informando o ocorrido.

Em C++, por exemplo, há uma biblioteca padrão chamada assert.h onde está definida a função assert. Caso a condição passada para a função assert não seja verdadeira, a execução do programa é interrompida e é informado o local (arquivo) e a linha onde a condição foi testada. Geralmente outras linguagens também possuem uma função assert em uma de suas bibliotecas padrão, ou é facilmente conseguida com terceiros ou mesmo implementada.

O exemplo que irei mostrar estará implementado em C++, mas acredito poder ser facilmente aplicado à qualquer linguagem de programação.

Considere a seguinte rotina:

 

 

A rotina cliente (que irá chamá-la) deve ter por obrigação passar um objeto válido (no caso, um objeto não-nulo). Ela se beneficiará de, passado um objeto válido, ter ele adicionado à uma lista para posterior uso.

A rotina fornecedora (a exemplificada) deve ter por obrigação adicionar um objeto à lista. Ela se beneficiará de não precisar fazer nada quando um objeto for nulo.

Fica então estabelecido um contrato entre as rotinas.

Geralmente, focaremos atenção na rotina fornecedora. Isto porque por ela sabemos também se a rotina cliente cumpriu com suas obrigações.

No caso exemplificado a rotina cliente tem uma pré-condição (obrigação) "objeto deve ser válido" e uma pós-condição (benefício/resultado) "ter o objeto adicionado à lista".

Veja a implementação:

Corpo da rotina

 

Repare que a rotina com contrato tem a seguinte estrutura:

<pré-condições>  // Obrigações da rotina chamadora

<implementação>

<pós-condições> // Garantia dos benefícios para a rotina chamadora

 

Nas pré-condições temos os requerimentos que qualquer rotina que for chamá-la deve satisfazer para estar correta. E nas pós-condições as propriedades que devem ser garantidas para quando houver o retorno (da execução) de volta à rotina chamadora.

Invariáveis

Acabamos de ver como aplicamos um contrato à uma rotina. Pudemos identificar facilmente cliente, fornecedor, obrigações e benefícios de cada uma. No entanto, na introdução sob os contratos, informei que há determinadas regras que estão implícitas e que, apesar de não declaradas no contrato são esperadas de estarem contidas. A estas regras implícitas e imutáveis, damos o nome de invariáveis. Uma invariável, como o próprio nome diz é uma regra que não varia no decorrer do contexto (do programa, ou da classe, enfim). Ela teria de estar presente em todos os contratos, de forma a assegurar que todos eles a tenham cumprido.

Por isso, o desejável é que uma invariável seja declarada somente uma vez e que seja implicitamente presente em todos os contratos (rotinas).

No entanto, somente algumas poucas linguagens atualmente suportam a notação de contratos, como Eiffel e a linguagem D. Essas possuem palavras reservadas (como invariant) que possibilitam a implementação correta de DbC.

Em outras, por sua vez, é necessário um certo trabalho para simular alguns desses conceitos.

Contratos ou tratamento de entradas inválidas

Uma diferença clara que se deve ter em mente ao decidir quando usar um contrato - usando uma cláusula de verificação (assert) - ou quando tratar uma entrada inválida - usando uma expressão condicional (if) - é: determinado valor é esperado ou é um caso especial ?

Por exemplo, suponha que uma rotina espere que um determinado objeto que será usado não seja nulo. Como é esperado que ele não seja nulo, não é aceitável que ele seja. Logo, deve-se usar uma cláusula de verificação para garantir que a rotina receberá o objeto no estado esperado.

Se ao invés disso a rotina fosse projetada para tolerar objetos nulos e tomasse determinada ação de acordo com esta condição, então deveria-se usar uma expressão condicional no lugar.

A decisão vai de encontro com o comportamento desejado em seu código e como ele irá se comportar caso haja condições que vão contra aquilo que é esperado. 

Não se deve misturar as duas coisas. A presença de verificações redundantes aumenta as chances de inconsistência. Se o contrato é preciso e explícito não há necessidade de checagens redundantes.

Conclusão

O uso de contratos no desenvolvimento de software pode levar a:

  • um melhor entendimento das relações entre as rotinas de um programa e do comportamento das rotinas em si;
  • assegurar um mínimo de confiabilidade no código;
  • diminuição de bugs por análise das obrigações e verificações de cada rotina;
  • facilidade de verificação de bugs (os asserts acusam falhas nas condições esperadas);
  • significante aumento da estabilidade (com a previsão e reflexão dos comportamentos do programa, o código é escrito para ser menos suscetível a imprevisibilidades);

Em próximos posts estarei exemplificando a notação de contratos em linguagens que a suportam e que não a suportam. Até breve.

 

Nenhum comentário: