четверг, 13 августа 2015 г.

Начать ли использовать контракты?

Тут народ на rsdn-е интересуется, а стоит ли пользоваться Code Contracts,  вопрос интересный. Ответ такой вопрос звучит обычно так:“it depends”, но я могу помочь понять, от чего и что depends.

Краткий экскурс в историю контрактов

Сама идея контрактного программирования появилась в 80-х годах в голове Бертрана Мейера и основывалась на матанских выкладках Хоара и других умных дядек.

Заключалась она в следующем: программа сама по себе не является корректной или не корректной, это понятие применимо лишь к паре – (программа, ожидаемое поведение). А поскольку ожидаемое поведение обычно является совершенно непонятным делом, то хочется иметь возможность в кратком и implementation-agnostic виде выразить это самое поведение. Причем сделать это как можно ближе к реализации этого самого поведения.

Для выражения ожидаемого поведения (a.k.a. спецификации) использовались утверждения, а чтобы понять, кто в программе не прав, вызывающий или вызываемый код, то добавлено несколько видов утверждений:

 

  • Предусловия – нарушения которых говорит о баге в вызывающем коде («клиент не прав»).
  • Постусловия – нарушения которых говорят о баге в вызываемом коде («сервис не прав»).
  • Инварианты класса – нарушения которых также говорят о баге в вызываемом коде. Это ОО-специфическая штука, которая позволяет четко сказать, чем же является валидное состояние объекта, чтобы не множить предусловия и постусловия. Тут нужно помнить, что инвариант валиден от момента создания объекта до момента его уничтожения – вызова деструктора/метода Dispose, но может быть не валидным *внутри* вызова метода. Инвариант толжен быть валидным в *видимых точках*.
  • Утверждения о корректности реализации – нарушения которых также говорят о баге в вызываемом коде. Это старые добрые ассерты, растыканые в дебрях реализации, и упрощающие отлов багов.
  • Инварианты цикла – экзотическая штука, которая «доказывает» о сходимости цикла. Есть только в Eiffel и их можно игнорировать.

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

Поскольку утверждения (assertions) – вещь не новая, то в чем же разница между контрактами и старыми добрыми assert-ами? А разница в том, что контрактное программирование подразумевает «интеграцию» утверждений в инструменты, код и мозг программиста.

Любой инструмент, который следует принципам контрактного программирования должен предоставлять следующие возможности:

  • Возможность задавать уровень утверждений: убрать всё; оставить только предусловия; оставить предусловия и постусловия; оставить все утверждения.
  • Возможность генерации документации и удобный способ «обнаружения» контрактов в среде исполнения. Тот самый тулинг!
  • Возможность доказать корректность программы на этапе компиляции (статический верификатор).

Любая полноценная реализация «контрактов» должна обладать всеми этими характеристиками, ибо, как пишет пророк-контрактов-Мейер, именно в объединении всех этих идей и заключается сила (хотя бытует мнение, что сила в правде!).

С момента публикации разных букварей о контрактном программированя и появлении подобных тулов прошло много времени, но идея нашла лишь частичное примерение. Почему – ниже. Сейчас же можно на пальцах руки пересчитать «полноценные реализации», толковых из которых чуть менее, чем ноль. Есть Eiffel с полной поддержкой контрактов, есть языки с предусловиями/постусловиями и без возможности задавать поведение нарушений во время исполнения и без статической верификации, и есть ряд тулов для платформ .NET и Java.

Краткий экскурс в историю Code Contracts

Поскольку идея контрактного программирования звучит здраво, не удивительно, что умные мужи во всяких крупных компания решили взять молоток и сделать свою реализацию. В Microsoft этим занимались ребята из исследовательского подразделения (a.k.a. Microsoft Research) в размере, человек, кажется, трех.

К моменту начала работы над библиотекой, уже была запилена поддержка контрактов в языке Spec#, что дало понять, что идея работает. Но здравости идей не достаточно, чтобы можно было убедить Хейлсебрга и компанию в добавлении контрактов в мейнстрим языки, типа C#/VB. Что делать? Тогда было принято осознанное решение сделать контракты language agnostic – привязать их к платформе и сделать доступными для любого языка платформы .NET (ну, как мы потом узнаем, это работает скорее в теории).

И что это означает с точки зрения реализации? Это значит, что «контракты» должны работать на IL-уровне: декомпилировать существующий код, находить вызовы нужных методов (методы класса System.Diagnostics.Contracts.Contract) и переписывать их в зависимости от настроек пользователя – в классические утверждения, в генерацию исключения или просто выпиливать эти вызовы к чертям. Или же находить контракты и пытаться доказать, что программа корректна или нет (статик чекер).

