Pílula de Atualização .NET - Code Contracts


Uma das novidades da plataforma .NET Framework 4.0 são os Code Contracts ou Contratos de Códigos.

O que vem a ser este tal de Code Contracts ????

Vamos começar com a definição formal:

"Os Contratos de Códigos permitem a você expressar pré-condições, pós-condições e invariantes de objeto em seu código para verificação em tempo de  execução, análise estática e documentação."

Para podermos entender melhor temos que usar uma terminologia usada em contratos transpondo-a para área de desenvolvimento de software.

O que é uma pré-condição ? (Precondition)

É uma condição que deve ser satisfeita no início da execução de um código.(método, rotina, função,etc.)

O que é uma pós-condição ? (PostCondition)

É uma condição que deve ser garantida ao final da execução do código. (método, rotina, função,etc.)

O que é um Invariante ? (Invariant)

É uma condição que tem que ser verdadeira ao logo do ciclo de vida de um objeto.

Em resumo os Code Contracts da plataforma .NET seguem o princípio do Design by Contract que possuem três princípios que os contratos de código também devem cuidar:

1. Precondition : Refere-se a coisas que ele espera fazer;
2. Postcondition : Refere-se a coisas que ele garante fazer.
3. Invariant : Refere-se a coisas que ele mantém;

  O Code Contracts é o equivalente na plataforma .NET ao Design by Contract que é uma marca registrada da Eiffel Software.

A programação por contrato(Design by Contract) é uma abordagem para projetar software de computador. Ela prevê que os criadores de software devem definir  especificações de interface formal, precisas e verificáveis para componentes de software, que estendam a definição comum de tipos abstratos de dados com pré-condições, pós-condições e invariantes. Estas especificações são chamados de "contratos", de acordo com uma metáfora conceitual com as condições e obrigações dos contratos.

Esse recurso tem o objetivo de permitir ao desenvolvedor uma forma fácil de definir um conjunto de condições ou regras para as classes como, por exemplo, obrigar o preenchimento de suas propriedades.

Além disso, como desenvolvedores temos a obrigação de validar qualquer entrada para um programa a partir de um agente externo. Para isso perdemos não pouco tempo desenvolvendo blocos de validações que verificam uma condição e lançam uma exceção com uma mensagem ao usuário.

Essa codificação pode acabar tornando o nosso código mais verboso e rebuscado tornando-o difícil de ler, entender e mais suscetível a falhas.

Apenas para ilustrar isso veja o código abaixo onde temos um método com 2 parâmetros que nos obrigou a escrever várias linhas de código para validação:

Public Sub testeMacoratti(arg1 As String, arg2 As String)
If arg1 Is Nothing Then
   Throw New ArgumentNullException("Argumento não pode ser nulo, Parameter Name: arg1")
End If
If arg2 Is Nothing Then
    Throw New ArgumentNullException("Argumento não pode ser nulo, Parameter Name: arg2")
End If
If arg1.Trim().Length = 0 OrElse arg2.Trim().Length = 0 Then
    Throw New ArgumentException("Argumento não pode ser vazio ou possuir espaços")
End If
If arg1.Equals(arg2) Then
    Throw New ArgumentOutOfRangeException("Os parâmetros não pode ser iguais, Parameter : arg1, arg2")
End If
'lógica usada para validação
Console.WriteLine("Os objetos não podem ser nulos, vazios nem iguais")
End Sub

Embora possamos mover a validação para outro lugar ainda sim cada método vai exigir a sua própria validação específica e sem perceber lá esta você escrevendo a sua livraria de códigos para validação.

O Code Contracts surge nesse cenário como uma ferramenta que você pode usar para facilitar o seu trabalho e tornar o seu código mais fácil de manter e de testar.

O papel do Code Contracts é escrever o código para você durante a execução nem mesmo tendo uma library padrão para lidar com ele mas também fazendo com que as instruções de validação estejam disponíveis antes de qualquer código.

Uma das grandes vantagens dos Code Contracts é que o seu assembly vai tratar cada uma dessas entradas de validação interna dentro do bloco if e também permitir que você escreva a verificação estática sobre estes durante a compilação.

Além disso através dos Code Contracts podemos :

Obs: Nas versões 2005 e 2008 você podia usar este recurso com o Spec#. Na versão 2010 os recursos estão disponíveis no namespace System.Diagnostics.Contracts

Usando Code Contracts na prática

Vamos abrir o Visual Studio 2010 (eu estou usando a versão Ultimate) e criar um novo projeto do tipo console usando a linguagem Visual Basic chamado CodeContracts;

Antes de continuar você deve verificar se tem o Code Contracts habilitado para o seu projeto.

Faça assim:

1- Clique com o botão direito sobre o nome do projeto e selecione a opção Properties;
2- Na janela de Propriedades localize a aba Code Contracts conforme mostra a figura abaixo:

Conseguiu localizar a aba Code Contracts ?

Se você não achou aba deverá instalar a ferramenta neste link => http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

Essa instalação é necessária para ter a verificação de tipo estática para o seu Visual Studio. Para isso, faça o download do instalador a partir do link DevLabs. Uma vez que você instale com sucesso, basta criar um novo projeto no seu Visual Studio e você poderá verificar a nova aba na seção de propriedades do projeto para configurar o código de seu contrato com MSBuild.

Ao lado temos  o botão para download da ferramenta:

Sugiro que você selecione a versão : Premium Edition

Se desejar aproveite para dar uma olhada na documentação.

Após você fazer o download basta instalar (antes feche o projeto do Visual Studio se ele estiver aberto) para ter o Code Contracts ativo no seu Visual Studio.

Com tudo instalado vamos voltar ao nosso projeto...

Abra o projeto e na janela de propriedades, selecione a aba Code Contracts e marque as opções RunTime Checking e Static Checking conforme mostra a figura abaixo:

Primeiro vamos criar um método usando o código normal idêntico ao usado no início do arquivo.

Abaixo vemos o método testeMacoratti() que recebe dois argumentos e abaixo vemos dois possíveis resultados. O primeiro lançando a exceção e o segundo quando a lógica usada é verificada com sucesso.

Module Module1
    Sub Main()
        Dim nome As String = "Macoratti"
        Dim site As String = "Macoratti.net"
        Try
            testeMacoratti(nome, site)
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try
        Console.ReadKey()
    End Sub
    Public Sub testeMacoratti(arg1 As String, arg2 As String)
        If arg1 Is Nothing Then
            Throw New ArgumentNullException("Argumento não pode ser nulo, Parameter Name: arg1")
        End If
        If arg2 Is Nothing Then
            Throw New ArgumentNullException("Argumento não pode ser nulo, Parameter Name: arg2")
        End If
        If arg1.Trim().Length = 0 OrElse arg2.Trim().Length = 0 Then
            Throw New ArgumentException("Argumento não pode ser vazio ou possuir espaços")
        End If
        If arg1.Equals(arg2) Then
            Throw New ArgumentOutOfRangeException("Os parâmetros não pode ser iguais, Parameter : arg1, arg2")
        End If
        'lógica usada para validação
        Console.WriteLine("Logica Usada ::> Os objetos não podem ser nulos, vazios nem iguais. OK")
        Console.ReadKey()
    End Sub
    
End Module

Vejamos agora como usar o recurso do Code Contracts com base neste exemplo.

Testando uma PreCondition

Lembre-se que uma condição prévia ou pré-condição (PreCondition) significa que algo precisa ser satisfeito antes de ser executado, e portanto se uma PreCondition falha nenhuma código posterior será executado.

No menu Project clique em Add Class e informe o nome TesteContracts.vb, a seguir defina o código abaixo nesta classe:

Imports System.Diagnostics.Contracts

