Проектирование по контракту (Design by Contract) - это метод проектирования, являющийся центральным свойством языка Eiffel. И метод, и язык разработаны Бертраном Мейером [33]. Однако проектирование по контракту не является привилегией только языка Eiffel, этот метод можно применять и в любом другом языке программирования.
Главной идеей проектирования по контракту является понятие утверждения. Утверждение (assertion) - это булево высказывание, которое никогда не должно принимать ложное значение и поэтому может быть ложным только в результате ошибки. Обычно утверждение проверяется только во время отладки и не проверяется в режиме выполнения. Действительно при выполнении программы никогда не следует предполагать, что утверждение проверяется.
В методе проектирования по контракту определены утверждения трех типов: предусловия, постусловия и инварианты. Предусловия и постусловия применяются к операциям. Постусловие - это высказывание относительно того, как будет выглядеть окружающий мир после выполнения операции. Например, если мы определяем для числа операцию «извлечь квадратный корень», постусловие может принимать форму input = result * result, где result является выходом, a input - исходное значение числа. Постусловие — это хороший способ выразить, что должно быть сделано, не говоря при этом, как это сделать. Другими словами, постусловия позволяют отделить интерфейс от реализации.
Предусловие - это высказывание относительно того, как должен выглядеть окружающий мир до выполнения операции. Для операции «извлечь квадратный корень» можно определить предусловие input >= 0. Такое предусловие утверждает, что применение операции «извлечь квадратный корень» для отрицательного числа является ошибочным и последствия такого применения не определены.
На первый взгляд эта идея кажется неудачной, поскольку нам придется выполнить некоторые дополнительные проверки, чтобы убедиться в корректности выполнения операции «извлечь квадратный корень». При этом возникает важный вопрос: на кого ляжет ответственность за выполнение этой проверки.
Предусловие явным образом устанавливает, что за подобную проверку отвечает вызывающий объект. Без такого явного указания обязанностей мы можем получить либо недостаточный уровень проверки (когда каждая из сторон предполагает, что ответственность несет другая сторона), либо чрезмерную проверку (когда она будет выполняться обеими сторонами). Излишняя проверка тоже плоха, поскольку это влечет за собой дублирование кода проверки, что, в свою очередь, может существенно увеличить сложность про-
граммы. Явное определение ответственности помогает снизить сложность кода. Опасность того, что вызывающий объект забудет выполнить проверку, уменьшается тем обстоятельством, что утверждение обычно проверяется во время отладки и тестирования.
Исходя из этих определений предусловия и постусловия, мы можем дать строгое определение термина исключение. Исключение возникает, когда предусловие операции выполнено, но операция не может возвратить значение в соответствии с указанным постусловием.
Инвариант представляет собой утверждение относительно класса. Например, класс Account (Счет) может иметь инвариант, который утверждает, что balance == sum(entries.amount()). Инвариант должен быть «всегда» истинным для всех экземпляров класса. В данном случае «всегда» означает «всякий раз, когда объект доступен для выполнения над ним операции».
По существу это означает, что инвариант дополняет предусловия и постусловия, связанные со всеми открытыми операциями данного класса. Значение инварианта может оказаться ложным во время выполнения некоторого метода, однако оно должно снова стать истинным к моменту взаимодействия с любым другим объектом.
Утверждения могут играть уникальную роль в определении подклассов. Одна из опасностей наследования состоит в том, что операции подкласса можно переопределить так, что они станут не совместимыми с операциями суперкласса. Утверждения уменьшают вероятность этого. Инварианты и постусловия класса должны применяться ко всем подклассам. Подклассы могут усилить эти утверждения, но не могут их ослабить. С другой стороны, предусловия нельзя усилить, но можно ослабить.
На первый взгляд все это кажется излишним, однако имеет весьма важное значение для обеспечения динамического связывания. В соответствии с принципом замещения необходимо всегда иметь возможность обратиться к объекту подкласса так, как если бы он был экземпляром суперкласса. Если подкласс усилил свое предусловие, то операция суперкласса, примененная к подклассу, может завершиться аварийно.