Cutting Edge

Invariáveis e herança em contratos de código

Dino Esposito

Dino EspositoNas edições passadas desta coluna, expliquei dois dos tipos mais comuns de contratos de software — pré-condições e pós-condições — e analisei sua sintaxe e semântica da perspectiva da API de contratos de código no Microsoft .NET Framework 4. Neste mês, apresentarei primeiramente o terceiro tipo mais importante de contrato — o invariável — e, então, passarei a examinar o comportamento das classes baseadas em contrato quando a herança é aplicada.

Invariáveis

Em termos gerais, uma invariável é uma condição que sempre permanece verdadeira em um determinado contexto. Aplicada a um software orientado a objetos, uma invariável indica uma condição que sempre é avaliada como verdadeira em cada instância de uma classe. A invariável é uma ferramenta formidável que informa prontamente se o estado de qualquer instância de uma determinada classe se torna inválido. Colocando de outra forma, um contrato invariável define formalmente as condições nas quais uma instância de uma classe é avaliada como em bom estado. Por mais exagerado que possa parecer, esse é apenas o primeiro conceito que você precisa compreender inicialmente, e depois implementar, ao modelar um domínio de negócios através de classes. O design orientado a domínio (DDD) é agora a tecnologia comprovada para modelos de cenários de negócios complexos e atribui à lógica invariável uma posição de importância no design. O DDD, de fato, recomenda firmemente que você nunca use instâncias de classes que estejam em um estado inválido. Da mesma maneira, o DDD recomenda que você escreva alocadores de suas classes que retornem objetos em um estado válido e, também, que seus objetos retornem a um estado válido após cada operação.

O DDD é uma metodologia que deixa a implementação real dos contratos para você. No .NET Framework 4, os contratos de código ajudam muito a realizar uma implementação bem-sucedida, com um mínimo de esforço da sua parte. Vamos saber mais sobre as invariáveis no .NET Framework 4.

O que é a lógica invariável?

As invariáveis, de alguma maneira, estão relacionadas à boa modelagem de objetos e ao design de software? Uma profunda compreensão do domínio é essencial. Essa compreensão do domínio leva você naturalmente a encontrar suas invariáveis. E algumas classes simplesmente não precisam de invariáveis. A falta de invariáveis não é um sintoma alarmante por si só. Uma classe que não tenha restrições sobre o que se espera que contenha e sobre o que se espera que faça simplesmente não tem condições invariáveis. Se esse for o resultado de sua análise, ótimo.

Imagine uma classe que representa notícias a serem publicadas. A classe provavelmente tem um título, um resumo e uma data de publicação. Onde estão as invariáveis? Bem, isso depende do domínio de negócios. A data da publicação é necessária? Se sim, você deve garantir que as notícias sempre tenham uma data válida, e a definição do que é uma "data válida" também faz parte do contexto. Se a data for opcional, então você pode economizar uma invariável e garantir que o conteúdo da propriedade seja validado antes do uso no contexto no qual ela está sendo usada. O mesmo pode ser dito sobre o título e o resumo. Faz sentido ter notícias sem título ou conteúdo? Se isso fizer sentido no cenário de negócios que você está considerando, você terá uma classe sem invariáveis. Caso contrário, esteja pronto para incluir verificações contra a falta de um título e de conteúdo.

De maneira mais geral, uma classe sem comportamento e funcionando como um contêiner de dados levemente relacionados provavelmente não tem invariáveis. Na dúvida, sugiro que se pergunte “posso armazenar algum valor aqui?” para cada propriedade da classe, seja pública, protegida ou privada, se definida por meio de métodos. Isso deve ajudar a compreender claramente se você está ignorando pontos importantes do modelo.

Como muitos outros aspectos do design, a descoberta de invariáveis será mais proveitosa se feita no início do processo de design. Adicionar uma invariável posteriormente no processo de desenvolvimento é sempre possível, mas lhe custará mais em termos de refatoração. Se você fizer isso, tenha cuidado com a regressão.

Invariáveis nos contratos de código

No .NET Framework 4, um contrato invariável de uma classe é o conjunto das condições que devem ser sempre verdadeiras para qualquer instância da classe. Ao adicionar contratos a uma classe, as pré-condições são para a descoberta de bugs no chamador da classe, enquanto as pós-condições e invariáveis estão relacionadas à descoberta de bugs em sua classe e suas subclasses.

