Альтернативная проверка предусловий в Code Contracts

в 22:37, , рубрики: .net, code contracts, design by contract, метки: , , ,

При попытке использования библиотеки 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%MicrosoftContractsLanguageCSharp после установки библиотеки 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, которое будет вызываться при нарушении предусловий/постусловий/инвариантов. Например, перед запуском интеграционного или юнит-теста можно подписаться на это событие, и если падает предусловие, но тест остается зеленым, то можно закатывать рукава и идти искать того нерадивого программиста, который ловит исключения, не предназначенные для обработки.

Заключение

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

Автор: SergeyT

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js