Public Class TesteContracts

    Public Sub InvocarContrato(ByVal arg1 As String, ByVal arg2 As String)
        Contract.Requires(Of ArgumentNullException)(arg1 IsNot Nothing, "Argumento não pode ser nulo, Parameter Name: arg1")
        Contract.Requires(Of ArgumentNullException)(arg2 IsNot Nothing, "Argumento não pode ser nulo, Parameter Name: arg2")
        Contract.Requires(Of ArgumentException)(arg1.Trim().Length > 0 AndAlso arg2.Trim().Length > 0, "Os argumentos não podem ser vazios")
        Contract.Requires(Of ArgumentOutOfRangeException)(Not arg1.Equals(arg2), "Os argumentos não podem ser iguais, Parameter : arg1, arg2")

        'lógica usada para validação
        Console.WriteLine("Logica Usada ::> Os objetos não podem ser nulos, vazios nem iguais. OK")
    End Sub

End Class

Observe que estamos usando o namespace : Imports System.Diagnostics.Contracts

No código se o arg1 e arg2 forem nulos ou forem iguais será lançada uma exceção com uma mensagem customizada.

Note que estamos fazendo uma chamada para o método estático Contract.Requires pois é através deste método que garantimos as pré-condições.

O método Contract.Requires permite que você passe uma condição e uma mensagem de forma seja lançada uma exceção sempre que a condição não for satisfeita.  Esse  método assegura que a exceção ocorra antes, sendo que ela é tratada como uma condição prévia.

Antes de continuar abra a janela de propriedades do projeto e selecione a aba Code Contracts;

Em Assembly Mode marque a opção Standard Contract Requeries conforme mostra a figura abaixo:

Quando estamos testando um condição prévia (PreCondition) que lança uma exceção precisamos ativar a verificação em tempo de execução ; alterando o AssemblyMode para Standard , que reescreve as instruções IL durante a compilação, a exceção vai passar.

A seguir no Módulo defina o código abaixo onde iremos chamar o método InvocarContrato() da classe TesteContracts:

Module Module1
Sub Main()
Dim cc As New TesteContracts
  Try
      cc.InvocarContrato(Nothing, Nothing)
      Console.ReadKey()
  Catch ex As Exception
     Console.WriteLine(ex.Message)
     Console.ReadKey()
  End Try
End Sub
End Module

Executando o código acima teremos:

Observe o resultado: Veja que o compilador nos alerta que um contrato foi violado, no caso uma pré-condição (Precondition failed).

Testando uma PosCondition

Uma pós condição (PostCondition) executa o seu código e lança uma exceção depois de executá-lo com sucesso.

O método Contract.Ensures permite definir uma Pós-Condição. Vamos alterar o código da nossa classe TesteContracts usando este método para testar uma pós-condição conforme abaixo:

Imports System.Diagnostics.Contracts

Public Class TesteContracts

    Public Sub InvocarContrato(ByVal arg1 As String, ByVal arg2 As String)
        Contract.Requires(Of ArgumentNullException)(arg1 IsNot Nothing, "Argumento não pode ser nulo, Parameter Name: arg1")
        Contract.Requires(Of ArgumentNullException)(arg2 IsNot Nothing, "Argumento não pode ser nulo, Parameter Name: arg2")
        Contract.Ensures(arg1.Trim().Length > 0 AndAlso arg2.Trim().Length > 0, "Os argumentos não podem ser vazios")
        Contract.Ensures(Not arg1.Equals(arg2), "Os argumentos não podem ser iguais, Parameter : arg1, arg2")

        'Contract.Requires(Of ArgumentException)(arg1.Trim().Length > 0 AndAlso arg2.Trim().Length > 0, "Os argumentos não podem ser vazios")
        'Contract.Requires(Of ArgumentOutOfRangeException)(Not arg1.Equals(arg2), "Os argumentos não podem ser iguais, Parameter : arg1, arg2")

        'lógica usada para validação
        Console.WriteLine("Logica Usada ::> Os objetos não podem ser nulos, vazios nem iguais. OK")
    End Sub

End Class

