Это третья статья из цикла статей, посвященных теоретическим аспектам проектирования по контракту. На этот раз речь пойдет о том, какая связь между утверждениями, проверкой входных данных и защитным программированием, а также будет рассмотрен вопрос о том, что же происходит во время выполнения при нарушении утверждений.
Утверждения и проверка входных данных
Проектирование по контракту предназначено для формализации взаимоотношения двух программных элементов внутри доверенной среды и не предназначена для взаимодействия программного элемента с внешним миром. Главный принцип защищенного кода «все входные данные зловредны, пока не доказано противное» [Howard] остается в силе и о нем не следует забывать при разработке любых приложений, не зависимо от того, построены они по принципам проектирования по контракту или без него.
Рисунок 3 – Доверенная и ненадежная среда
Наличие предусловий в модулях ввода данных не являются достаточными, поскольку невозможно возложить ответственность за их соблюдение непосредственно на пользователя. Все данные, которые пересекают границу ненадежной и доверенной среды требуют явной и тщательной проверки. Принципы проектирования по контракту вступают в силу только внутри доверенной среды в виде постусловий модулей ввода.
Проектирование по контракту и защитное программирование
Одним из главных принципов проектирования по контракту является отсутствие проверок предусловий внутри тела программы. Это правило противоречит принципам защитного программирования, в котором “открытые методы класса предполагают, что данные небезопасны и отвечают за их проверку и исправление. Если данные были проверены открытыми методами класса, закрытые методы могут считать, что данные безопасны” [McConnell] При этом внутри открытых методов рекомендуется применять обработчики ошибок (например, генерировать исключение или возвращать соответствующий код ошибки, в случае неверных входных данных), а в закрытых методах применять утверждения (assertions) (поскольку это характеризует программные ошибки).
Как было показано в предыдущем разделе, в проектировании по контракту никто не отказывается от проверки ненадежных данных. Ключевое различие между контрактным и защитным программированием заключается в месте прохождения границы между ненадежной и доверенной средами. В проектировании по контракту эта граница проходит в модулях ввода и обработки входных данных, а в защищенном программировании она проходит по открытому интерфейсу любого класса.
Кроме того, в защитном программировании существует негласное правило, согласно которому “лишняя проверка никогда не повредит”, и действительно, если рассмотреть простой пример (например, функцию извлечения квадратного корня double Sqrt(double x)) может показаться, что никакого вреда от дополнительной проверки нет и быть не может, но так ли это на самом деле?
Основные принципы разработки (которые не имеют никакого отношения к контрактам) говорят о том, что лучше всего, чтобы класс или функция решала только одну задачу, но делала это хорошо. Одна дополнительная проверка внутри функции Sqrt в вашей домашней работе по информатике никакой погоды не сделает, но если говорить о реальных крупных проектах, то в них код обработки ошибок может занимать более половины всего кода.
Проектирование по контракту идет по пути “чем меньше и конкретнее задача, тем проще разработка, поддержка и сопровождение кода”. “С этой глобальной точки зрения простота становится критическим фактором. Сложность – главный враг качества… Добавляя избыточные проверки, добавляете больше кода. Больше кода – больше сложности, отсюда и больше источников условий, приводящих к тому, что все пойдет не так, это приведет к дальнейшему разрастанию кода и так до бесконечности. Если пойти по этой дороге, то определенно можно сказать одно – мы никогда не достигнем надежности. Чем больше пишем, тем больше придется писать” [Meyer2005].
Автор проектирования по контракту, Бертран Мейер считает, что обеспечение надежности отдельных модулей недостаточно для построения надежного ПО. “Для систем сколь либо существенных размеров недостаточно обеспечение качества отдельных элементов, - более важно гарантировать, что для каждого взаимодействия двух элементов задан явный список взаимных обязательств и преимуществ – контракт”.
Когда контракт нарушается
Мы говорили о том, что поставщик не должен проверять предусловие в своем коде, он может положиться на его выполнение и не рассматривать случаи его нарушения. Но что же произойдет во время выполнения при нарушении контракта и что семантически значит нарушение предусловия, постусловия или инварианта? Давайте попробуем найти ответы на все эти вопросы.
Нарушение контракта означает отклонение конкретной реализации от заданной спецификации, что говорит о некорректности реализации и является проявлением ошибок (или “жучков”) в программном коде.
Нарушение предусловия в период выполнения означает наличие ошибок на стороне клиента (поскольку за выполнение предусловия отвечает клиент; в этом случае «виноват заказчик» и ошибки находятся в его коде). При этом выполнение заданной функции не является целесообразным. Нарушение постусловия означает нарушение контракта со стороны поставщика, что в свою очередь означает наличие ошибок в реализации услуги (при этом предполагается, что вызов метода осуществляется при выполненном предусловии).
Как мы говорили ранее, инвариант класса можно рассматривать как добавление еще одного утверждения в предусловия и постусловия всех экспортируемых методов класса, и поскольку его соблюдение ложится на поставщика услуги, то нарушение инварианта эквивалентно нарушению постусловия (т.е. это ошибка в коде поставщика).
Конкретные проявления нарушения контракта во время выполнения определяются, прежде всего, уровнем мониторинга периода выполнения (который может регулироваться при компиляции приложения). Так, техника проектирования по контракту изначально предусматривает возможность изменения степени мониторинга, в зависимости от уверенности в качестве полученного кода. В общем случае, при включенном мониторинге нарушение утверждения в период выполнения приводит к генерации исключения, но некоторые реализации позволяют выбирать поведение между исключениями и стандартным механизмом нарушения утверждений (которое приводит к прерыванию работы приложения).
Мне не удалось понять в чем преимущество проектирования по контракту перед защищенным программированием. Цитата из статьи: “С этой глобальной точки зрения простота становится критическим фактором. Сложность – главный враг качества… Добавляя избыточные проверки, добавляете больше кода", - предполагает, что при контрактном проектировании получается меньше кода, но не понятно почему это должно быть так. Насколько я понял, предлагается заменить все проверки корректности входных данных на проверки выполнения условий контракта - кажется, это требует примерно одинакового количества кода. В чем же преимущество?
ОтветитьУдалить@m-ustinov: Разница в том, что в случае применение контрактов проверка предусловий ограничивается публичной частью класса/модуля, а внутри модуля эти проверки отсутствуют.
ОтветитьУдалитьЗащитное программирование обычно придерживается более параноидальной точки зрения, предлагая проверять все и вся, где только можно.
В случае же контрактов мы самим дизайном системы "отрезаем" невозможное состояние системы, что делает рассуждение о ней и ее сопровождение существенно более простой задачей.
(Сравните класс, который написан таким образом, что его проектировщики думали об инварианте класса. В этом случае, внутри методов нам не нужно проверять множество "невозможных" состояний. Именно в этом заключается идея контракта: строгость, которая ведет к простоте).
>Защитное программирование обычно придерживается более параноидальной точки зрения, предлагая проверять все и вся, где только можно.
ОтветитьУдалитьНасколько я понимаю, не совсем так. В самоё же статье выше написано:
"Это правило противоречит принципам защитного программирования, в котором “открытые методы класса предполагают, что данные небезопасны и отвечают за их проверку и исправление. Если данные были проверены открытыми методами класса, закрытые методы могут считать, что данные безопасны”
Так, давно это было, поэтому тонкие моменты я уже могу и подзабыть.
УдалитьЕсли не ошибаюсь, то разница между этими двумя подходами еще и в том, что в защитном программировании проверки могут быть не только в открытых методах, но и дублироваться в закрытых методах.
В контрактном же программировании, "валидация аргументов" как таковая отсутствует (вместо нее используются более декларативные контракты) и в закрытых методах проверок нет никаких (там нет даже контрактов), что существенно уменьшает суммарное количество проверок.
Я знаю, я поздно присоединяюсь к обсуждению, но попробую...
ОтветитьУдалитьУ меня есть ощущение что для защиты приходится добавлять проверки во внутренние методы, но для осмысленности сообщения ошибки дублировать его и во внешнем. Понимаю, что этого можно было бы избежать, но на практике почему-то это постояннно повторяется.
Еще не понятно как обрабатывать ошибки. Какое исключение с каким сообщенрем кидать? Я не знаю до куда исключение дойдет и какая детализация сообщения нужна. Например сообенте "value can't be negative" из вызова Circle.AreaForRadius(-1) логично, а из new Circle(-1).CalculateArea() -- нет. (глупый пример, знаю, но надеюсь понятно)
С контрактами мне кажутся полезными две вещи, по сравнению с защитным программированием:
1. Не надо думать, как обработать ошибку. Как описать условия понятно интуитивно. Над ошибкой надо дополнительно думать в контексте.
2. Предусловия, постусловия, инварианты избавляют от неоднозначности, кто виноват.