Pull to refresh

Программирование по контракту в .NET Framework 4

Reading time 9 min
Views 9K
Столкнувшись с проблемой смены работы и желания работать разработчиком в хорошей конторе, понял, что мне не хватает знаний в области архитектуры, проектирования, ООП и прочих, не специфичных для платформы или языка вещах. Источники получения информации, кроме личного опыта, стандартные – книги и Интернет.

К тому времени были прочитаны книги Фаулера о рефакторинге и книга GoF. Эти книги многое мне дали и были очень полезными, но хотелось чего-то более основополагающего об ООП. Поискав по форумам, я нашел несколько книг, которые меня заинтересовали:
Бертран Мейер «Объектно-ориентированное конструирование программных систем»
Гради Буч, Объектно-ориентированный анализ и проектирование
Барбара Лисков. Использование абстракций и спецификаций при разработке программ

К сожаление последней не нашел в электронном виде, а в бумажном не знаю даже где искать. Из наличия в магазине на тот момент была только книга Мейера, ее я и взял.

Немного о книге.


Книга мне понравилась, многое упорядочила и объяснила, хотя остались некоторые нюансы. Перевод, в целом, грамотный, его делали люди, которые в курсе технологии и терминологии. К сожалению, я не нашел год написания текста, но как я понял примерно середина 90-х. Это во многом объясняет, почему автор так усердно доказывает, что ООП лучше других, популярных на тот момент, подходов. Некоторые моменты мне не очень понравились. Первое, много уделено некоторому негативу о функциональном программировании. Второе, вообще вся книга, как мне показалось, написана в стиле «Есть два мнения, одно мое, другое неправильное». Автор создал язык Eiffel, в основу которого легли описанные в книге принципы, все спорные и граничные решения трактуются в пользу автора, в сопровождении разгромных комментариев другого варианта. Иной раз становиться непонятным, как языки Java и C# с такими «огромными недостатками» стали популярны. Хотя, надо признать, что генерики были очень ожидаемыми и в Java и в .Net.

Программирование по контракту.


Красной нитью проходит через все книгу идея программирования по контракту. С каждой подпрограммой (методом) могут быть связаны предусловие и постусловие. Предусловие устанавливает свойства, которые должны выполняться каждый раз, когда программа вызывается; постусловие определяет свойства, гарантируемые программой по ее завершению. Предусловия и постусловия описывают свойства отдельных программ. Но экземпляры класса обладают также глобальными свойствами. Их называют инвариантами класса, и они определяют более глубокие семантические свойства и ограничения целостности, характеризующие класс. Инвариант добавляется к предусловию и постусловию. Программирование по контракту – прекрасный способ документирование кода, отладки, исключения лишних проверок. Например, если в постусловии написано, что метод возвращает неотрицательное число, то нет необходимости проверять число на неотрицательность при извлечении квадратного корня. Все это ведет к повышению надежности, качества и скорости разработки.

К сожалению, разработчики .Net (были) лишены возможности использовать этот подход в своей работе. Можно конечно использовать Debug.Assert, но это не совсем то. Assert позволяет ставить условия для определенного участка кода, хотя, их можно использовать вместо предусловий и постусловий. Но есть еще инварианты класса, которые устанавливаются для всех методов класса и, возможно, наследников.

Части системы контрактов в .Net framework 4.0.


Система контрактов состоит из четырех основных частей. Первая часть это библиотека. Контракты кодируются с использованием вызова статических методов класса Contract (пространство имен System.Diagnostics.Contracts) из сборки mscorlib.dll. Контракты имеют декларативный характер, и эти статические вызовы в начале вашего методы можно рассматривать как часть сигнатуры метода. Они являются методами, а не атрибутами, поскольку атрибуты очень ограничены, но эти концепции близки.
Вторая часть это binary rewriter, ccrewrite.exe. Этот инструмент модифицирует инструкции MSIL и проверяет контракт. Это инструмент дает возможность проверки выполнения контрактов, чтобы помочь при отладке кода. Без него, контракты просто документация и не включается в скомпилированный код.
Третья часть это инструмент статической проверки, cccheck.exe, который анализирует код без его выполнения и пытается доказать, что все контракты выполнены.
Четвертая часть — ccrefgen.exe, который создает отдельную сборку, содержащую только контракт.

Библиотека контрактов. Предусловие.


Существует три основные формы предусловия, два из которых основаны на методе Requires класса Contract. Оба подхода имеют перегруженные варианты, позволяющие включать сообщение при нарушении контракта.

public Boolean TryAddItemToBill(Item item)<br>{<br>    Contract.Requires<NullReferenceException>(item != null);<br>    Contract.Requires(item.Price >= 0);<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Данный метод – простой способ указать, что требуется при входе в метод. Форма Requires делает возможным выброс исключения при нарушении контракта.

Третья форма предусловий – использование конструкции if-then-throw для проверки параметров

public Boolean ExampleMethod(String parameter)<br>{<br>    if (parameter == null)<br>        throw new ArgumentNullException("parameter must be non-null");<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Такой подход имеет свои плюсы в том что выполняется каждый раз при запуске. Но система контрактов имеет больше возможностей (инструментарий, наследование и прочее). Для превращения данной конструкции в контракт необходимо добавить вызов метода Contract.EndContractBlock()

public Boolean ExampleMethod(String parameter)<br>{<br>    if (parameter == null)<br>        throw new ArgumentNullException("parameter must be non-null");<br>    // tells tools the if-check is a contract<br>    Contract.EndContractBlock();<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Конструкция if-then-throw может встречаться несколько раз в коде метода, но только если конструкция начинает метод и после нее идет вызов метода Contract.EndContractBlock() данный код будет являться контрактом.

Постусловия.

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

public Boolean TryAddItemToBill(Item item)<br>{<br>    Contract.Ensures(TotalCost >= Contract.OldValue(TotalCost));<br>    Contract.Ensures(ItemsOnBill.Contains(item) || (Contract.Result<Boolean>() == false));<br>    Contract.EnsuresOnThrow<IOException>(TotalCost == Contract.OldValue(TotalCost))<br>}<br> <br><br>* This source code was highlighted with Source Code Highlighter.


Второй вариант (сделан для иллюстрации)

public int Foo(int arg)
  {
   Contract.EnsuresOnThrow<DivideByZeroException>(false);
   int i = 5 / arg;
   return i;
  }


* This source code was highlighted with Source Code Highlighter.


При передачи данному методу нуля, будет нарушаться контракт.

Постусловие проверяется при выходе из метода

public class ExampleClass<br>{<br>    public Int32 myValue;<br>    public Int32 Sum(Int32 value)<br>    {<br>        Contract.Ensures(Contract.OldValue(this.myValue) == this.myValue);<br>        myValue += value; //это нарушение контракта и будет отловлено<br>        return myValue;<br>    }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Инвариант


Инвариант кодируется с помощью метода Invariant.

public static void Invariant(bool condition);<br>public static void Invariant(bool condition, String userMessage);<br><br>* This source code was highlighted with Source Code Highlighter.


Они декларируются в единственном методе класса, который содержит только инварианты и помечен атрибутом ContractInvariantMethod. По сложившейся традиции данный метод называют ObjectInvariant и помечают protected, чтобы нельзя было вызвать этот метод явно из чужого кода.

[ContractInvariantMethod]<br>protected void ObjectInvariant()<br>{<br>    Contract.Invariant(TotalCost >= 0);<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Отладка с помощью контрактов.


Есть несколько возможных сценариев использования контрактов для отладки. Один из них заключается в использовании инструментов статического анализа и разбора контрактов. Второй вариант – проверка во время выполнения. Чтобы получить максимальную отдачу необходимо знать, что происходило при нарушении контракта. Здесь можно выделить две стадии: уведомление и реакция.
Если контракт нарушен, возникает событие

public sealed class ContractFailedEventArgs : EventArgs<br>{<br>    public String Message { get; }<br>    public String Condition { get; }<br>    public ContractFailureKind FailureKind { get; }<br>    public Exception OriginalException { get; }<br>    public Boolean Handled { get; }<br>    public Boolean Unwind { get; }<br>    public void SetHandled();<br>    public void SetUnwind();<br>    public ContractFailedEventArgs(ContractFailureKind failureKind,<br>        String message, String condition, <br>        Exception originalException);<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


Помните, что это пререлиз, и что-то может поменяться к окончательному релизу.
Вы можете использовать данное событие для журналирования.

Если вы используете тестовый фреймворк, вы можете сделать например так (пример для фреймворк Visual Studio)

[AssemblyInitialize]<br>public static void AssemblyInitialize(TestContext testContext)<br>{<br>    Contract.ContractFailed += (sender, eventArgs) =><br>    {<br>        eventArgs.SetHandled();<br>        eventArgs.SetUnwind(); // cause code to abort after event<br>        Assert.Fail(eventArgs.Message); // report as test failure<br>    };<br>}<br><br>* This source code was highlighted with Source Code Highlighter.


UPD



Наследование



Многие в комментариях находят сходство с Debug.Assert и юнит-тестированием. Кроме изначально различных целей, значимое отличем заключается в наследовании. Контракты наследуются!
Пример, консольное приложение,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;

namespace ConsoleApplication1
{
 class ClassWithContract
 {
  int intNotNegativeField;
  [ContractInvariantMethod]
  protected virtual void ObjectInvariant()
  {
   Contract.Invariant(this.intNotNegativeField >= 0);
  }

  public virtual int intNotNegativeProperty
  {
   get
   {
    return this.intNotNegativeField;
   }
   set
   {
    this.intNotNegativeField = value;
   }
  }

  public ClassWithContract()
  {
  }

 }

 class InheritFromClassWithContract : ClassWithContract
 {

 }
}

* This source code was highlighted with Source Code Highlighter.


и код Programm.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
 class Program
 {
  static void Main(string[] args)
  {
   InheritFromClassWithContract myObject = new InheritFromClassWithContract();
   myObject.intNotNegativeProperty = -3;
   
  }
 }
}

* This source code was highlighted with Source Code Highlighter.


В данном примере контракт будет нарушен. Также учтены усиления-ослабления условий. По Мейеру, наследник имеет право ослабить предусловие (большее количество вариантов принимать) и усилить постусловие — еще больше ограничить возвращаемое значение.
Это естественно, учитывая динамическое связывание и полиморфизм.

Данная статья подготовлена на основе статьи (вольный перевод с сокращениями) Code Contracts, автор Melitta Andersen

Ссылки по теме.
Tags:
Hubs:
+11
Comments 26
Comments Comments 26

Articles