При попытке использования библиотеки Code Contracts в реальном проекте может возникнуть небольшая сложность: хотя сам класс Contract с методами проверки предусловий и постусловий, располагается в mscorlib начиная с 4-й версии .NET Framework, но без установки самой библиотеки Code Contracts, они не попадают в результирующую сборку.
Это может вызвать определенные сложности в крупных распределенных командах, поскольку для нормального использования контрактов всем разработчикам придется установить дополнительное расширение. А поскольку у ключевых людей проекта может не быть четкой уверенности в том, а нужно ли вообще нам это добро, то такой переход может быть затруднительным.
Однако Code Contracts поддерживает дополнительный «режим совместимости», который позволяет «жестко зашить» проверки предусловий в результирующий код, так что они будут видны всем, не зависимо от того, установлены контракты на машине разработчика или нет.
Постановка проблемы
Давайте вначале рассмотрим пример, который более четко покажет, в чем проблема.
class SimpleClass
{
public int Foo(string s)
{
Contract.Requires(s != null);
return s.Length;
}
}
С этим кодом совершенно все в порядке и при попытке вызова метода Foo с null, мы получим нарушение контракта, что при установленной библиотеке Code Contracts и включенной проверке предусловий приведет к генерации исключения 'System.Diagnostics.Contracts.__ContractsRuntime.ContractException' .
Да, именно этого мы и ждем, но особенность заключается в том, что код генерации исключения генерируется не компилятором, а отдельным процессом, который запускается сразу после компиляции. А это значит, что без библиотеки Code Contracts, выполнение этого кода приведет к генерации NullReferenceExcpetion, поскольку никакой дополнительной валидации аргументов не останется и в помине. Я неоднократно сталкивался с тем, что такое поведение вызывало примерно такую реакцию: «WTF? Куда делась моя проверка!»
Поскольку мы не хотим слышать подобные “WTF?!?” от наших коллег, у которых контракты не установлены, то хотелось бы иметь способ зашить проверку предусловий более основательным образом.
Ручная проверка предусловий
Библиотека Code Contracts позволяет использовать предусловия в старом формате. Это значит, что если существующий метод уже содержит проверку входных параметров (т.е. проверку предусловий), то для преобразования их в полноценные предусловия после них достаточно добавить вызов Contract.EndContractBlock:
public class SimpleClass
{
public int Foo(string s)
{
if (s == null)
throw new ArgumentNullException("s");
Contract.EndContractBlock();
return s.Length;
}
}
Добавление вызова Contract.EndContractBlock превращает одну (или несколько) проверок входных параметров в полноценные предусловия. Теперь, для любого разработчика, у которого контракты не установлены, этот код будет выглядеть, как и раньше. В то время, как обладатели контрактов, смогут пользоваться всеми их преимуществами, такими как проверка валидности программы с помощью Static Checker-а, автоматическая генерация документации, возможность отлова всех нарушений контрактов (подробнее об этом будет ниже). Отличие этого способа проверки лишь в том, что их нельзя отключить и выпилить из кода полностью.
Данный подход можно совмещать с более продвинутыми техниками использования контрактов. Так, например, можно совмещать old-style проверку предусловий совместно с проверкой постусловий и инвариантов. Но поскольку постусловия и инварианты в большей мере касаются самого класса, а не его клиентов, то это никак не затронет всех тех разработчиков, у которых контракты не установлены.
ПРИМЕЧАНИЕ
Библиотека Code Contracts позволяет настраивать, какие проверки должны оставаться в результирующем коде. Если разработчики достаточно уверены в своем коде, то они могут убрать все проверки из кода и сэкономить несколько тактов процессора на каждой из них. Более подробно об уровнях мониторинга и возможностях по их управлению можно почитать в статье: «Мониторинг утверждений в период выполнения».
Использование существующих методов проверки
Еще одним стандартным способом валидации аргументов является использование специальных классов (guard-ов) с набором разных методов, типа NotNull, NotNullOrEmpty и т.п. Библиотека Code Contracts поддерживает возможность превращения подобных методов в полноценные контракты: для этого методы класса валидатора нужно пометить атрибутом ContractArgumentValidatorAttribute.
ПРИМЕЧАНИЕ
К сожалению атрибут ContractArgumentValidatorAttribute не входит в состав .NET Framework версии 4.0, он появится только в версии 4.5. Разруливается эта ситуация путем добавления в ваш проект файла ContractExtensions.cs, который появится в %ProgramFiles%\Microsoft\Contracts\Language\CSharp после установки библиотеки Code Contracts.
public static class Guard
{
[ContractArgumentValidatorAttribute]
public static void IsNotNull<T>(T t) where T : class
{
if (t == null)
throw new ArgumentNullException("t");
Contract.EndContractBlock();
}
}
Теперь мы можем использовать старый добрый метод IsNotNull для проверки предусловий:
public int Foo(string s)
{
Guard.IsNotNull(s);
return s.Length;
}
Отступление от темы. Contract.ContractFailed
Возможно, вы обращали внимание на существование двух версии метода Contract.Requires, одна из которых является обобщенной (generic) и может использоваться для генерации нужного типа исключения, нарушение же необобщенной версии приводит к генерации внутреннего (internal) исключение типа ContractException.
Причина, по которой по умолчанию генерируется внутреннее исключение, заключается в том, что нарушение контракта не может быть восстановлено программным путем. Это баг в коде и для его устранения необходимо изменение этого кода. Однако при использовании любого подхода к проверке предусловий (Contract.Requires + 2 рассмотренных сегодня подхода), пользователь может «захавать» исключение, перехватив базовый тип исключения.
В классе Contract есть событие ContractFailed, которое будет вызываться при нарушении предусловий/постусловий/инвариантов. Например, перед запуском интеграционного или юнит-теста можно подписаться на это событие, и если падает предусловие, но тест остается зеленым, то можно закатывать рукава и идти искать того нерадивого программиста, который ловит исключения, не предназначенные для обработки.
Заключение
Использование одного из описанных здесь подходов для перехода к контрактному программированию позволяет плавно мигрировать код на контракты, не ломая жизнь остальной части команды. При этом вы можете использовать старые средства валидации со всеми достоинствами контрактов (включая возможность узнать о нарушении предусловий, описанную в предыдущем разделе), не заставляя всех и каждого устанавливать дополнительные утилиты на свои машины.
Kak vam eto ?
ОтветитьУдалитьhttp://social.msdn.microsoft.com/Forums/en-US/codecontracts/thread/e2b9b734-2d6e-432b-9735-b3a2b6316c8c
Я так полагаю, контракты пытаются запомнить предыдущее состояние объекта для вычисления постусловия. Вот и вынесли "код запоминания" за скобки.
ОтветитьУдалитьТоска... Надеюсь, что это исправят.
Да и ограничение по видимости не радует. Класс, как правило, имеет закрытые данные и проверки в публичных методах, а тут получается надо делать ещё закрытые "проверочные" методы, чтобы у них был доступ к данным класса.
ОтветитьУдалитьИли я что-то упустил?..
@Alexander: не совсем понял о каких закрытых "проверочных" методах идет речь.
ОтветитьУдалитьВ контрактах существует такое понятие как "правило доступности предусловия", которое заключается в том, что вызывающий код должен иметь возможность убедиться в том, что он не нарушает контракт вызываемого кода. Но при этом такого правила нет для постусловий или инвариантов. Но я не уверен, что вы именно об этом.
Именно об этом. Если в своих if-then-throw (в public методах) я мог городить любые условия, то здесь получается в предусловиях можно использовать только public данные.
ОтветитьУдалитьДа, но это не баг, это фича.
ОтветитьУдалитьНарушение предусловия является багом в вызывающем коде, а это значит, что вызывающий код должен быть способным проверить, находится ли целевой объект в удовлетворительном состоянии или правильные ли аргументы.
Иногда это может потребовать выставлять дополнительное состояние или отказаться от предусловий, если это не возможно (поскольку формально, такие проверки предусловиями уже не являются).
В принципе да, обдумал, согласен. Откуда вызывающему знать что его запрос неверен если доступа к данным, определяющим это, у него нет (хотя от этого не легче).
ОтветитьУдалитьНу а практическое применение того, что вызывающий код может получить доступ к предусловиям есть? Не считая внутренних нужд самой этой системы.
Практическое следствие заключается только в понятности: мы точно знаем, что происходит с нашим классом и когда он находится в валидном или не валидном состоянии.
ОтветитьУдалитьСама идея проектирования по контракту более полезна с точки зрения дизайна как такового, а не с точки зрения поддержки инструментов. Подобное же ограничение просто уменьшит количество ситуаций, когда объект будет находится в непонятном состоянии (ибо он черный ящик) и будет не понятно, что с ним не так.