Выбрать главу

• Постусловия. Состояние окружающего мира на момент завершения работы подпрограммы – то, что гарантируется подпрограммой. Сам факт того, что в ней имеется постусловие, подразумевает, что подпрограмма завершит свою работу: бесконечные циклы не допускаются.

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

Рассмотрим контракт на создание программы, которая осуществляет вставку значения данных в упорядоченный список уникальных данных. При работе с 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]:

"Использование подклассов должно осуществляться через интерфейс базового класса, но при этом пользователь не обязан знать, в чем состоит различие между ними".

Другими словами, вы хотите убедиться в том, что вновь созданный подтип действительно "схож-с тем" (базовым) типом – что он поддерживает те же самые методы и эти методы имеют тот же смысл. Этого можно добиться при помощи контрактов. Контракт необходимо определить единожды (в базовом классе) с тем, чтобы он применялся к вновь создаваемым подклассам автоматически. Подкласс может (необязательно) использовать более широкий диапазон входных значений или же предоставлять более жесткие гарантии. Но, по крайней мере, подкласс должен использовать тот же интервал и предоставлять те же гарантии, что и родительский класс.

Рассмотрим базовый класс Java, именуемый java.awt.Component. Вы можете обрабатывать любой визуальный элемент в AWT или Swing как тип Component и не знать, чем является подкласс в действительности – кнопкой, подложкой, меню или чем-то другим. Каждый отдельный элемент может предоставлять дополнительные, специфические функциональные возможности, но, по крайней мере, он должен предоставлять базовые средства, определенные типом Component. Однако ничто не может помешать вам создать для типа Component подтип, который предоставляет методы с правильными названиями, приводящие к неправильным результатам. Вы легко можете создать метод paint, который ничего не закрашивает, или же метод setFont, который не устанавливает шрифт. AWT не обладает контрактами, которые способны обнаружить факт нарушения вами соглашения.

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

/**

* @pre f != null

* @post getFont() == f

 */

public void setFont(final Font f) {

//…

Реализация принципа ППК

Самая большая польза от использования принципа ПИК состоит в том, что он ставит вопросы требований и гарантий во главу угла. В период работы над проектом простое перечисление факторов – каков диапазон входных значений, каковы граничные условия, что можно ожидать от работы подпрограммы (или, что важнее, чего от нее ожидать нельзя), – является громадным шагом вперед в написании лучших программ. Не обозначив эти позиции, вы скатываетесь к программированию в расчете на совпадение (см. раздел "Программирование в расчете на стечение обстоятельств"), на чем многие проекты начинаются, заканчиваются и терпят крах.

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