Note que eu comentei as duas linhas usadas no exemplo anterior e as substitui por duas linhas onde utilizo o método Ensures.

Vamos agora alterar também o código do nosso Módulo usado para testar o Code Contracts conforme abaixo:

Module Module1
Sub Main()
Dim cc As New TesteContracts
Try
    Console.WriteLine("Informe o argumento 1" & vbNewLine)
    Dim arg1 As String = Console.ReadLine()
    Console.WriteLine("Informe o argumento 2" & vbNewLine)
    Dim arg2 As String = Console.ReadLine()

     cc.InvocarContrato(arg1, arg2)

    Console.ReadKey()

Catch ex As Exception
    Console.WriteLine(ex.Message)
    Console.ReadKey()
End Try
End Sub
End Module

Ao definir a pós condição dissemos que após o método acabar de ser executado os argumentos não podem ser vazios nem iguais.

Executando o projeto e informando dois argumentos iguais iremos obter:

Observe a mensagem gerada após a execução do método indicando que houve uma violação da pós condição (PostCondition failed).

E para encerrar Invariants

Bem, até onde entendemos, uma  pré-condição e uma pós-condição só funcionam quando há uma chamada para o programa. As Invariantes trabalham de uma forma um pouco diferente.

Invariantes são as condições especiais que devem avaliar a verdade antes e após a execução do código. Ou seja uma invariante é uma condição que deve ser satisfeita ao longo de toda a vida de um objeto.

Por isso elas garantem que o objeto seguirá as condições, mesmo que existam subtipos nos quais você sobrescreveu os métodos, para cada bloco do método como propriedades, eventos, e métodos. Dessa forma elas garantem que nunca haverá uma situação para todo o objeto, onde a condição falha.

Para usar invariantes temos que criar um método que infere as condições.

Vamos definir então um classe chamada Login.vb com o seguinte código:

Imports System.Diagnostics.Contracts
Public Class Login
    Public Property Login() As String
        Get
            Return _login
        End Get
        Set(value As String)
            _login = value
        End Set
    End Property
    Private _login As String
    Public Property Senha() As String
        Get
            Return _senha
        End Get
        Set(value As String)
            _senha = value
        End Set
    End Property
    Private _senha As String
    Public Sub AlteraSenha(_novaSenha As String)
        Me.Senha = _novaSenha
    End Sub
    <ContractInvariantMethod()> _
    Private Sub ObjectInvariant()
        Contract.Invariant(Me.Senha IsNot Nothing)
    End Sub
End Class

Aqui você deve observar que  <ContractInvariantMethod> é um atributo especial que designa o método a ser tratado com condições Invariantes. Você não tem permissão para escrever qualquer coisa que não seja um sequência Contract.Invariant neste método e o método deve ser privado.

Assim marcamos o método com um atributo <ContractInvariantMethod> e neste  método definimos o contrato chamando o método Invariant.

Neste exemplo definimos que um objeto do tipo Login não poderá ter uma senha nula.

Para testar vamos incluir o seguinte código no método Main() do módulo:

 Sub Main()
        Console.WriteLine("Testando Invariants" & vbNewLine)
        Console.WriteLine("Vou alterar a senha para nothing para simular o erro" & vbNewLine)
        Try
            Dim login = New Login
            login.AlteraSenha(Nothing)
        Catch ex As Exception
            MsgBox(" Erro : " & ex.Message)
        End Try
        Console.ReadKey()
    End Sub

Executando o projeto iremos obter :

E assim vimos que com Code Contracts podemos separar o que são condicionais e o que são regras e também podemos gerar documentação com as informações do contrato; mas a possibilidade de encontrar chamadas inválidas antes que elas possam causar um erro usando a checagem estática faz de Code Contracts uma ferramenta a ser avaliada e estudada com atenção.

"Em verdade , em verdade vos digo que vem a hora, e agora é, em que os mortos ouvirão a voz do Filho de Deus, e os que a ouvirem viverão."(João-5:25)

Referências: