Поэтому будем осторожными в этом мире несовершенных систем, устаревших временных масштабов, смешных инструментальных средств и невыполнимых требований.
Если все общество отклоняется от нормы, чтобы понять вас, скорее всего это паранойя.
Вуди Аллен
21
Проектирование по контракту
Ничто не ошеломляет людей так сильно, как здравый смысл и честная сделка.
Ральф Уолдо Эмерсон, Эссе
Работать с компьютерными системами всегда непросто. Работать с людьми еще сложнее. И поскольку мы (как биологический вид) развиваемся достаточно долго, то у нас явно было больше времени на выяснение природы человеческих взаимоотношений. Некоторые из тех решений, к которым мы пришли в течение нескольких последних тысячелетий, могут быть применены и к созданию программного обеспечения. Одним из лучших способов, с помощью которого можно удостовериться в честности заключаемой сделки, является контракт.
В контракте определены ваши права и обязанности, а также права и обязанности другой стороны. В дополнение к этому существует соглашение, касающееся юридических последствий, возникающих в том случае, если какая-либо из сторон окажется не в состоянии соблюдать условий контракта.
Допустим, у вас есть контракт на работу, где определены количество отрабатываемых часов и правила поведения, которым вы обязаны следовать. В ответ фирма платит вам заработную плату и предоставляет другие льготы. Каждая из сторон выполняет свои обязательства, а в результате выигрывают все.
Эта идея используется во всем мире – и формальным, и неформальным образом – для того, чтобы помочь людям во взаимодействии. Можем ли мы применить этот же принцип, чтобы способствовать взаимодействию программных модулей? Ответ на этот вопрос положительный.
Проектирование по контракту
Бертран Мейер [Меу97b] разработал концепцию проектирования по контракту для языка Eiffel [25]. Это простая, но мощная методика, сосредоточенная на документировании (и согласовании) прав и обязанностей программных модулей в целях обеспечения корректности программы. Так что же означает "корректная программа"? Это та программа, которая делает не более и не менее того, на что она претендует. Документирование и подтверждение указанных претензий лежит в основе принципа проектирования по контракту (в дальнейшем, для краткости, будем называть его ППК).
Каждая функция и метод в программной системе осуществляет некоторое действие. До того как подпрограмма начнет выполнять это действие, она может иметь некие виды на состояние окружающего мира, а также может констатировать состояние окружающего мира на момент завершения работы. Б. Мейер описывает эти виды и претензии следующим образом:
• Предусловия. Требования подпрограммы – то, что обязано быть истинным для того, чтобы подпрограмма могла вызываться. Если предусловия нарушены, программа не должна вызываться ни в коем случае. Ответственность за передачу качественных данных лежит на вызывающей программе (см. врезку ниже "Кто несет ответственность?").
• Постусловия. Состояние окружающего мира на момент завершения работы подпрограммы – то, что гарантируется подпрограммой. Сам факт того, что в ней имеется постусловие, подразумевает, что подпрограмма завершит свою работу: бесконечные циклы не допускаются.
• Инварианты класса. Класс гарантирует, что данное условие всегда истинно с точки зрения вызывающей программы. Во время внутренней обработки подпрограммы инвариант может и не выполняться, но к моменту выхода из подпрограммы и передачи управления обратно к вызывающей программе инвариант обязан быть истинным. (Следует заметить, что класс не может давать неограниченное право доступа для записи к любому элементу данных, участвующему в инварианте.)
Рассмотрим контракт на создание программы, которая осуществляет вставку значения данных в упорядоченный список уникальных данных. При работе с iContract (препроцессором для языка Java, который можно загрузить с [URL 17]) этот контракт может быть реализован следующим образом:
/**г
* ©invariant forall Node n in elements() |
* n.prev() != null
* implies
* n.value().compareTo(n.prev().value()) > 0
*/
public class DBC_list {
/**
* @pre contains(aNode) == false
* @post contains(aNode) == true
*/
public void insertNode(final Node aNode) {
//…
Здесь стоит сказать, что элементы в списке всегда должны располагаться в порядке возрастания. При вставке нового элемента этот порядок уже может быть нарушен, и мы гарантируем, что элемент будет обнаружен после того, как он был вставлен в список.
Вы формулируете эти предусловия, постусловия и инварианты на целевом языке программирования, возможно, с некоторыми расширениями. Например, iContract предоставляет операторы логики предикатов – forall, exists и implies, дополняя обычные конструкции языка Java. Ваши утверждения могут сделать запрос о состоянии любого объекта, к которому имеется доступ со стороны метода, но удостоверьтесь, что запрос не окажет никакого побочного воздействия (см. ниже врезку "Утверждения и побочные условия").
ППК и параметры-константы
Во многих случаях постусловие будет использовать параметры, переданные в метод, для проверки правильности поведения. Но если подпрограмме разрешено изменять переданный параметр, то у вас есть возможность обойти условия контракта. В отличие от языка Java, язык Eiffel не позволяет подобных действий. В данном случае для указания наших намерений, сводящихся к неизменяемости параметра в пределах метода, используется ключевое слово final (из языка Java). Это не является "защитой от дурака" – подклассы не имеют ограничений при повторном определении параметра как не являющегося окончательным. В качестве альтернативы можно использовать синтаксис variable@pre (принятый в iContract), чтобы получить исходное значение переменной, существовавшее на момент входа в метод.
Следовательно, в контракте между подпрограммой и любой потенциально вызывающей ее программой может быть записано следующее:
"Если вызывающая программа выполняет все предусловия подпрограммы, то подпрограмма гарантирует, что по завершении ее работы все постусловия и инварианты будут истинными".
Если одна из сторон нарушает условия контракта, то применяется предварительно согласованная мера, например, возбуждается исключение или происходит завершение работы программы. Что бы ни происходило, вы не ошибетесь, утверждая, что нарушение условий контракта есть ошибка. Это происходит далеко не всегда, и поэтому предусловия не должны использоваться для осуществления таких процедур, как проверка правильности данных, вводимых пользователем.
Подсказка 31: Проектируйте в соответствии с контрактами
В разделе «Ортогональность» рекомендуется создавать «скромные» программы. В данном случае упор делается на «ленивую» программу: проявите строгость в том, что вы принимаете до начала работы, и обещайте как можно меньше взамен. Следует помнить, что если в контракте указано, что вы принимаете все условия, а взамен обещаете весь мир, то вам придется написать… ну очень большую программу!
Наследование и полиморфизм являются краеугольными камнями объектно-ориентированных языков программирования и представляют собой область, в которой принцип программирования по контракту может проявиться особенно ярко. Предположим, что вы используете наследование при создании связи типа «это-схоже-с-тем», где один класс «схож-с-тем» (другим) классом. Вероятно, вы действуете в соответствии с принципом замещения, изложенным в книге "Liskov Substitution Principle" [Lis88]: