Este artigo foi traduzido por máquina.

Vanguarda

Contratos de código: herança e o princípio de Liskov

Dino Esposito

Dino EspositoAssim como contratos reais, contratos de software você vincular a restrições adicionais e custam-lhe algo em termos de tempo. Quando um contrato é permanente, você pode querer certificar-se de que você não quebra seus termos. Quando se trata de contratos de software — incluindo contratos de código no Microsoft.NET Framework — quase todo desenvolvedor eventualmente irá manifestar dúvidas sobre os custos computacionais de ter contratos em torno de classes. Contratos são apropriadas para seu software, independentemente do tipo de construção? Ou são contratos em vez disso, principalmente um depuração auxílios que devem ser retirado de código de varejo?

Eiffel, a primeira língua a introdução de contratos de software, tem palavras-chave de idioma nativo para definir condições, pós-condições e invariantes. Em Eiffel, portanto, contratos são parte da linguagem. Se usado no código-fonte de uma classe, contratos se tornar parte integrante do código.

No.NET, porém, contratos fazem parte do quadro e não pertencem a idiomas suportados. Isso significa que verificação de tempo de execução pode ser ativado ou desativado no Irão. Em particular, no.NET você está autorizado a decidir sobre contratos numa base de configuração por compilação. Em Java, as coisas são quase o mesmo. Você usa um quadro externo e quer adicionar código do contrato para as fontes para ser compilado ou pedir ferramentas em torno de estrutura para modificar o bytecode em conformidade.

Neste artigo, falarei sobre alguns cenários onde código contratos revelar-se particularmente útil na condução você em direção a uma maior qualidade de design de software global.

O que são contratos de código para

Uma prática recomendada perene de desenvolvedores de software é escrever métodos que verifiquem cuidadosamente quaisquer parâmetros de entrada que recebem. Se o parâmetro de entrada não corresponder às expectativas do método, uma exceção é descartada. Isso é conhecido como o se-então-throw padrão. Com contratos de condição prévia, este mesmo código parece mais agradável e mais compacto. Mais interessante, ele também lê melhor, porque uma condição prévia permite que você indicar claramente apenas o que é necessário em vez de testes contra o que não é desejável. Assim, à primeira vista, contratos de software simplesmente olhar como uma abordagem mais agradável de gravação para evitar exceções em métodos de classe. Bem, há muito mais que isso.

O simples fato de que você acha de contratos para cada método indica que você está pensando agora mais sobre o papel desses métodos. No final, o projeto Obtém terser e terser. E contratos também representam uma valiosa forma de documentação, especialmente para fins de refatoração.

Contratos de código, no entanto, não estão limitados a condições prévias, apesar de condições prévias são a parte mais fácil de contratos de software para pegar. A combinação de condições prévias, pós-condições e invariantes — e a extensa aplicação deles em todo o código — dá-lhe uma vantagem decisiva e realmente leva a algum código de alta qualidade.

Afirmações vs. Código contratos vs. Testes

Contratos de código não são inteiramente como afirmações e outros instrumentos de depuração. Enquanto contratos podem ajudá-lo a rastrear erros, eles não substituem um depurador bom ou um conjunto de testes de unidade bem-feito. Como afirmações, contratos de código indica uma condição que deve ser verificada em um determinado ponto durante a execução de um programa.

Uma afirmação que não é um sintoma de que algo está errado em algum lugar. Uma afirmação, no entanto, não pode dizer por que ele falhou e onde se originou o problema. Um contrato de código falhar, por outro lado, você diz muito mais. Ele compartilha detalhes sobre o tipo de falha. Assim, por exemplo, você pode saber se a exceção foi gerada porque um determinado método recebeu um valor inaceitável, falha no cálculo do valor de retorno esperado ou mantém um estado inválido. Considerando que uma afirmação diz-lhe apenas sobre um sintoma de mau detectado, um contrato de código pode mostrar informação valiosa sobre como o método deve ser usado. Esta informação em última análise, pode ajudá-lo a entender o que tem de ser corrigido para parar de violar uma afirmação determinada.

Como contratos de software se relacionam ao teste de unidade? Obviamente, um não exclui o outro e os dois recursos são espécie de ortogonais. Um equipamento de teste é um programa externo que funciona através da aplicação de uma entrada fixa para as classes selecionadas e métodos para ver como eles se comportam. Contratos são uma maneira para as classes gritar quando algo está errado. Para testar a contratos, no entanto, você deve estar executando o código.

