Опубликован: 17.10.2005 | Уровень: специалист | Доступ: свободно
Лекция 16:

Техника наследования

Аннотация: Наследование - ключевая составляющая ОО-подхода к повторному использованию и расширяемости. В этой лекции нам предстоит исследовать новые возможности, разнородные, но демонстрирующие замечательные следствия красоты базисных идей.

Следствия красоты базисных идей:

  • Связь наследования с утверждениями и Проектированием по Контракту.
  • Глобальная структура наследования, где все классы согласованы.
  • Замороженные компоненты, для которых не применим принцип Открыт-Закрыт.
  • Ограниченная универсальность: как задавать требования на родовые параметры.
  • Попытка присваивания: как безопасно приводить к типу.
  • Как и когда изменять свойства типа при повторных объявлениях.
  • Закрепленные объявления, помогающие избежать лавины переобъявлений.
  • Непростые отношения между наследованием и скрытием информации.

Вопросам наследования будут посвящены еще две лекции: обзор проблем типизации представлен в "Типизация" , а подробное обсуждение методологии наследования - в лекции 6 курса "Основы объектно-ориентированного проектирования".

Большинство разделов этой лекции строится по единому принципу: экзаменуются следствия идей предыдущих двух лекций, обнаруживаются проблемы, они подробно анализируются, предлагается обоснованное решение. Ключевым является шаг анализа - как только проблема становится ясной, зачастую решение ее находится сразу же.

Наследование и утверждения

Обладая изрядной мощью, наследование может быть и опасным. Не будь механизма утверждений, создатели классов могли бы весьма "вероломно" пользоваться повторными объявлениями и динамическим связыванием для изменения семантики операций без возможности контроля со стороны клиента. Утверждения способны на большее: они дают нам более глубокое понимание природы наследования. Не будет преувеличением сказать, что лишь понимание принципов Проектирования по Контракту позволяет в полной мере постичь сущность концепции наследования.

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

Инварианты

С правилом об инвариантах класса мы встречались и прежде:

Правило родительских инвариантов

Инварианты всех родителей применимы и к самому классу.

Инварианты родителей добавляются к классу. Инварианты соединяются логической операцией and then. (Если у класса нет явного инварианта, то инвариант True играет эту роль.) По индукции в классе действуют инварианты всех его предков, как прямых, так и косвенных.

Как следствие, выписывать инварианты родителей в инварианте потомка еще раз не нужно (хотя семантически такая избыточность не вредит: a and then a есть то же самое, что a ).

Полностью восстановленный инвариант класса можно найти в плоской и краткой плоской форме последнего (см. "Множественное наследование" ).

Предусловия и постусловия при наличии динамического связывания

В случае с предусловиями и постусловиями ситуация чуть сложнее. Общая идея, как отмечалось, состоит в том, что любое повторное объявление должно удовлетворять утверждениям оригинальной подпрограммы. Это особенно важно, если подпрограмма отложена: без такого ограничения на будущую реализацию, задание предусловие и постусловий для отложенных подпрограмм было бы бесполезным или, хуже того, привело бы к нежелательному результату. Те же требования к предусловию и постусловию остаются и при переопределении эффективных подпрограмм.

Анализируя механизмы повторного объявления, полиморфизма и динамического связывания, можно дать точную формулировку искомого правила. Но для начала представим типичный случай.

Рассмотрим класс и его подпрограммы, имеющие как предусловие, так и постусловие:

Подпрограмма, клиент и контракт

Рис. 16.1. Подпрограмма, клиент и контракт

На рис. 16.1 показан клиент C класса A. Чтобы быть клиентом, класс C, как правило, включает в одну из своих подпрограмм объявление и вызов вида:

a1: A
...
a1.r

Для простоты мы проигнорируем все аргументы, которые может требовать r, и положим, что r является процедурой, хотя наши рассуждения в равной мере применимы и к функциям.

Вызов будет корректен лишь тогда, когда он удовлетворяет предусловию. Гарантировать, что C соблюдает свою часть контракта, можно, к примеру, предварив вызов проверкой предусловия, написав вместо a1.r конструкцию:

if \ a1.\alpha \ then
\\
        a1.r
\\
                 check \ a1.\beta \ end     - \ постусловие \ должно \ выполняться
\\
        \dots  \ Инструкции, \ которые \ могут \ предполагать \ истинность \ a1 \dots 
\\
end

(Как отмечалось при обсуждении утверждений, не всегда требуется проверка: достаточно, с помощью if или без него, гарантировать выполнение условия a перед вызовом r. Для простоты будем использовать if -форму, игнорируя предложение else.)

Обеспечив соблюдение предусловия, клиент C рассчитывает на выполнение постусловия a1.\beta при возврате из r.

Все это является основой Проектирования по Контракту: в момент вызова подпрограммы клиент должен обеспечить соблюдение предусловия, а в ответ при возврате из подпрограммы он полагается на выполнение постусловия.

Что происходит, когда вводится наследование?

Подпрограмма, клиент, контракт и потомок

Рис. 16.2. Подпрограмма, клиент, контракт и потомок

Пусть новый класс A' порожден от A и содержит повторное объявление r. Как он может, если вообще может, заменить прежнее предусловие \alpha новым \gamma, а прежнее постусловие \beta - новым \delta?

Чтобы найти ответ, рассмотрим обязательства клиента. В вызове a1.r цель a1 может - в силу полиморфизма - иметь тип A'. Однако C об этом не знает! Единственным объявлением a1 остается исходная строка

a1: A

где упоминается A, но не A'. На деле C может использовать A', даже если его автор не знает о наличии такого класса. Вызов подпрограммы r может произойти, например, в процедуре C вида:

some_routine_of_C (a1: A) is
         do
                 ...; a1.r;...
         end

Тогда при вызове some_routine_of_C из другого класса в нем может использоваться фактический параметр типа A', даже если в тексте клиента C класс A' нигде не упоминается. Динамическое связывание как раз и означает тот факт, что обращение к r приведет в этом случае к использованию переопределенной версии A'.

Итак, может сложиться ситуация, в которой C, являясь только клиентом A, фактически во время выполнения использует версии компонентов класса A'. (Можно сказать, что C - "динамический клиент" A', хотя в тексте C об этом и не говорится.)

Что это значит для C? Только одно - проблемы, которые возникнут, если не предпринять никаких действий. Клиент C может добросовестно выполнять свою часть контракта, и все же в результате он будет обманут. Например,

if\ a1.\alpha\  then \a1.r\ end

если a1 полиморфно присоединена к объекту типа A', инструкция вызовет подпрограмму, ожидающую выполнения \gamma и гарантирующую выполнение \delta, в то время как клиент получил указание соблюдать \alpha и ожидать выполнения \beta. Налицо возможное расхождение во взглядах клиента и поставщика на контракт.

Александр Шалухо
Александр Шалухо
Как сбросить прогресс по курсу? Хочу начать заново
Анатолий Садков
Анатолий Садков
Вопросик
Александр Качанов
Александр Качанов
Япония, Токио
Янош Орос
Янош Орос
Украина, Киев