Code contracts: предусловия
Какую цель преследует использование контрактов в коде, а именно предусловий, постусловий и инвариантов? Идея тесно связана с Fail-fast принципом: чем быстрее вы заметите неожиданное поведение, тем быстрее вы его исправите.
В большинстве случаев более эффективно дать приложению упасть, нежели пытаться автоматически восстановиться после ошибки, т.к. вы никогда не знаете наверняка, что именно это за ошибка и как она повлияет на приложение. Продолжение работы софта после ошибки в нем может привести к неконсистентному состоянию данных в БД.
Так что же такое предусловия контрактов и как они отличаются от валидации входящих данных? Ключевой момент здесь в том, что нарушение предусловия всегда говорит о наличие бага в коде клиента. Неверные входящие данные, с другой стороны, не являются ошибкой в работе системы.
Это основное отличие, все остальные являются следствием из него. Давайте посмотрим на них подробнее.
Отличия между предусловиями контрактов и валидацией входящих данных
Предусловия контрактов можно представить в виде защитного экрана, помещенного внутри вашего вашего кода для того, чтобы убедиться, что все идет хорошо. С другой стороны, валидация входящих данных — это экран, размещенный для «обороны» от внешнего мира:
Красные сигналы обозначают неверные взаимодействия (или невалидные данные), идущие от пользователей или других приложений. Зеленые сигналы означают допустимые взаимодействия. Ваша цель как разработчика — убедиться, что невалидные дынные не могут проникнуть внутрь системы.
Если красный сигнал появляется внутри вашей системы, это означает, что либо вы не отфильтровали входящие данные, либо ваш собственный код генерирует неверные данные.
В любом случае, это является багом в системе, который нужно локализовать и устранить как можно быстрее. И в этом как раз нам помогают контракты. Они позволяют остановить распространение невалидных данных по приложению и быстро выявить причину возникновения багов:
Когда ваше приложение имеет хороший набор предусловий контрактов (и, еще лучше, постусловий и инвариантов), невалидные взаимодействия локализуются и их распространение прекращается сразу же как только они возникают, что очень сильно облегчает отладку приложения.
Валидация входящих данных — это механизм, направленный на защиту вашего приложения от проникновения неверных данных извне. Этот механизм не делает никаких предположений о данных, с которыми он работает. Это означает, что данные могут быть невалидными, и это само по себе — валидная ситуация.
В самом деле, если пользователь вводит «десять» в целочисленное поле, вы не хотите чтобы ваше приложение обрушилось. Вместо этого, вы вежливо указываете пользователю на ошибку.
В другой стороны, предусловия контрактов делают предположения о том, что данные внутри вашей системы находятся в валидном состоянии. Если это не так, в ваше приложение закралась ошибка.
Предусловия контрактов: best practices
Давайте посмотрим, что может рассматриваться как предусловие контракта. Контракт — это публичная оферта, предложенная классом-сервисом. В ней говорится, что если клиент следует некоторым правилам (предусловиям), то сервис гарантирует некоторые результаты, описанные в постусловиях.
Это приводит к следующим характеристикам, которым должно следовать каждое предусловие контракта:
- Предусловия должны быть публичными. Это означает, что разработчик класса-клиента должен иметь возможность узнать о них перед тем как начнет писать код.
- Они должны быть легко проверяемыми. Разработчик клиента не должен писать сложных алгоритмов для их проверки перед вызовом метода.
- Предусловия не должны полагаться на непубличное состояние, т.к. это ограничивает возможности клиента по их предварительной проверке.
- Они должны быть стабильны. Это означает, что результат выполнения проверки предусловия не должен зависеть от внешнего состояния.
Давайте рассмотрим эти пункты более подробно. Первый довольно прост, он означает, что все предусловия должны быть каким-то образом описаны, так что разработчик может ознакомиться с ними перед использованием класса. C# пока что не позволяет встраивать контракты прямо в сигнатуру методов, так что наилучшим на текущий момент способом является описание их в начале метода.
Второй пункт означает, что сервисный класс не должен заставлять разработчика класса-клиента выполнять сложные проверки для того, чтобы удовлетворить контракт. Если алгоритм проверки сложен, его следует вынести в отдельный метод, который клиенты смогут использовать для валидации предусловия:
public int Distribute(int amount)
{
Contract.Requires(CanDistribute(amount));
return DistributeCore(amount);
}
Следующий пункт следует из этого: вы не должны делать ваши предусловия зависимыми от непубличных методов или полей, иначе клиенты не смогут делать соответствующие проверки перед взаимодействием с классом.
Последний пункт говорит от повторяемости результата проверки. Если предусловия класса зависят от внешней среды — к примеру, существования файла на диске, — клиенты не смогут добиться успешного выполнения метода в 100% случаев:
public string ReadFile(string filePath)
{
Contract.Requires(File.Exists(filePath));
// Rest of the method
}
В этом примере файл может быть удален или стать недоступным в промежутке между вызовом метода ReadFile и началом проверки контракта, и клиент не может ничего сделать для того, чтобы избежать этого. Такая проверка не является предусловием контакта, т.к. она не говорит о наличии бага в клиентском коде, поэтому мы не можем вносить ее как предусловие.
Заключение
Предусловия контактов и валидация входящих данных имеют схожее, но тем не менее различное предназначение. В то время как валидация входящих данных позволяет удостовериться, что система защищены от внешнего мира, предусловия контрактов защищают ваш код внутри системы и позволяют быстро локализовать и устранить ошибки в ней.
Ссылка на оригинал статьи: C# code contracts vs input validation
Комментарии (16)
EvilsInterrupt
04.08.2015 11:52Прошу пояснить вот это:
>>нарушение предусловия всегда говорит о наличие бага в коде клиента
В статье идет речь о том, что предусловие это внутренняя проверка. Т.е. за 'firewall'-ом, в роли которого выступает «проверка входных данных». Как это может быть багой в коде клиента в случае, если клиент передал данные удовлетворяющие входному условию?vkhorikov
04.08.2015 13:49В коде клиента — имеется ввиду в коде класса-клиента внутри системы, а не в коде системы, которая вызывает API извне.
Т.е. на первой картинке баг будет в классе Class1.
chumakov-ilya
06.08.2015 22:25Чем технически различаются контракты и валидация? Верно ли, что «правильные» контракты должны производить статическую проверку на этапе компиляции, т.е. на if-ах и assert-ах контракт нереализуем? Есть ли альтернативы у проекта CodeContracts?
vkhorikov
07.08.2015 00:24Контракты в коде — это набор техник, направленных на
1) Документирование поведения кода
2) Обеспечение быстрого фидбека в случае некорректного поведения кода
Проект CodeContracts и статические проверки — инструментарий поверх этих техник. Они полезны, но не являются обязательными.
Мы точно также можем обозначить контракты в коде (как минимум предусловия) с помощью самописных конструкций (сводящихся к обычным if-ам), главное чтобы было очевидно, что это именно контакты, а не что-то другое.EvilsInterrupt
07.08.2015 12:17Допустим в моей библиотеке есть класс, который использует программист-пользователь в своем коде. Этот класс CodeReview и допустим в нем есть метод public(filename). Метод берет файл, шлет на веб-ресурс этот файл и публикует файл для коде-ревью. Задачка из головы, метод тоже. Это чтобы предметнее рассуждать.
Есть ситуации:
1. Файла не существует
2. К файлу нет прав по чтению
3. Файл не текстовой, а бинарный к примеру exe-файл с переименованным расширением. Т.е. это не код с точки зрения проводящих коде-ревью
4. Файл слишком огромный. Программист очень постарался(написал скрипт, чтобы сломать систему) и создал файл на 25ГБ
и др. но остановимся
Как надо, с вашей точки зрения входные условия и как контракты? ;)vkhorikov
07.08.2015 14:17Все описанные вами проверки не являются контрактами, предусловия контрактов не должны зависить от состояния внешних по отношению к коду систем (в вашем случае это диск).
EvilsInterrupt
07.08.2015 15:40Я и не говорю, что это проверки. Просто привел различные ситуации, которые могут произойти и задал вопрос о том как надо написать код чтобы получить проверку входного условия и какой надо написать код для получения контракта?
З.Ы.: Возможно последнее предложение не совсем ясное. Прошу прощенияvkhorikov
07.08.2015 16:56Если я правильно понял, вопрос в том, как правильно обозначать проверки в коде и как контракты?
У меня в коде это обычно выглядит так. Предусловия контактов обозначаю через самописный класс Contracts (внутри которого if с выбрасыванием специального исключения в случае невыполнения условия):
public void DeleteUser(User user) { Contracts.Require(user.Status != UserStatus.Deleted); user.MarkAsDeleted(); }
Проверки выполняются обычными if-ами:
Organization org = _organizationRepository.GetById(model.OrganizationId); if (orgOrNothing == null) return Error("No organization found for Id = " + model.OrganizationId);
Так при чтении кода становится понятно что есть что.chumakov-ilya
07.08.2015 19:42Спасибо за пример. Верно ли, что результат невыполнения контракта — всегда исключение, а при валидации ситуация обрабатывается внутри метода?
Если внутри GetById проверить OrganizationId > 0, это будет контракт или валидация?vkhorikov
07.08.2015 20:46+1Верно ли, что результат невыполнения контракта — всегда исключение, а при валидации ситуация обрабатывается внутри метода?
Верно. Причем исключения эти не должны отлавливаться в коде (разве что для логирования ошибки), приложению в этом случае нужно дать упасть (fail-fast прицнип). Более подробно на тему исключений: habrahabr.ru/post/263685
Если внутри GetById проверить OrganizationId > 0, это будет контракт или валидация?
В целом ничего не мешает обозначить это как контракт, но с точки зрения best practices контрактами лучше всего помечать значимые с точки зрения модеолирования условия. К примеру то, что юзера нельзя удалить если он уже удален — значимое условие и дает разработчику важную информацию о доменной модели приложения. Еще пример:
public class Organization : Entity { public void ProvisionUser(User user, Subscription subscription) { Contracts.Require(UsersInternal.Contains(user)); Contracts.Require(SubscriptionsInternal.Contains(subscription)); Contracts.Require(subscription.SeatsRemaining > 0); Contracts.Require(subscription.IsProvisionable); Contracts.Require(!user.IsProvisionedTo(subscription)); subscription.AddSeat(user); user.AddSeat(subscription); } }
Здесь мы даем кучу информации о том, какие условия должны быть соблюдены, чтобы подписать юзера на subscription, клиент этого кода должен быть ответственен за их выполнение перед вызовом ProvisionUser.
Заметил, что я не ответил на этот вопрос:
Чем технически различаются контракты и валидация?
Технически разницы нет. Весь инструментарий поверх контрактов — опционален. Разница в том, как трактовать результат невыполнения условия. В случае с контрактами невыполнение условия — это всегда ошибка в приложении, с точки зрения валидации, — обычное дело, которое можно выводить юзеру в качестве ошибки.
EvilsInterrupt
07.08.2015 21:52Ваш DeleteUser() это метод внутренний или же он вызывается в пользовательском коде?
vkhorikov
07.08.2015 21:58Это часть публичного API одного из классов доменной модели.
EvilsInterrupt
07.08.2015 22:14ОК.
Другими словами, Вы называете предусловия — контрактами?
Есть книга «Touch of Class» и там очень хорошо про предусловия, постусловия, инварианты.vkhorikov
07.08.2015 22:22Да, под предусловиями я имею ввиду именно предусловия контрактов. Я выделяю предусловия (и оставляюю за скобками инварианты и постусловия), т.к. предусловия обычно путают с валидацией приходящих извне данных, с постусловиями и инвариантами в этом плане проще.
Конкретно эту книгу Мейера я не читал, но вообще первоисточник моих знаних о контрактах именно Мейер, конкретно эта его книга: www.amazon.com/Object-Oriented-Software-Construction-CD-ROM-Edition/dp/0136291554
vladimirkolyada
Учитывая, что контракты теперь пилят не так активно (можно сказать отказались от них, от конкретной реализации), как раньше, то это вызывает боль. Более того, было заявлено, что они сильно замедляют процесс сборки, но не скрою, сам на это я не наткнулся — у меня их не так много используется.
vkhorikov
Есть вероятность, что их добавят на уровне самого языка в одной из следующий версия C#: github.com/dotnet/roslyn/issues/119