Звучит довольно просто, и процесс чтения и переписывания IL-а и был довольно простым, в году эдак 2003-м, до появления анонимных методов с замыканиями, до появления блоков итераторов и асинк методов. Теперь представьте себе, что три ресечера занимаются тулом десять лет, когда кишки платформы претерпевают существенные изменения, выходят новые тулы для чтения IL-а, а сам генерируемый IL периодически меняется при добавлении новых возможностей. Тогда станет понятным, что метод типа MikesArchitecture – это вполне нормально, что форматирование – это для трусов, и что разобраться в логике переписывателя без слабых наркотиков не получиться. Не говоря уже о статическом анализаторе, понять (и простить?) который сможет лишь PhD под чем-то очень ядреным.

Адаптация внутри Microsoft-а

Если посмотреть на http://referencesource.microsoft.com, то можно найти с тысячу ссылок на Contract.Requires и пару тысяч вызовов Contract.EndContractBlock внутри .NET Framework . Это все равно очень мало, поскольку валидация аргументов – это наше все, а контракты – это наше все для валидации аргументов.

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

Культура в Microsoft-е такова, что там нет возможности навязать команде какие-то инструменты. Если инструмент команде нравится, то она им будет пользоваться. Если нет (и это не противоречит некоторым глобальным стандартам), то инструмент использоваться не будет.

К сожалению, с контрактами, особенности реализации и «исследовательский подход», сделали свое дело. Code Contracts даже сейчас обладают посредственной производительностью, влияют на ран-тайм поведение «переписанного» кода, не обладают нормальным тулингом и не позволяют использовать статический анализатор в промышленных масштабах. Идея-то прекрасная, но реализация четко соответствует вложенным в него средствам (хотя я бы сказал, что для инструмента, который вышел из под пера трех человек – результат превышает вложенные финансовые средства!).

А раз не было адаптации крупными подразделениями, не последовали дополнительные инвестиции, которые бы позволили довести тул до ума. Проблема курицы и яйца в чистом виде.

Facts and Fallacies (факты и заблуждения, кажется)

Теперь от философии к делу. Code Contracts обладает рядом особенностей, часть которых является следствием некоторых архитектурных решений, а другая часть – навязана принципами контрактного программирования. Но не все мифы об этой туле, ИМХО, соответствуют действительности.

  • Они мееедленные! (в плане компиляции)
    Это верно. Наличие ccrewrite-а замедлит билд в раза этак три и перестанет работать в солюшене с тысячей проектов. Проблемы там в том, что необходимо получить полное транзитивное замыкание всех зависимостей для получения контрактов вызываемого кода. Задача эта сложная, и текущая реализация не линейная.
  • Статический анализатор не работает.
    Это тоже верно. Cccheck выдает много ложных предупреждений и может потребоваться серьезный тюнинг кода, чтобы рассказать анализатору, что все нормально, и это он дурак. В результате cccheck на реальных крупных проектах используется редко или запускается избирательно для анализа кода раз в пару месяцев.
  • Тулинг желает лучшего.
    И снова верно. Есть Code Contracts Editor Extensions, но эта штука у меня никогда толком не работала. Поэтому я взялся за плагин к Решарперу, но эта вещь весьма ограниченная.
  • Результирующий код работает медленнее.
    Это не совсем верно. В моем текущем проекте народ просто повернут на эффективности – кастомные сериализаторы, сотни структур и человеко-год оптимизаций. Но контракты все еще есть. Главная проблема в контрактах была связана с recursion check-ом, который вставляется rewriter-ом, когда в предусловии используется член текущего класса. Выключение этой проверки дало 15% прироста производительности в тяжеловесном end-to-end сценарии, а полное отключение контрактов после этого дало еще 5-6%.
  • Генерация внутреннего ContractException – это страшный баг реализации.
    Ну, это by design. Если хочется обрабатывать исключение, то можно использовать Contract.Requires<ArgumentException>(predicate), но в большинстве случаев нарушение предусловия или постусловия должно лететь по стеку на самый верх и там уже либо крэшить приложение или выдавать сообщение пользователю о внутренней ошибке.
  • Зачем контракты, можно и if-throw-ми обойтись.
    Ну, следуя такой логике, можно и на ассемблере писать. Я с этим утверждением не согласен. Думать в терминах контрактов – очень полезно, и меня лично использование инструмента стимулирует. Плюс возможность вырубить контракты целиком или подписаться на нарушение контракта – бывает очень полезным.
  • Переходить на контракты сложно. Это же все менять нужно!
    Это тоже не совсем верно, точнее совсем не верно. Есть несколько простых шагов по безболезненной адаптации этой тулы. Начать проще всего с декорации существующих классов, типа Guard.NotNull атрибутом ContractArgumentValidatorAttribute (подробнее – тут), или путем добавления Contract.EndContractBlock после обычного if-throw. В этом случае, даже если библиотека Code Contracts не будет установлена на машине разработчика, поведение во время исполнения останется старым, а вы сможете получить новые плюшки (типа постусловий, инвариантов, генерации документации или даже статического верификатора).