O contrato invariável é definido por meio de um ou mais métodos ad hoc. Esses são métodos de instância, privados, retornando voids e ‘enfeitados’ com um atributo especial — o ContractInvariantMethod. Além disso, os métodos invariáveis não podem conter código além das chamadas necessárias para definir condições invariáveis. Por exemplo, você não pode adicionar qualquer tipo de lógica aos métodos invariáveis, seja pura ou não. Você não pode nem mesmo adicionar lógica para simplesmente registrar o estado da classe. Eis como definir um contrato invariável de uma classe:

public class News {
  public String Title {get; set;}   
  public String Body {get; set;}

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(!String.IsNullOrEmpty(Title));
    Contract.Invariant(!String.IsNullOrEmpty(Body));
  }
}

A classe News coloca condições invariáveis de que Title e Body nunca sejam nulas ou vazias. Observe que, para esse código funcionar, você precisa ativar a verificação completa no momento da execução na configuração do projeto para as várias compilações, conforme apropriado (consulte a Figura 1).

Invariants Require Full Runtime Checking for Contracts

Figura 1 As invariáveis requerem verificação completa no momento da execução para os contratos

Agora experimente o seguinte código simples:

var n = new News();

Você ficará surpreso em receber uma exceção de falha do contrato. Você criou com sucesso uma nova instância da classe News; mas, por azar, ela está em um estado inválido. Uma nova perspectiva de invariáveis é necessária.

No DDD, as invariáveis estão associadas ao conceito de um alocador. Um alocador é apenas um método público responsável pela criação de instâncias de uma classe. No DDD, cada alocador é responsável por retornar instâncias de entidades de domínio em um estado válido. O resultado é que, ao usar invariáveis, você deve garantir que as condições sejam atendidas em um determinado momento.

Mas qual é esse momento? O DDD e a implementação real dos contratos de código concordam que as invariáveis sejam verificadas na saída de qualquer método público, incluindo construtores e setters. A Figura 2 mostra uma versão revisada da classe News, que adiciona um construtor. Um alocador é igual a um construtor, exceto pelo fato de que, sendo um método estático, ele pode receber um nome personalizado e sensível ao contexto e resultar em um código mais legível.

Figura 2 Invariáveis e construtores com reconhecimento de invariáveis

public class News
{
  public News(String title, String body)
  {
    Contract.Requires<ArgumentException>(
      !String.IsNullOrEmpty(title));
    Contract.Requires<ArgumentException>(
      !String.IsNullOrEmpty(body));

    Title = title;
    Body = body;
  }

  public String Title { get; set; }
  public String Body { get; set; }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(!String.IsNullOrEmpty(Title));
    Contract.Invariant(!String.IsNullOrEmpty(Body));
  }
}

O código que consome a classe News pode ser o seguinte:

var n = new News("Title", "This is the news");

Esse código não emitirá nenhuma exceção, pois a instância é criada e retornada a um estado que atenda às invariáveis. E se você adicionar a seguinte linha:

var n = new News("Title", "This is the news");
n.Title = "";

Definir a propriedade Title como uma cadeia de caracteres vazia coloca o objeto em um estado inválido. Como as invariáveis são verificadas na saída dos métodos públicos — e os setters de propriedade são métodos públicos — você receberá uma exceção novamente. É interessante observar que, se você usar campos públicos em vez de propriedades públicas, as invariáveis não serão verificadas e o código será executado sem problemas. Seu objeto, entretanto, está em um estado inválido.

Observe que ter objetos em um estado inválido não é necessariamente uma fonte de problemas. Em grandes sistemas, entretanto, em nome da segurança, você deve gerenciar as coisas de modo a obter exceções automaticamente se um estado inválido for detectado. Isso ajuda a gerenciar o desenvolvimento e a conduzir seus testes. Em pequenos aplicativos, as invariáveis podem não ser uma necessidade, mesmo quando algumas resultarem de análise.

Apesar de as variáveis precisarem ser verificadas na saída de um método público, dentro do corpo de qualquer método público um estado pode estar temporariamente inválido. O que importa é que as invariáveis permaneçam verdadeiras antes e depois da execução dos métodos públicos.

Como evitar que o objeto fique em um estado inválido? Uma ferramenta de análise estática, como o Microsoft Static Code Checker, poderia detectar que uma determinada atribuição violará uma invariável. As invariáveis protegem você de um comportamento corrompido, mas também podem ajudar a identificar entradas mal-especificadas. Ao especificá-las apropriadamente, você poderá localizar bugs no código mais facilmente usando uma determinada classe.

Herança de contrato

A Figura 3 mostra outra classe com um método invariável definido. Essa classe pode funcionar como raiz de um modelo de domínio.

Figura 3 Classe-raiz baseada em invariável para um modelo de domínio