Testes de unidade são uma ótima ferramenta para capturar regressão após um profundo processo de refatoração. Contratos são talvez mais informativa do que testes para documentar o comportamento esperado de métodos. Para obter o valor de concepção de testes, você deve praticar test-driven development (TDD). Contratos são, provavelmente, uma ferramenta mais simples do que TDD para métodos de documento e design.

Contratos de adicionar informações extras ao seu código e deixá-la até você decidir se essa informação deve fazê-lo para os binários implantados. Testes de unidade envolve um projeto externo que pode estimar como o código está fazendo. Se você compila informações do contrato ou não, ter contrato clara informação antecipadamente ajuda imensamente como auxiliar de design e documentação.

Contratos de código e dados de entrada

Contratos referem-se às condições que sempre prender verdadeiras no fluxo normal de execução de um programa. Isso parece sugerir que é o local ideal onde você pode querer usar contratos em bibliotecas internas que são apenas a entrada estritamente controladas pelo desenvolvedor. Classes que são expostos directamente à entrada do usuário não são necessariamente um bom lugar para os contratos. Se você definir condições prévias em dados de entrada não filtrados, o contrato pode falhar e lançar uma exceção. Mas isso é realmente o que você quer? Na maioria das vezes, você deseja degradar graciosamente ou retornar uma mensagem educada para o usuário. Você não quer uma exceção e você não quer jogar e, em seguida, capturar uma exceção apenas para recuperar graciosamente.

No.NET, contratos de código pertencem a bibliotecas e pode ser anotações de dados um bom complemento para (e, em alguns casos, um substituto para). Anotações de dados fazem um grande trabalho em relação à interface do usuário porque no Silverlight e ASP.NET você tem componentes que compreendem essas anotações e ajustar o código ou o HTML de saída. Sobre a camada de domínio, no entanto, geralmente é necessário mais do que apenas atributos, e contratos de código são um substituto ideal. Não necessariamente eu estou dizendo que você não pode obter os mesmos recursos com atributos que você pode com contratos de código. Acho, no entanto, que em termos de legibilidade e expressividade, os resultados são melhores com contratos de código que atributos. (Aliás, isso é precisamente porque a equipe de contratos de código prefere código simples sobre atributos.)

Contratos herdados

Contratos de software são herdáveis em praticamente qualquer plataforma que suporte-los, e a.NET Framework não é excepção. Quando você derivar uma classe nova de um já existente, a classe derivada pega o comportamento, contexto e contratos do pai. Que parece ser o curso natural das coisas. Herança de contratos não representam qualquer problema de invariantes anteriores e posteriores. É um pouco problemático para condições prévias, porém. Vamos abordar invariantes e considere o código em Figura 1.

Figura 1 herdando invariantes

public class Rectangle
{
  public virtual Int32 Width { get; set; }
  public virtual Int32 Height { get; set; }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(Width > 0);
    Contract.Invariant(Height > 0);
  }
}
public class Square : Rectangle
{
  public Square()
  {
  }

  public Square(Int32 size)
  {
    Width = size;
    Height = size;
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(Width == Height);
  }
  ...
}

A classe base Retangular tem duas constantes: largura e altura são maiores que zero. A classe derivada Square adiciona outra condição invariável: devem corresponder a largura e altura. Mesmo do ponto de vista lógica, isso faz sentido. Um quadrado é como um retângulo, exceto que possui uma restrição adicional: largura e altura devem ser sempre o mesmo.

Para posteriores, as coisas funcionam na maior parte da mesma maneira. Uma classe derivada que substitui um método e adiciona mais pós-condições apenas amplia os recursos da classe base e age como um caso especial da classe pai que faz tudo o que o pai faz e muito mais.

E quanto a condições prévias, então? É exatamente por isso que em suma contratos através de uma hierarquia de classes é uma operação delicada. Logicamente falando, um método de classe é o mesmo que uma função matemática. Obter alguns valores de entrada e produzir alguma saída. Em matemática, o intervalo de valores produzidos por uma função é conhecido como o contradomínio; o domínio é o intervalo de valores de entrada possíveis. Adicionando invariantes anteriores e posteriores a um método de classe derivada, você apenas aumenta o tamanho do contradomínio do método. Mas, adicionando condições prévias, restringir o domínio do método. Isso é algo que você realmente deve estar preocupado? Continue a ler.

O princípio de Liskov

SÓLIDO é um acrônimo popular que resulta das iniciais de cinco princípios-chave de design de software incluindo responsabilidade única, aberto/fechado, segregação de Interface e inversão de dependência. O l no estado sólido significa o princípio da substituição de Liskov. Você pode aprender muito mais sobre o princípio de Liskov na bit.ly/lKXCxF.

Em poucas palavras, o princípio de Liskov afirma que sempre deve ser seguro usar uma subclasse em qualquer lugar onde a classe pai é esperada. Tão enfático como pode parecer, isto é não algo que podemos sair da caixa com orientação a objetos simples. Nenhum compilador de qualquer linguagem orientada a objeto pode fazer a mágica de garantir que guarda sempre o princípio.

É responsabilidade do desenvolvedor precisa garantir que é seguro usar qualquer classe derivada em lugares onde a classe pai é esperada. Repare que eu disse "seguro". Orientação a objetos simples torna possível usar quaisquer classes derivadas em lugares onde a classe pai é esperada. "Possível" não é o mesmo que "segura". Para cumprir o princípio de Liskov, terá de aderir a uma regra simple: O domínio de um método não pode ser reduzido em uma subclasse.

Contratos de código e o princípio de Liskov

Além da definição formal e abstrata, o princípio de Liskov tem muito a ver com contratos de software e pode ser facilmente reformulado em termos de uma tecnologia específica, como.Contratos de código NET. O ponto principal é que uma classe derivada não pode apenas adicionar condições prévias. Ao fazê-lo, ele irá restringir o intervalo de valores possíveis, sendo aceites para um método, possivelmente criando falhas de tempo de execução.

É importante observar que violar o princípio não necessariamente resulta em uma exceção de tempo de execução ou mau comportamento. No entanto, é um sinal de que um contra-exemplo possível quebras de seu código. Em outras palavras, efeitos de violação podem ondulação em toda a base de código inteiro e mostrar sintomas nefastas em áreas aparentemente não relacionadas. Faz toda a base de código mais difícil de manter e evoluir — um pecado mortal nos dias de hoje. Imagine que você tenha o código Figura 2.

Figura 2 ilustrando o princípio de Liskov

public class Rectangle
{
  public Int32 Width { get; private set; }
  public Int32 Height { get; private set; }

  public virtual void SetSize(Int32 width, Int32 height)
  {
    Width = width;
    Height = height;
  }
}
public class Square : Rectangle
{
  public override void SetSize(Int32 width, Int32 height)
  {
    Contract.Requires<ArgumentException>(width == height);
    base.SetSize(width, width);
  }
}

A Praça de classe herda de retângulo e adiciona apenas uma condição prévia. Neste momento, o código a seguir (o que representa um contra-exemplo possível) falhará:

private static void Transform(Rectangle rect)
  {
    // Height becomes twice the width
    rect.SetSize(rect.Width, 2*rect.Width);
  }

O método Transform foi escrito originalmente para lidar com instâncias da classe Retangular, e ele faz o seu trabalho muito bem. Suponha que um dia você estender o sistema e iniciar passando instâncias da Praça para o mesmo código (intacto), conforme mostrado aqui:

var square = new Square();
square.SetSize(20, 20);
Transform(square);

Dependendo da relação entre quadrada e Retangular, o método Transform pode começar a falhar sem explicação aparente.

Pior ainda, você pode facilmente identificar como corrigir o problema, mas por causa da hierarquia de classes, pode não ser algo que você quer levar levemente. Então você acaba corrigir o bug com uma solução alternativa, conforme mostrado aqui:

private static void Transform(Rectangle rect)
{
  // Height becomes twice the width
  if (rect is Square)
  {
    // ...
return;
  }
  rect.SetSize(rect.Width, 2*rect.Width);
}

Mas, independentemente de seu esforço, a notória bola de Lama apenas começou a crescer mais. A coisa agradável sobre.NET e o compilador c# é que se você usar código contratos para expressar condições prévias, você receber um aviso do compilador de se você está violando o princípio de Liskov (consulte a Figura 3).

The Warning You Get When You’re Violating the Liskov Principle

Figura 3 O aviso que você começa quando você está violando o princípio de Liskov

O princípio sólido menos entendido e aplicado menos

Tendo ensinado uma.Classe design líquido para um par de anos agora, eu acho que posso afirmar que os princípios sólidos, o princípio de Liskov é de longe o menos compreendida e aplicada. Muitas vezes, um comportamento estranho detectado em um sistema de software pode ser rastreado para baixo a uma violação do princípio de Liskov. Bem o suficiente, contratos de código pode ajudar significativamente nesta área, se apenas você ter um olhar cuidadoso avisos do compilador.

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 é palestrante assíduo em eventos do setor em todo o mundo. Você pode segui-lo no Twitter em twitter.com/despos.

Graças aos seguinte perito técnico para revisão deste artigo: Manuel fahndrich