Интересности      Книги      Утилиты    

19 апреля 2010 г.

Code Contracts в .NET 4.0

В .NET 4.0 в рамках CLR появилась такая новинка как Code Contracts. Что оно такое? Code Contracts это развитие идеи программирования по контракту (Design by Contract), которая была введена Бертраном Мейером, создателем языка Эйфель. Чтобы услышать объяснение того что такое контракты и как они улучшают разработку программного обеспечения можно почитать его интервью.

Контракт – это по сути спецификация компонентов системы. Вот как определили Контрактное программирование в википедии:

Контрактное программирование — это метод проектирования программного обеспечения. Он предполагает, что проектировщик должен определить формальные, точные и верифицируемые спецификации интерфейсов для компонентов системы.

Также как понимает это сам создатель можно прочитать в его интервью.

Итак зачем они нужны? Для того чтобы проверить или верифицировать объекты и компоненты системы, при этом улучшая отладку, тестирование, документирование.

Справедливости ради, следует заметить, что Code contracts достаточно давно существуют как проект в рамках Microsoft Research, но с приходом .NET 4.0 для него наступил звездный час и его внесли в CLR. Хотя мне кажется это вполне логичным развитием как Debug.Assert(Trace.Assert) и if-then-throw контрактов которые появились вместе с самим C#, так и определенная популяризация идея defensive programming.

CLR без сode contracts располагает похожим механизмом. Казалось бы, все что привносят сode contracts можно было реализовать до этого с помощью Debug.Assert, Trace.Assert, или throw. Но тем не менее код с сode contracts выглядит намного чище, и прежние ассерты не давали возможности проводить статическую проверку на этапе компиляции, не было возможности создавать автоматически сгенерированы юнит-тесты, не было возможности сгенерировать документацию для объекта с учетом тех спецификаций, которые они накладывают на объект. Теперь же с появлением code contracts все это возможно.

Давайте посмотрим насколько это возможно.

И хотя сам класс System.Diagnostics.Contracts.Contract включили в состав .NET 4.0, тем не менее, чтобы его использовать необходимо придется либо вручную определить в свойствах проекта символ CONTRACTS_FULL для проверки контрактов в runtime либо загрузить с сайта проекта tools, которые добавят в свойства проекта новую вкладку с названием code contracts. В Visual Studio 2010, релизном варианте, скачанном после запуска, такая вкладка отсутствует.

image

Давайте попробуем, как ведет себя статическая проверка. Достаточно на этой вкладке отметить чекбокс “Perform static checking” и скомпилировать код с контрактами и/или нарушением этих контрактов. В итоге все проверки появятся, как warnings если было нарушение.

image

Тестируемость. Например, можно создать автосгенерированные юнит-тесты с помощью такого инструмента как Pex (http://research.microsoft.com/en-us/projects/pex/). На скриншоте pex вывел результат теста, который не прошел проверку по контракту:

image

Для генерирования документации существует утилита Code Contract Document Generator Tool (CCDocGen.exe). Она добавляет информацию про контракты в уже сгенерированные компилятором файлы XML документации, а чтобы из XML файла получить, например, документацию в стиле MSDN достаточно воспользоваться утилитой SandCastle (http://sandcastle.codeplex.com/). Хотя помимо сгенерированой документации, сам код является в некотором роде самодокументированым. Т.е. достаточно одним глазом взглянуть на контракты чтобы определить каким требованиям должны соотвествовать объекты.

Итак, преимуществ перед старыми средствами достаточно много.

Code contracts есть 3 видов:

- Preconditions – используется для валидации аргументов
- Postconditions – для проверки состояния по завершению метода, независимо от того нормально он завершился или с исключением
- Object invariants – для проверки, что данные объекта находятся в хорошем состоянии на протяжении жизни объекта

Если одно из этих условий нарушается, то при runtime проверке мы получим ContractException. Существует также возможность подписаться на событие Contract.ContractFailed чтобы либо продолжить/прервать выполнение, либо отреагировать на нарушение контракта. Например подписку на ContractFailed можно использовать, например для того чтобы юнит-тесты не остановили процесс сборки тестов на билд машине.

Если контракт будет нарушен в runtime то мы увидим стандартный для ассертов диалог.

image

Контракты очень похожи на ассерты поэтому для их понимания давайте лучше по смотрим на них в действии. Итак, пусть у нас есть объекты Order и OrderItem.
public class Order
{
    List<OrderItem> orderedItems = new List<OrderItem>();
    decimal orderPrice = 0;

    public void MakeNewOrder(OrderItem orderItem)
    {
      // precondition
      Contract.Requires<ArgumentNullException>(orderItem != null);
      Contract.Requires(Contract.ForAll(orderedItems, p => p != orderItem));

      // postcondition
      Contract.Ensures(Contract.Exists(orderedItems, p => p == orderItem));
      Contract.Ensures(orderPrice > Contract.OldValue(orderPrice));

      orderedItems.Add(orderItem);
      orderPrice += orderItem.Price;
    }

    [ContractInvariantMethod]
    private void ObjectInvariant()
    {
      Contract.Invariant(orderPrice > 0);
      
    }    
}

Аналогом такого класса с контрактами может быть класс выполненный с контрактами в стиле if-then-throw:
public class OrderOld
  {
    List<OrderItem> orderedItems = new List<OrderItem>();
    decimal orderPrice = 0;

    public void MakeNewOrder(OrderItem orderItem)
    {
      // precondition
      if (orderItem == null)
        throw new ArgumentNullException("orderItem", "orderItem is null.");
      if (orderedItems.Any(p => p == orderItem))
        throw new InvalidOperationException("Item already ordered");
      Contract.EndContractBlock();

      decimal oldPrice = orderPrice;

      orderedItems.Add(orderItem);
      orderPrice += orderItem.Price;

      // postcondition
      if (!orderedItems.Any(p => p == orderItem))
        throw new InvalidOperationException("Item wasn't added");
      if (orderPrice <= oldPrice)
        throw new InvalidOperationException("Item's price invalid");

      //invariant
      if (orderPrice < 0)
        throw new InvalidOperationException("Order's price invalid");
    }
  }

Предусловия(preconditions) реализованы используя Contract.Requires и специфицируют, что OrderItem не должен быть null или уже заказан. Пост условия - Contract.Ensures, что OrderItem должен быть заказан и цена заказа должна возрасти. И есть еще инвариант. Инвариант реализуются с помощью методов с атрибутом ContractInvariantMethod при чем их может несколько но в итоге они будут объединены. Так вот, инварианты будут вызваны в конце вызова каждого public метода экземпляра класса. Условия используемые в preconditions, preconditions и invariants не должны изменять состояние объекта. Метод Contract.Requires<ArgumentNullException> например в случае неверного контракта выбросит соотвествущее исключение.

Как это работает? Специальная утилита Code contract rewriter tool (CCRewrite.exe) модифицирует IL код, так что предусловия будут выполняться в начале метода, пост – в конце, а инварианты – после каждого публичного. Эта утилита не модифицирует только методы Finalize или Dispose.

Среди прочих полезных методов класса Contract:
Contract.Requires<ArgumentNullException>(x != null, “x”) – используется для того чтобы если предусловие не прошло проверки вызвать соотвествующее исключение
Contract.EnsuresOnThrow – постусловие, если метод завершится указанным исключением
Contract.Result<int>() – используется если в контракте понадобится сравнить с возвращаемым значением метода
Contract.OldValue(xs.Length) – используется для сравнения с предыдущим значением в контракте
Contract.ForAll – можно также накладывать контракты на диапазоны(списки) значений
Contract.ValueAtReturn(out x) – помагает для out значений
Contract. Exists – если нужно проверить на существование в списке
Contract.Equals – проверка на равенство


Contract.EndContractBlock – для того чтобы контракты «старого» вида if-then-throw распознавались как code contracts


if (orderItem == null)
  throw new ArgumentNullException("orderItem", "orderItem is null.");
if (orderedItems.Any(p => p == orderItem))
  throw new InvalidOperationException("Item already ordered");
Contract.EndContractBlock();

А также Contract.Assume и Contract.Assert. Contract.Assert это аналог Debug.Assert(Trace.Assert) с одной лишь разницей, что при использовании варианта из code contracts мы получаем статическую проверку. Условие в Contract.Assume в отличии от Contract.Assert не проверяется, а сохраняется как истинное для того чтобы помочь в статической проверке контрактов.

Также контракты можно использвать с интерфейсами и абстрактными классами, правда так как методы в них могут не иметь тела поэтому контракты для них пишутся в отдельном классе, а итерфейс/абстрактный класс и класс с их контрактами связываются между собой атрибутами ContractClass и ContractClassFor:

[ContractClass(typeof(OrderItemContract))]
  public interface IOrderItem
  {
    string ItemName { get; set; }
    decimal Price { get; set; }
  }

  [ContractClassFor(typeof(IOrderItem))]
  sealed class OrderItemContract : IOrderItem
  {
    private string itemName;
    public string ItemName
    {
      get
      {
        Contract.Ensures(!string.IsNullOrEmpty(itemName));
        return itemName;
      }
      set
      {
        itemName = value;
      }
    }
    private decimal price;
    public decimal Price
    {
      get
      {
        return price;
      }
      set
      {
        Contract.Requires(value > 0);
        price = value;
      }
    }
  }

Контракты также наследуются. Правда действуют определенные правила:

- В наследниках нельзя определить предусловия, предусловия определяются только в базовом классе
- В наследниках можно усилить постусловия
- Инварианты наследника используются вместе с инвариантами базового класса для верификации объекта наследника.

А что если используемые методы контрактов в CLR нас чем-то не устраивают, либо хочется их дополнить. Существует механизм для этого – custom contracts или custom rewriters methods. Суть этого механизма в том что мы можем сами определить эти методы, например, в отдельной сборке, а потом указать этот contract runtime class и/или сборку в которой он лежит либо используя інтерфейс(вкладку code contracts -> custom rewriters methods можно увидеть на скриншоте с вкладкой) либо опции командной строки. Методы которые нужно переопределить должны иметь следующий вид:
public static class RuntimeFailureMethods
  {
    public static void Requires(bool cond, string userMsg, string condText)
    { /*...*/ }
    public static void Requires<E>(bool cond, string userMsg, string condText)
    where E : Exception
    { /*...*/ }
    public static void Ensures(bool cond, string userMsg, string condText)
    { /*...*/ }
    public static void EnsuresOnThrow(bool cond, string userMsg, string condText, Exception innerException)
    { /*...*/ }
    public static void Assert(bool cond, string userMsg, string condText)
    { /*...*/ }
    public static void Assume(bool cond, string userMsg, string condText)
    { /*...*/ }
    public static void Invariant(bool cond, string userMsg, string condText)
    { /*...*/ }
    public static void ReportFailure(ContractFailureKind kind, string userMsg, string condText, Exception inner)
    { /*...*/ }
    public static string RaiseContractFailedEvent(ContractFailureKind kind, string userMsg, string condText, Exception inner)
    { /*...*/ }
    public static void TriggerFailure(string message, string userMsg, string condText, Exception inner)
    { /*...*/ }
  }

Если какие-либо методы пропущены, то они либо будут синтезированы либо исопользованы стандартные методы. Класс с custom rewriters methods может лежать как в отдельной сборку так и в основной, главное чтоб rewriter смог его найти.

Code contracts несомненно полезная штука, которую будут использовать и возможно в будущем она полностью заменит Debug.Assert/Trace.Assert.

1 комментарий:

  1. Спасибо за статью, для меня она имеет скорее образовательный смысл, я такого никогда раньше не юзал, так держать, дружище, полезно почитать!

    ОтветитьУдалить