Наследование и полиморфизм являются краеугольными камнями объектно-ориентированных языков программирования и представляют собой область, в которой принцип программирования по контракту может проявиться особенно ярко. Предположим, что вы используете наследование при создании связи типа «это-схоже-с-тем», где один класс «схож-с-тем» (другим) классом. Вероятно, вы действуете в соответствии с принципом замещения, изложенным в книге «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) {
//…
Самая большая польза от использования принципа ПИК состоит в том, что он ставит вопросы требований и гарантий во главу угла. В период работы над проектом простое перечисление факторов – каков диапазон входных значений, каковы граничные условия, что можно ожидать от работы подпрограммы (или, что важнее, чего от нее ожидать нельзя), – является громадным шагом вперед в написании лучших программ. Не обозначив эти позиции, вы скатываетесь к программированию в расчете на совпадение (см. раздел «Программирование в расчете на стечение обстоятельств»), на чем многие проекты начинаются, заканчиваются и терпят крах.
В языках программирования, которые не поддерживают в программах принцип ППК, на этом можно было бы и остановиться – и это неплохо. В конце концов, принцип ППК относится к методикам проектирования. Даже без автоматической проверки вы можете помещать контракт в текст программы (как комментарий) и все равно получать от этого реальную выгоду. По меньшей мере, закомментированные контракты дают вам отправную точку для поиска в случае возникновения неприятностей.
Утверждения
Документирование этих предположений уже само по себе неплохо, но вы можете извлечь из этого еще большую пользу, если заставите компилятор проверять имеющийся контракт. Отчасти вы можете эмулировать эту проверку на некоторых языках программирования, применяя так называемые утверждения (см. «Программирование утверждений»). Но почему лишь отчасти? Разве вы не можете использовать утверждения для всего того, на что способен принцип ППК?
К сожалению, ответ на этот вопрос отрицательный. Для начала, не существует средств, поддерживающих распространение действия утверждений вниз по иерархии наследования. Это означает, что если вы отменяете метод базового класса, у которого имеется свой контракт, то утверждения, реализующие этот контракт, не будут вызываться корректно (если только вы не продублируете их вручную во вновь написанной программе). Не забывайте, что прежде чем выйти из любого метода необходимо вручную вызвать инвариант класса (и все инварианты базового класса). Основная проблема состоит в том, что контракт не соблюдается автоматически.
Читать дальше