domingo, 28 de janeiro de 2007

Aplicando Design by Contract

No último post sobre DbC, aprofundei seus conceitos e levantei algumas considerações necessárias para sua aplicação. Agora, introduzirei alguns exemplos práticos que permitirão materializar os conceitos vistos e dar início à prática de DbC.

 

DbC em linguagens que suportam nativamente

Em linguagens como EiffelD, existem estruturas na linguagem que permitem especificar formalmente um contrato.

Por exemplo, em D:

 

class Hora
{
    invariant
    {
        assert( _hora >= 0 && _hora <= 23 );
        assert( _minuto >= 0 && _minuto <= 59 );
        assert( _segundo >= 0 && _minuto <= 59 );
    };
 
    this(
      const int hora,
      const int minuto,
      const int segundo
      )
    {
        _hora = hora;
        _minuto = hora;
        _segundo = segundo;
    }
 
    this()
    {
        this( 0, 0, 0 );
    }
 
    ~this()
    {
    }
 
    int hora() { return ( _hora ); }
    void hora(int novaHora) { _hora = novaHora; }
 
    int minuto() { return ( _minuto ); }
    void minuto(int novoMinuto) { _minuto =         novoMinuto; }
 
    int segundo() { return ( _segundo ); }
    void segundo(int novoSegundo) { _segundo =     novoSegundo; } 
 
private:
    int _hora;
    int _minuto;
    int _segundo;
};

 

Aqui, a estrutura invariant definiu que os atributos privados _hora, _minuto e _segundo deverão sempre estar dentro de uma faixa de valores determinada, para todas as instâncias que a classe venha a ter. Enquanto algum método é executado, a verificação do contrato na invariant é desativado. Depois da execução, essas condições serão novamente verificadas.

Estas estruturas de invariáveis somente podem testar o valor de atributos e métodos que não sejam públicos. Para os métodos, caso haja a chamada de um método público dentro de um método privado, a chamada do último se tornará inválida para a invariável, gerando um erro.

Quanto a definição de contratos dentro de métodos, geralmente há uma estrutura como a exemplificada na linguagem D:

 

string Hora::FormatarComoString(
  const string &formato)
{
    in
    {
        // PRE-CONDICOES
        bool formatoValido =
          Formatador::FormatoHoraValido( formato );
  
        assert( formatoValido );
    }
    out
    {
        // POS-CONDICOES
    }
    body
    {
         // IMPLEMENTACAO DO METODO
  
        return ( Formatador::FormatarHora(
          *this, formato ) );
    }
}

A linguagem apresenta três blocos nos quais separa as pré-condições (in), pós-condições (out) e a implementação do método em si (body). Ambos os blocos de pré e pós-condições são opcionais, podendo ser declarados ou não. E ao compilar o programa em modo de liberação (release mode), o código destes dois blocos (ou seja, os contratos estabelecidos para o método) são automaticamente ignorados.

 

DbC em linguagens que não suportam nativamente

Nas demais linguagens, infelizmente estas estruturas não existem e, para implementarmos contratos, temos que usar de recursos específicos de cada linguagem e tentar simular o mesmo funcionamento.

Sobre pré e pós-condições, se a linguagem que estivermos usando possuir uma rotina como assert, podemos simulá-las:

// C++
string Hora::FormatarComoString(
  const string &formato)
{
    // PRE-CONDICOES ..............................
    bool formatoValido =
      Formatador::FormatoHoraValido( formato );
    assert( formatoValido );

    // IMPLEMENTACAO DO METODO ....................
    string resultado = Formatador::FormatarHora(
      *this, formato );

    // POS-CONDICOES ..............................
    // (validaria o resultado, caso preciso)
    // ............................................

    return ( resultado );

}

Repare que, diferente do exemplificado na linguagem D, em C++ tivemos de ter declarar as pós-condições após a implementação do método. O retorno do método só pode ser feito após a validação das pós-condições, caso hajam. Logo, o código deve ser implementado de forma a sempre poder fazer a validação das pós-condições - o que pode se tornar mais difícil quando o método precisa retornar após a validação de condições que não estão presentes no contrato. Neste caso, é preciso desviar a execução do código para o ponto onde as pós-condições são verificadas.

Com as invariáveis, é recomendado colocar as verificações dentro de um método virtual protegido (para poder ser redefinido por classes filhas) e chamar este método após as pós-condições:

// C++
string Hora::FormatarComoString(
  const string &formato)
{
    // PRE-CONDICOES ............................
    bool formatoValido =
      Formatador::FormatoHoraValido( formato );
    assert( formatoValido );

    // IMPLEMENTACAO DO METODO ..................
    string resultado = Formatador::FormatarHora(
      *this, formato );

    // POS-CONDICOES ............................
    // (validaria o resultado, caso preciso)    
       

    // INVARIAVEIS .............................        VerificarInvariaveis();
    // .........................................

    return ( resultado );   

}


// Metodo virtual protegido
void Hora::VerificarInvariaveis() const
{
    assert( _hora >= 0 && _hora <= 23 );
    assert( _minuto >= 0 && _minuto <= 59 );
    assert( _segundo >= 0 && _minuto <= 59 );
}

 

Há o incômodo de ter que acrescentar o método VerificarInvariaveis em cada método da classe. Porém esta é uma forma fácil de acrescentar invariáveis ao código de sua linguagem.

 

Considerações sobre outras possibilidades

Em algumas linguagens é ainda possível usar de outros artifícios para simular o mesmo comportamento de DbC em linguagens como D ou Eiffel. Em C++, por exemplo, há algumas bibliotecas que tentam simular este comportamento através de macros:

Veja #1#2 e #3.

Em Java, há um framework que através de classes de proxy dinâmico, captura definições à la DbC que são declaradas em comentários do código - no formato JavaDoc - e assim possibilitam introduzir fácilmente contratos ao código.

Há diversas opções, dependendo da linguagem escolhida, que possibilitam a introdução de contratos. Vale a pena procurar por formas simples e que contenham boa portabilidade.

Além de bibliotecas e frameworks que simulem DbC, há algumas que simplesmente focam no uso de assertivas. Um bom exemplo é a biblioteca SmartAssert (C++). Ela acrescenta diversas opções em relação ao assert usual e assim facilita muito as coisas para o programador.

Qualquer que seja a linguagem e suas possibilidades, há sempre uma forma de introduzir o conceito de contratos e assim melhorar a qualidade do código escrito.

 

 

Nenhum comentário: