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

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

    К тому времени были прочитаны книги Фаулера о рефакторинге и книга 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

    Ссылки по теме.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 26

      0
      Я правильно понимаю, что контракт — это что-то вроде трех последовательных методов, работающих с одним набором данных, первый из которых выполняет проверки, второй — модифицирует данный, а третий — проверяет их допустимость?
        +5
        Контракт — это развитие представления о верифицируемости кода для императивного программирования.

        Так как для мейнстрим-языков (императивных, в основном) формальный анализ корректности представляется необозримо сложным (потому что в императивном языке модель потоков данных вторична, и потому ее крайне труждоемко отследить и упорядочить), сделана попытка ввести предположения, упрощающие этот анализ.
        Для этого делается попытка привязать граф потоков данных к графу потоков управления — в ключевых точках последнего (вход в процедуру/выход в процедуру/некоторые другие ключевые точки) данные анализируются на предмет соответствия ожидаемым данным в этих точках.

        Кроме того, контракт — это еще и самодокументирование кода.
          0
          В данный момент (.net contracts) это реализовано при помощи инструментирования сборок, чтобы обеспечить как compile-time-, так и runtime-анализ, но ничего не мешает выполнять, например, только compile-time анализ.
          0
          Контракт — это набор некоторых условий, которые должны быть верны перед вызовом метода (подпрограммы) для предусловия и должны быть верны при выходе из метода (для постусловия).

          Например, есть метод

          double GetSqrt(double d)
          {

          }

          Можно написать условие проверки d на неотрицательность, а можно написать контракт

          Contract.Requires(d>=0);

          И теперь всем понятно, что неотрицательное d — требование спецификации, кроме того в релиз-версии данная проверка не будет проводиться. Я пока не разобрался с наследованием, но, как я понимаю, при переопределении контракт должен сохраняться. Причем предусловие может ослабевать, а постусловие усиливаться.

          Для постусловий тоже самое. При выходе из метода мы можем что-либо сказать нашим клиентам, по поводу возвращаемого результата ил состояния объекта.

          public double Sum()
          {
          double res = 0;
          ...// какой-то код
          Contract.Ensures(res > 0); // res > 0 и я гарантирую это, говорит нам метод с контрактом
          return res;
          }

          то есть метод никак не сможет вернуть значение меньшее или равное единицы, поэтому если вам, в вашем методе требуется положительное double, вы можете смело и БЕЗ ПРОВЕРКИ использовать результат данного метода, ибо контракт.

            0
            *никак не сможет вернуть значение меньшее или равное НУЛЯ
            0
            спасибо за статью — недавно наткнулся на эту статью в msdn и отложил на прочтение на потом.

            P.S. Подредактируйте предложение — «Предусловия и постусловия описывают свойства отдельных программ. но экземпляры класс обладают также глобальными свойствами.»
              0
              Вроде та же идея используется в формальных методах доказательства алгоритмов.
              Было интересно почитать, спасибо.
                0
                Да, знакомые вещи, старые идеи и это даже не середина 90ых, а гораздо раньше, смотрите работы
                Дейкстры, например «Дисциплина программирования», все это от туда формальное доказательство правильности алгоритмов, слабейшее предусловие и прочие радости :)

                Тут более интересно войдет ли это все когда нибудь в мейнстрим?
                  0
                  Хз, но надежда есть :).

                  Например, Midori — инициатива M$ по созданию формально верифицируемой ОС. Они так рьяно взялись за это дело, что даже перекупили себе в MS Research разработчика BitC (язык) и Coyotous (первый и единственный прототип формально верифицируемой ОС), «заставив» его прекратить разработку вышеупомянутых проектов.

                  Другое дело, что вряд ли это интересно большинству. Как вы себе представляете формально доказанный магазин на php? :)
                  0
                  о, а я инвариатны видел в языке D)) И кстати как понял, условия проверяются только в отлдаочной сборке, а в release отключаются, да?
                  • UFO just landed and posted this here
                      0
                      Как я понял наследованием и наличием инвариантов.
                      Ну и назначение немного другое: ассертами мы для себя проверяем какие-либо условия в любых точках. Здесь мы проверяем условие на входе или выходе и всем гарантируем (и требуем!) соблюдение контракта, что, в итоге, должно повысить качество кода, убрать лишние проверки, ускорить отладку.

                      Например, Вы сделали некий метод, в котором по контракту возвращается положительное число. При использовании Вашего метода, как входа своего

                      double d = MyMath.GetSqrt(yourClass.YourMethodWithPositiveReturnByContract())

                      Я НЕ БУДУ проверять, что Ваш метод возвращает положительное число, я верю контракту.

                      Если Вы сделаете проверку с помощью Assert — то Вы, может быть, убедите только себя в положительности результата, я верить не буду и включу проверку на неотрицательность.

                      Как-то так.
                        0
                        Та нет, все проще. :)

                        Контракты представляют собой более-менее дисциплинированное применение assert(). Однако же идея восходит к соответствию Карри-Говарда, которое как бы намекает нам, что
                        — между системами типов и логическими система есть соответствие
                        — программы являются доказательствами

                        Например, тип функции fact (факториал), что-то вроде

                        fact: forall x, x >= 0, exists y, y >= 0; x -> y

                        Ну а доказательством этого утверждения является программа.
                      0
                      Если я правильно понял, то указанный подход изоморфен юнит-тестируемому результату проектирования + моковый фреймворк.

                      Фишка контрактов в изначальной интегрируемости?
                        0
                        Здесь я пытался объяснить чем отличается использование Assert от контрактов на мой взгляд. Контракт после релиза ваших сборок доступен клиентам (в смысле не конечным пользователям, а тем кто будет использовать ваши сборки в своей работе, например ваш коллега), тесты и ассерты не всегда.

                        Инвариантов вообще никак не сделать без огромного дублирования кода, что есть зло. Таким образом, с помощью инвариантов легко поддерживать объект в согласованном состоянии.

                        Третий момент — наследование. Я пока не разбирался с наследованием контрактов в .Net, но в оригинале (у Мейера) контракты наследуются, чего опять же нет при использовании ассертов и тестов. Причем предусловие может ослабевать, а постусловие усиливаться при переходе к дочерним классам.

                        Инструменты — в том числе попытка проверки кода без его запуска.

                          0
                          хм. ну да, понятно.

                          Юниттесты все же круче тем, что они отделены от кода, тогда как контракты должны быть с ним тесно связаны.

                          Но тема интерсная, спасибо.
                            +1
                            вы можете использовать контракт просто как документацию в релизах (также с Assert сейчас).

                            Есть возможность проверки с помощью ccrewrite.exe — примерно так-же, как NUnit (руками пока ничего не трогал, все в теории).
                            Вы можете отделить контракт от кода и получить чистый контракт — с помощью ccrefgen.exe
                            Ну и попытаться проверить контракт без запуска сборки на основе кода (статически) с помощью утилиты cccheck.exe.

                            В ближайшее время планирую изучить подробнее данные инструменты и сделать обзор.

                            Ну а насчет связки контракта и кода — наоборот это плюс, имхо. Ведь именно контракт показывает что вы ждете на входе и что будет на выходе. Тесты и ассерты для внутреннего потребления, контракты при взаимодействии сборок различных поставщиков.
                              0
                              Контракт тоже можно отделить от кода. Например, использование аспектно-ориентированного программирования позволяет это сделать. Или вообще специализированные технологии описания контрактов типа UniTESK или Spec#.
                          0
                          А никто не задумывался, как это повлияет на производительность кода?
                            0
                            Контракт в релизе, так же, как и Assert не компилируется. Так что никак. А если еще считать, что уйдут лишние проверки, там где и так контракт что-то гарантирует, то даже ускориться несколько должно.
                            +1
                            >второй гарантирует выброс исключения при нарушении контракта.
                            Неправда. Он гарантирует, что при вылете исключения будет соблюдено условие (например, не изменится переданное по ссылке значение).

                            Опять же, невнятно написано что компилируется в Release, а что нет.
                              0
                              Спасибо, подправил.

                              Насчет компиляции.

                              В свойства проекта добавляется новая закладка Code Contracts, там мы и выбираем, будет проверка контракта рантайм или нет, если будет, то в каком виде (Assert или выброс исключения). Так в студии. На самом деле мы просто выбираем чем компилировать нашу сборку — обычным компилятором или с помощью ccrewrite.exe. Второй вариант включает проверки контрактов.
                                0
                                The other form, Requires, makes that same statement, but further guarantees that if the condition is not met, an exception of type TException should be thrown. It is also unique in that it is always compiled, so use of this method entails a hard dependency on the tools. You should decide if you want that before using this method.
                              0
                              >В данном примере контракт будет нарушен. Также учтены усиления-ослабления условий.
                              >По Мейеру, наследник имеет право ослабить предусловие (большее количество вариантов принимать)
                              >и усилить постусловие — еще больше ограничить возвращаемое значение.

                              В наследниках нельзя ни усилить, ни ослабить контракт. См. их официальный pdf-ник (пункт 3).
                                0
                                У них написано, что по логике ослаблять предусловие нормально, но они не увидели ни одного разумного ослабления предусловия. Так как ослабление предусловия несет некоторые сложности, они решили это не реализовывать.

                                Постусловие и инвариант можно добавлять, таким образом ужесточая контракт.
                                  0
                                  Да, прошу прощения. Пока прописываю только Requires, так что их и воспринимаю «контрактом» :)

                              Only users with full accounts can post comments. Log in, please.