public abstract class DomainObject
{
  public abstract Boolean IsValid();

  [Pure]
  private Boolean IsValidState()
  {
    return IsValid();
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(IsValidState());
  }
}

Na classe DomainObject, a condição invariável é expressa por meio de um método privado declarado como método puro (ou seja, que não altera o estado). Internamente, o método privado chama um método abstrato que as classes derivadas usarão para indicar suas próprias invariáveis. A Figura 4 mostra uma possível classe derivada de DomainObject que substitui o método IsValid.

Figura 4 Substituindo um método usado pelas invariáveis

public class Customer : DomainObject
{
  private Int32 _id;
  private String _companyName, _contact;

  public Customer(Int32 id, String company)
  {
    Contract.Requires(id > 0);
    Contract.Requires(company.Length > 5);
    Contract.Requires(!String.IsNullOrWhiteSpace(company));

    Id = id;
    CompanyName = company;
  }
  ...
  public override bool IsValid()
  {
    return (Id > 0 && !String.IsNullOrWhiteSpace(CompanyName)); 
  }
}

A solução parece elegante e eficiente. Vamos experimentar obter uma nova instância da classe Customer passando dados válidos:

var c = new Customer(1, "DinoEs");

Se deixarmos de olhar para o construtor Customer, tudo parece bem. Entretanto, como Customer herda de DomainObject, o construtor DomainObject é chamado e a invariável é verificada. Como IsValid em DomainObject é virtual (na verdade, abstrato), a chamada é redirecionada para IsValid, conforme definido em Customer. Infelizmente, a verificação ocorre com uma instância que não foi completamente inicializada. Você obterá uma exceção, mas não é sua culpa. (Na última versão dos contratos de código, esse problema foi resolvido, e a verificação de invariável nos construtores é postergada até que o construtor mais externo seja chamado.)

Esse cenário leva a um problema conhecido: não chame membros virtuais de um construtor. Nesse caso, isso acontece não porque você codificou diretamente dessa maneira, mas como um efeito colateral da herança de contrato. Você tem duas soluções: extrair o "abstract" IsValid da classe base ou reclassificar para o código da Figura 5.

Figura 5 Substituindo um método usado pelas invariáveis

public abstract class DomainObject
{
  protected Boolean Initialized; 
  public abstract Boolean IsValid();

  [Pure]
  private Boolean IsInValidState()
  {
    return !Initialized || IsValid();
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(IsInValidState());
  }
}

public class Customer : DomainObject
{
  public Customer(Int32 id, String company)
  {
     ...
    Id = id;
    CompanyName = company;
    Initialized = true;
  }
     ...
}

O membro protegido Initialized funciona como um guarda e não chamará o IsValid substituído até que o objeto real seja inicializado. O membro Initialized pode ser um campo ou uma propriedade. Se for uma propriedade, no entanto, você fará um segundo tour pelas invariáveis — o que não é estritamente necessário, pois tudo já foi verificado uma vez. Nesse sentido, usar um campo resulta em um código ligeiramente mais rápido.

A herança de contrato é automática no sentido de que uma classe derivada recebe automaticamente os contratos definidos em sua classe base. Dessa maneira, uma classe derivada adicionará suas próprias pré-condições às pré-condições da classe base. O mesmo ocorre para pós-condições e invariáveis. Ao lidar com uma cadeia de herança, o regravador intermediário da linguagem somará os contratos e os chamará na ordem correta quando e onde for apropriado.

Seja cuidadoso

As invariáveis não são à "prova de bala". Algumas vezes, as invariáveis podem ajudar de uma maneira e causar problemas de outra, especialmente se usadas em todas as classes e no contexto de uma hierarquia de classes. Apesar de você sempre se esforçar para identificar a lógica invariável em suas classes, se a implementação de invariáveis gerar problemas, então eu digo que é melhor deixá-las fora da implementação. Entretanto, tenha em mente que, se você tiver problemas, eles serão causados pela complexidade do modelo. As invariáveis são as ferramentas de que você precisa para gerenciar problemas de implementação; elas não são o problema em si.

Há pelo menos um motivo pelo qual somar contratos em uma hierarquia de classes é uma operação delicada. É uma explicação bastante longa, mas que será um bom assunto para o artigo do mês que vem.

Dino Esposito é o autor de “Programming Microsoft ASP.NET 4” (Microsoft Press, 2011) e coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Residente na Itália, Esposito é um palestrante sempre presente em eventos do setor no mundo inteiro. Você pode segui-lo no Twitter, em twitter.com/despos.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Manuel Fahndrich e Brian Grunkemeyer