Переопределение и утверждения
Если клиент класса POLYGON вызывает p.perimeter, то он ожидает получить значение периметра p, определенное спецификацией функции perimeter в определении этого класса. Но теперь, благодаря динамическому связыванию, клиент может вызвать другую программу, переопределенную в некотором классе-потомке. В классе RECTANGLE переопределение улучшает эффективность и не изменяет результат, но что помешало бы переопределить периметр так, чтобы новая версия вычисляла бы, скажем, площадь?
Это противоречит духу переопределения. Переопределение должно изменять реализацию процедуры, а не ее семантику. К счастью, утверждения позволяют ограничить семантику процедур. Неформально, основное правило контроля за переопределением и динамическим связыванием можно сформулировать просто: предусловие и постусловие программы должны быть применимы к любому ее переопределению, и, как мы уже видели, инвариант класса автоматически должен распространяться на всех его потомков.
Точные правила будут приведены ниже. Но уже сейчас можно заметить, что переопределение не является произвольным: допускаются только переопределения, сохраняющие семантику. Это дело автора программы - выразить ее семантику достаточно точно, но оставить при этом свободу для будущих реализаторов.
О реализации динамического связывания
Может возникнуть опасение, что динамическое связывание - это дорогой механизм, требующий во время выполнения поиска по графу наследования и поэтому накладных расходов, растущих с увеличением глубины этого графа.
К счастью, это не так в случае хорошо спроектированного (и статически типизированного) ОО-языка. Более детально это будет обсуждаться в конце лекции, но мы можем уже сейчас успокоить себя тем, что последствия динамического связывания не будут существенными для эффективности при работе в подходящем окружении.
Отложенные компоненты и классы
Полиморфизм и динамическое связывание означают, что в процессе проектирования ПО можно рассчитывать на абстракции и быть уверенными в том, что при выполнении будет выбрана подходящая реализация. Но перед выполнением все должно быть полностью реализовано.
Однако полная реализация не всегда нужна. Частично реализованные или не реализованные абстрактные элементы ПО помогают при решении многих задач: анализе проблемы и проектировании архитектуры системы (в этом случае можно их сохранить в заключительном продукте, чтобы запомнить ход анализа и проектирования), при фиксации соглашений между реализаторами, при описании промежуточных точек в классификации.
Отложенные компоненты и классы обеспечивают необходимый механизм абстракции.
Движения произвольных фигур
Чтобы понять необходимость в отложенных процедурах и классах, снова рассмотрим иерархию фигур FIGURE.
Рис. 14.8. Снова иерархия FIGURE
Наиболее общим понятием здесь является FIGURE. Основываясь на механизмах полиморфизма и динамического связывания, можно попытаться применить описанную ранее общую схему:
transform (f: FIGURE) is
-- Применить специфическое преобразование к f.
do
f.rotate (...)
f.translate (...)
end
с соответствующими значениями опущенных аргументов. Тогда все следующие вызовы корректны:
transform (r) -- для r: RECTANGLE
transform (c) -- для c: CIRCLE
transform (figarray.item (i)) -- для массива фигур: ARRAY [POLYGON]
Иными словами, требуется применить преобразования rotate и translate к фигуре f и предоставить механизму динамического связывания выбор подходящей версии (различной для классов RECTANGLE и CIRCLE), зависящей от текущего вида фигуры f, который выяснится во время выполнения.
Это действительно работает и является типичным примером элегантного стиля, ставшего возможным благодаря полиморфизму и динамическому связыванию, стиля, основанного на принципе Единственного выбора. Требуется только переопределить rotate и translate для различных вовлеченных в вычисление классов.
Но переопределять-то нечего! Класс FIGURE - это очень общее понятие, покрывающее все виды двумерных фигур. Ясно, что невозможно написать версию процедур rotate и translate, подходящую для всех фигур "вообще", не уточнив информацию об их виде.
Таким образом, мы имеем ситуацию, в которой процедура transform будет выполняться корректно, благодаря динамическому связыванию, но статически она незаконна, поскольку rotate и translate не являются компонентами класса FIGURE. Проверка типов выявит в вызовах f.rotate и f.translate ошибки.
Можно, конечно, ввести на уровне класса FIGURE процедуру rotate, которая ничего не будет делать. Но это опасный путь, компоненты rotate (center, angle) имеют интуитивно хорошо понятную семантику и "ничего не делать" не является их разумной реализацией.
Отложенный компонент
Таким образом, нужен способ спецификации компонентов rotate и translate на уровне класса FIGURE, который возлагал бы обязанность по их фактической реализации на потомков этого класса. Это достигается объявлением этих компонентов как "отложенных". При этом вся часть тела процедуры с командами заменяется ключевым словом deferred. В классе FIGURE будет объявление:
rotate (center: POINT; angle: REAL) is
-- Повернуть на угол angle вокруг точки center.
deferred
end
и аналогично будет объявлен компонент translate. Это означает, что этот компонент известен в том классе, где появилось такое объявление, но его реализации находятся в классах - собственных потомках. В таком случае вызов вида f.rotate в процедуре transform становится законным.
Объявленный таким образом компонент называется отложенным компонентом. Компонент, не являющийся отложенным, - имеющий реализацию (например, любой из ранее встретившихся нам компонентов), называется эффективным.
Эффективизация компонента
В некоторых собственных потомках класса FIGURE потребуется заменить отложенную версию эффективной. Например,