Будущее

С будущим все довольно сложно.

Библиотека Code Contracts перешла в открытый доступ и ее развитием занимается комьюнити. После выпуска поддержки VS2015 это самое комьюнити достаточно неплохо активизировалось – появляются баги и, что не менее важно, появляются фиксы. Я планирую в ближайшее время допилить нормальные постусловия для асинхронных методов. Есть еще планы по доработке тулинга.

В ближайшее время репозиторий переедет в dotnet организацию на github-е, что упростит некоторые организационные вопросы. С точки же зрения официального развития со стороны Microsoft, то оно, скорее всего, закончено.

Но помимо Code Contracts, есть теоретические планы по добавлению контрактов в новую редакцию языка C# (вот обсуждение на гитхабе). Но пока что совсем не ясно, что из этого выйдет, каким функционалом будут обладать новые контракты, когда они появятся, и появятся ли они вообще.

Так использовать или нет?

Я всегда стараюсь разделять инструмент от практик разработки. Понятно, что в некоторых случаях отсутствие инструмента делает практику бесполезной, но это не тот случай.

Смысл-то не столько в Code Contracts, как в библиотеке, сколько в Design by Contracts, как способе разделения ответственности между программными компонентами. Берем новый класс. Задаем себе вопрос: а какой у него контракт? Что за предусловия, какие постусловия, насколько инварианты его простые и очевидные? Если ответить на эти вопросы нельзя, то инструмент не очень-то поможет. Нужно дизайн править!

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

Да, если что, это все мое личное мнение, официальное мнение Microsoft может быть похожим, не похожим, или вообще не знаю каким.

18 комментариев:

  1. Уже больше двух лет пользуюсь CodeContracts в проекте (благодаря этому блогу). Но в силу особенностей проекта: я единственный разработчик, WinForms приложение - насладиться плюсами не удалось, а погоревать от минусов довелось сполна. Первые месяцы проекта, пока статический анализатор справлялся - всё было супер, код писался дикими темпами, вставка нужных проверок спасала от долгих дебагов. Первая печалька была, когда пришлось отключить статический анализатор, но руки по привычке используя сниппеты вставляли проверки аргуменов, какое-то время...потом рукам стало лень...=(
    В солюшене больше 50 проектов и случается, что приходится ручками настраивать обработку контрактов...наточить топор нет времени и сил =(
    Инструмент такой хочется, хотя бы с минимальным функционалом, но максимально удобный, даже хочется за него заплатить.

    ОтветитьУдалить
    Ответы
    1. По поводу "наточить топор" для простой и легкой настройки множества проектов - можно сделать один файл с студийными пропертями, а во всех vcsproj просто заинклудить его.
      Я так вынес в один файл настройки по билду для почти 250 проектов в солюшене. В оригинальных файлах проекта осталось только имя, разные гуиды и референсы.
      После этого например смена фреймворка с 4 на 4.5.1 заняла час: 5 минут на редактирование файла, 25 минут на проверку компиляции. еще пол часа на автотесты. Так что рекомендую.

      Удалить
    2. Да, совет Евгения очень дельный. Это и правда лучший способ контроля за параметрами всех проектов в солюшене!

      Удалить
    3. Евгений, но все равно ведь при каждом новом проекте надо лезть в csproj и писать инклуд файла пропертей.
      Я к тому, что при динамичной разработке попробуй всей ораве разъясни зачем это надо делать и проконтролируй, чтобы не забыли.

      Удалить
    4. Проблнма решается путем использования своих common targets, в которых будет определено нужное свойство. Так никто ничего не забудет.

      Удалить
  2. Альтернатива контрактам - это использование системы типов и оверлоады. В Mercury есть mode'ы, в других функциональных языках используют "фантомные типы". В общем нелинейность проверки контрактов лечится правильной аннотацией значений которые могут попадать в функцию и которые могут выходить из функции.

    Например можно иметь что-то вроде:

    // annotation
    template NonZero { T value; operator T() const; };
    int safeDiv(int, NonZero);

    // handle both cases
    template
    R nonZero(std::function)>, std::function, T);

    // developers knows what he does
    template
    NonZero assertNonZero(T) noexcept;

    template
    NonZero assumeNonZero(T); // thorws something

    // can't write safeDiv(42,2) and safeDiv(42,0)
    int myfunc(int x)
    {
    retur nonZero([](NonZero x) { return 42/x;}, []() { return -1; }, x);
    }

    ОтветитьУдалить
    Ответы
    1. угловые скобочки посъедались. NonZero<int> и std::function<R(NonZero<T>)>

      Удалить
    2. В обсуждении на rsdn-е кто-то предложил похожий подход. Очень круто, но в обычном языке получается несколько многословно, ну и непонятно, как более сложные утверждения тут выражать...

      Удалить
  3. Я пользу от применения программирования по контакту (благодаря данному блогу) ощутил ещё года два назад. Выявлять, диагностировать и понимать требования к ПО становится намного легче, особенно на ранних стадиях разработки. Но вот, что касается Code Contracts для VS его, как уже отметил Сергей, его ещё допиливать и допиливать. Одно из основных препятствий, не считая вышеперечисленные, из-за чего пришлось отказаться от использования в некоторых проектах – автоматизированные билды, к машинам-агентам которых нет доступа (VSO в том числе). Там использовать CC не получится. А в целом было бы здорово иметь все это из коробки. Может user voice для этого организовать?

    ОтветитьУдалить
    Ответы
    1. Для этого можно использовать специальный NuGet: CodeContracts.MSBuild.
      Надо бы его тоже подпилить под новые платформы и обновить версию.

      Удалить
    2. Спасибо, не знал. Есть ли документация как использовать?

      Удалить
    3. Этот комментарий был удален автором.

      Удалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Сергей!

    Пробую использовать контракты. Второй день не могу решить проблему с методом
    Contract.EnsuresOnThrow(...);

    По какой-то причине он у меня не отрабатывается (в пошаговом режиме - попросту в него не заходит). В то время как Contract.Requires(...), Contract.Requires(...), Contract.Ensures(...) отрабатывают корректно. Попытки найти решение в поисковом сервисе ничего не дали.

    Например, в следующей функции Exception никогда не будет вызван:

    static void Main(string[] args)
    {
    Contract.EnsuresOnThrow(true);
    }

    У меня что-то не так с настройками?

    ОтветитьУдалить
    Ответы
    1. Я тут нафлудил - при публикации комментария обрезается TException в угловых скобках.

      Удалить
    2. Идея EnsuresOnThrow в том, чтобы "убедиться", что при возникновении исключения состояние объекта/мира находится в определенном состоянии.

      Вот пример метода, который упадет с нарушением постусловия:

      private static string field;
      static void Foo()
      {
      Contract.EnsuresOnThrow<Exception>(field == "hello");
      throw new Exception();
      }

      Этот метод говорит, что когда произойдет исключение, поле 'field' должно содержать значение "hello". Поскольку это не так, то текущий метод упадет не просто со сгенерированным исключением, а с ContractException-ом, в котором будет сказано, что исключительное постусловие не выполнено, и поле field не было равно "hello".

      Удалить
    3. На MSDN встречаются примеры, где Contract.Requires используется в середине методов. Например тут: https://msdn.microsoft.com/ru-ru/library/dd412883(v=vs.110).aspx
      Как такое возможно?

      Не совсем понятно, как быть, если нужно проверить входные параметры метода на соответствие какому-то сложному условию и вбросить исключение, если условие не выполняется.

      Например, пишу конструктор для треугольника. Хотелось бы написать так:

      private List sideList;
      private int perimeter;

      public Triangle(int a, int b, int c)
      {
      sideList = new List«int»(3) { a, b, c};
      perimeter = sideList.Sum();
      Contract.Requires«ArgumentOutOfRangeException»(Contract.ForAll(sideList, side > 0));
      Contract.Requires«ArgumentOutOfRangeException»(Contract.ForAll(sideList, side => side < perimeter - side));
      }

      А получается только так:

      private List«int» sideList;
      private int perimeter;
      public Triangle(int a, int b, int c)
      {
      Contract.Requires«ArgumentOutOfRangeException»(a> 0);
      Contract.Requires«ArgumentOutOfRangeException»(b> 0);
      Contract.Requires«ArgumentOutOfRangeException»(c> 0);
      Contract.Ensures(Contract.ForAll(sideList, side => side < perimeter - side));
      var sideList = new List«int»(3) { a, b, c};
      var perimeter = sideList.Sum();
      }

      Только в Ensures тут вбрасывается не ArgumentOutOfRangeException, а не ContractException. Возможно ли решить такую проблему при помощи контрактов, или лучше в таких случаях использовать if-throw ?

      Удалить
    4. *Только в Ensures тут вбрасывается не ArgumentOutOfRangeException, а ContractException.

      Удалить