Книга: Основы объектно-ориентированного программирования

Дублируемое наследование

Дядюшка Жак: С кем желаете Вы говорить, сударь, с конюхом или с поваром? Ибо я у Вас и то, и другое.

Мольер, "Скупой"

Дублируемое наследование (repeated inheritance) возникает, когда класс является потомком другого класса более чем на одном пути наследования. При этом возникает потенциальная неоднозначность, которую и следует разрешить.

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

Общие предки

Множественное наследование не запрещает, например, того, чтобы класс D был наследником классов B и C, каждый из которых является потомком класса A. Эту ситуацию и называют дублируемым наследованием.


Рис. 15.15.  Дублируемое наследование

Если B и C наследники потомков A, (случай 1), то такое наследование именуется косвенным. Если A, B и C - это один класс (случай 2), - наследование именуется прямым, что может быть записано в виде:

class D inherit
A
A
...
feature
...
end

По обе стороны океана

Следующий пример позволит нам промоделировать ситуацию дублируемого наследования и изучить возникающие проблемы. Пусть класс DRIVER имеет атрибуты:

age: INTEGER
address: STRING
violation_count: INTEGER -- Число записанных нарушений

и методы:

pass_birthday is do age := age + 1 end
pay_fee is
-- Оплата ежегодной лицензии.
do ... end

Класс наследник, US_DRIVER учитывает налоговое законодательство США, другой, FRENCH_DRIVER, - налоговое законодательство Франции.

Рассмотрим категорию людей, которым в течение года приходится водить машину в обеих странах. Нужного класса у нас еще нет, и простым решением этой проблемы кажется множественное наследование. Опишем класс FRENCH_US_DRIVER как порожденный от US_DRIVER и FRENCH_DRIVER. Налицо дублируемое наследование.


Рис. 15.16.  Типы водителей

Совместное использование и репликация

Из приведенного примера вытекает основная проблема дублируемого наследования: каков смысл компонентов дублируемого потомка (FRENCH_US_DRIVER), полученных от дублируемого предка (DRIVER)?

Рассмотрим компонент age. Он наследуется от обоих потомков DRIVER, так что, на первый взгляд, возникает конфликт имен, требующий переименования. Однако такое решение было бы неадекватно проблеме, так как реального конфликта здесь нет - атрибут age унаследованный от DRIVER, задает возраст водителя, и он один и тот же для всех потомков (если только не менять свои данные в зависимости от страны пребывания). То же относится к процедуре pass_birthday.

Внимательно перечитайте правило о конфликте имен:

Класс, наследующий от разных родителей различные компоненты с идентичным именем, некорректен.

Компоненты age (также как и pass_birthday), наследованные классом FRENCH_US_DRIVER от обоих родителей, не являются "различными", поэтому реального конфликта не возникает. Заметьте, неоднозначность могла бы возникнуть лишь в случае переопределения компонента в одном из классов. Чуть позже мы покажем, как справиться с этой проблемой, а пока предположим, что переопределений не происходит.

Если компонент дублируемого предка под одним и тем же именем наследуется от двух и более родителей, он становится одним компонентом дублируемого потомка. Этот случай будем называть совместным использованием компонента (sharing).

Всегда ли применяется совместное использование? Нет. Рассмотрим компоненты address, pay_fee, violation_count. Обращаясь в службу регистрации автотранспорта в разных странах, водители скорее всего будут указывать разные адреса и по-разному платить ежегодные сборы. Впрочем, и нарушения правил тоже будут различны. Каждый из таких компонентов, следует представить в дублируемом потомке двумя разными компонентами. Данный случай будем называть репликацией (replication).

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

Чтобы совместно использовать один из компонентов, достаточно под одним именем унаследовать исходную версию этого компонента от обоих родителей. Но как реализовать репликацию? Делая все наоборот: породив один компонент под двумя разными именами.

Эта идея не противоречит общему правилу, согласно которому каждое имя в классе служит обозначением лишь одного компонента. Поэтому репликация компонента означает переименование при наследовании.

Правило дублируемого наследования

У дублируемого потомка версии дублируемого компонента, наследуемые под одним и тем же именем, представляют один компонент. Версии, наследуемые под разными именами, представляют разные компоненты, являясь репликацией оригинала дублируемого предка.

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

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

Правило придает желаемую гибкость процессу объединения классов. Вот как может выглядеть класс FRENCH_US_DRIVER:

class FRENCH_US_DRIVER inherit
FRENCH_DRIVER
rename
address as french_address,
violation_count as french_violation_count,
pay_fee as pay_french_fee
end
US_DRIVER
rename
address as us_address,
violation_count as us_violation_count,
pay_fee as pay_us_fee
end
feature
...
end

В данном случае смена имен происходит на последнем этапе - у дублируемого потомка, но полное или частичное переименование могло быть выполнено и родителями - US_DRIVER и FRENCH_DRIVER. Важно, что будет в конце, - получит ли компонент при дублируемом наследовании одно или разные имена.

Компоненты age и pass_birthday переименованы не были, а потому, как мы и хотели, они используются совместно.

Реплицируемый атрибут, скажем, address, в каждом экземпляре класса FRENCH_US_ DRIVER будет представлен несколькими полями данных. Тогда при условии, что эти классы содержат только указанные нами компоненты, их экземпляры будут выглядеть как на рис. 15.18.


Рис. 15.17.  Совместное использование и репликация


Рис. 15.18.  Репликация атрибутов

(Организация FRENCH_DRIVER и US_DRIVER аналогична организации DRIVER, см. рисунок.)

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

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

[x]. нулевые затраты на поддержку универсальности;

[x]. низкие, ограниченные константой, затраты на динамическое связывание (не зависящие от наличия в системе дублируемого наследования классов).

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

Дублируемое наследование в С++ следует другому образцу. Уровень, на котором принимается решение, разделять или дублировать компоненты, - это класс. Поэтому при необходимости дублирования одного компонента, приходится дублировать все. В Java эта проблема исчезает, поскольку запрещено множественное наследование.

Ненавязчивое дублирующее наследование

На практике не столь часто встречаются примеры, подобные "межконтинентальным" водителям, в которых нужны и репликация компонентов, и их совместное применение. Они не для новичков. Следует приобрести опыт, чтобы браться за них.

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


Рис. 15.19.  Избыточное наследование

На рисунке показана типичная ошибка начинающих (или рассеянных разработчиков): класс D объявлен наследником B, ему нужны также свойства класса A, но B сам является потомком A. Забыв о транзитивности наследования, разработчик пишет:

class D ... inherit
A
...

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

Из этого есть только одно исключение: случай, когда B переопределяет один из компонентов A, что приведет к неоднозначности в D. Но тогда, как будет показано ниже, компилятор выдаст сообщение об ошибке, предлагая выбрать в D один из двух вариантов компонента.

Избыточное, хотя и безвредное наследование может произойти, если A - это класс, реализующий универсальные функции, например ввода-вывода, необходимые B и D. В этом случае достаточно объявить D наследником B. Это автоматически делает D потомком A, что позволяет обращаться ко всем нужным функциям. Избыточное наследование не нанесет никакого вреда, оставшись практически без последствий.

Такие случаи "безвредного" наследования могут происходить при порождении от универсальных классов ANY и GENERAL, речь о которых пойдет в следующей лекции.

Правило переименования

В этом разделе мы не введем никаких новых понятий, а лишь точнее сформулируем известные правила и приведем пример, призванный пояснить сказанное.

Начнем с запрета возникновения конфликта имен:

Определение: финальное имя

Финальным именем компонента класса является:

[x]. Для непосредственного компонента (объявленного в самом классе) - имя, под которым он объявлен.

[x]. Для наследуемого компонента без переименования - финальное имя компонента (рекурсивно) в том родительском классе, от которого оно унаследовано.

[x]. Для переименованного компонента - имя, полученное при переименовании.

Правило одного имени

Разные эффективные компоненты одного класса не могут иметь одно и то же финальное имя.

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

Ключевым в тексте правила является слово "разные". Если под одним именем мы наследуем от родителей компонентов их общего предка, действует принцип совместного использования компонентов: наследуется один компонент, и конфликта имен не возникает.

Запрет на дублирование имен касается лишь эффективных компонентов. Если один или более компонентов с омонимичными именами являются отложенными, их можно фактически слить воедино, поскольку отсутствует несовместимость реализаций. Подробнее мы поговорим об этом чуть ниже.

Приведенные правила просты и интуитивны. Чтобы в последний раз нам убедиться в их правильном понимании, построим простой пример, демонстрирующий допустимые и недопустимые варианты наследования.


Рис. 15.20.  Два варианта наследования

class A feature
this_one_OK: INTEGER
end
class B inherit A feature
portends_trouble: REAL
end
class C inherit A feature
portends_trouble: CHARACTER
end
class D inherit
-- Это неправильный вариант!
C
end

Класс D наследует this_one_OK дважды, один раз от B, другой раз - от C. Конфликта имен не возникает, поскольку данный компонент будет использоваться совместно. На самом деле, это - один компонент предка A.

Два компонента portend_trouble ("предвещающие беду") заслуженно получили такое имя. Они различны, потому их появление в D ведет к конфликту имен, делая класс некорректным. (У них разные типы, но и одинаковые типы никак не повлияли бы на ход нашего обсуждения.)

Переименовав один из компонентов, мы с легкостью сделаем D корректным:

class D inherit
-- Этот вариант класса теперь полностью корректен.
rename portends_trouble as does_not_portend_trouble_any_more end
C
end

Конфликт переопределений

Пока в ходе наследования мы меняли лишь имена. А что, если промежуточный предок, такой, как B или C (см. последний рисунок), переопределит дублируемо наследуемый компонент? При динамическом связывании это может привести к неоднозначности в D.

Проблему решают два простых механизма: отмена определения (undefinition) и выделение (selection). Как обычно, вы сами примете участие в их разработке и убедитесь в том, что при четкой постановке задачи нужная конструкция языка становится совершенно очевидной.

Пусть дублируемо наследуемый компонент переопределяется в одной из ветвей:


Рис. 15.21.  Переопределение - причина потенциальной неоднозначности

Класс B переопределяет f. Поэтому в D этот компонент представлен в двух вариантах: результат переопределения в B и исходный вариант из A, полученный через класс C. (Можно предполагать, что и C переопределяет f, но это не внесет в наше рассуждение ничего нового.) Такое положение дел отличается от предыдущих случаев, в которых мы имели лишь один вариант компонента, возможно, наследуемый под разными именами.

Что произойдет в результате? Ответ зависит от того, под одним или разными именами класс D наследует варианты компонентов. Подразумевает ли дублируемое наследование репликацию или совместное использование? Рассмотрим эти случаи по порядку.

Конфликт при совместном использовании: отмена определения и соединение компонентов

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

1 Если одна версия отложена, а другая - эффективна, то сложностей не возникает, будет использован эффективный вариант компонента. Заметим, что этот случай явно предусмотрен правилом одного имени: речь в нем идет лишь о конфликте имен двух эффективных версий.

2 Каждая версия эффективна, однако обе они переопределяются в D в предложении redefine. Проблемы снова не возникает, поскольку обе версии сливаются в одну, переопределяемую в тексте класса.

3 Обе версии эффективны, но обе не переопределяются, тогда действительно возникает конфликт имен. Класс D будет отвергнут, как нарушающий правило одного имени.

Нередко (3) означает ошибку: создана неоднозначность имен, и ее необходимо исправить. Тривиальным решением проблемы является переименование одного из вариантов, но тогда мы от рассматриваемого случая совместного использования переходим к репликации, изучаемой ниже.

Есть и другая, более изощренная возможность решения конфликта (3). Она состоит в том, чтобы позволить одному из вариантов "взять верх" над другим. Дальнейшее очевидно - свести эту ситуацию к (1), сделав один из двух вариантов отложенным.

Правила переопределения дают возможность переопределить компонент f как отложенный, хотя для этого и потребуется ввести промежуточный класс, скажем C', - наследника C, единственная роль которого - в переопределении отложенного f . Затем класс D должен быть порожден не от C, а от C'. Сложно и некрасиво. Вместо этого нам нужен простой языковой механизм: undefine. В секции наследования класса он приводит к появлению нового предложения:

class D inherit
C
undefine f end
feature
...
end

Синтаксически предложение undefine следует за rename (всякая отмена определения должна действовать на окончательный вариант имени компонента), но до redefine (прежде, чем что-то переопределять, мы должны позаботиться об отмене ненужных определений).

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


Рис. 15.22.  Два родителя и слияние компонентов

Мы хотим, чтобы D трактовал f и g как один компонент. Очевидно, это возможно лишь при условии совместимости семантики и сигнатур обоих компонентов (числа и типов аргументов и результата, если он есть). Допустим, что имена компонентов различны, и мы хотели бы сохранить имя f. Добиться желаемого можно, объединив переименование с отменой определения:

class D inherit
C
rename
g as f
undefine
f
end
feature
...
end

B получил полное превосходство над C, передавая классу D как сам компонент, так и его имя. Возможны и другие сочетания: компонент можно получить от одного из родителей, имя - от другого; можно переименовать оба компонента, присвоив им новое имя в D.

Еще один, более "симметричный" вариант соединения компонентов, заключается в замене обоих унаследованных вариантов на новый компонент. Достаточно указать оба компонента в предложении redefine, убедившись предварительно, что оба компонента имеют одно и то же финальное имя (добавив, если надо, выражение rename). В результате конфликта имен не возникнет (случай (2)), а объединение двух вариантов даст новый компонент.

Конфликты при репликации: выделение

Рассмотрим теперь случай конфликтов переопределений, связанных с репликацией. Пусть при дублируемом наследовании происходит переопределение и переименование эффективного компонента, так что имеем два эффективных компонента, наделенных собственными именами.


Рис. 15.23.  Необходимость выделения

Представленный на рисунке класс B меняет имя f на bf и переопределяет сам компонент. При этом мы опять полагаем, что C никак не меняет f, иное предположение нисколько не повлияет на ход нашего рассуждения. Более того, результат остался бы прежним, если бы B переопределял компонент f без его переименования, которое мы могли отложить до описания D. Допустим также, что речь не идет о соединении компонентов (которое происходит при переопределении обоих или отмене определения одного).

Поскольку компоненты наследуются под разными именами, то происходит их репликация. Класс D получает пару независимых компонентов, которые, в отличие от предыдущих случаев репликации, не являются копиями одного и того же компонента.

В отличие от случая совместного использования не возникает конфликта имен. Однако возникают другие конфликты, относящиеся к динамическому связыванию. Пусть полиморфная сущность a1 типа A (общий предок) на этапе выполнения связывается с экземпляром типа D (общим потомком). Что тогда означает вызов a1.f?

Правило динамического связывания гласит: вызываемый вариант f выбирается с учетом типа цели - объекта D. Но теперь это впервые нельзя истолковать однозначно: D содержит два равноценных варианта, известных под именами f и bf, соответствующих оригиналу f класса A.

Как и при конфликте имен, нельзя позволять компилятору делать выбор, пользуясь собственными правилами, - это противоречило бы принципам ясности и надежности. Управление ситуацией должно оставаться за автором разработки.

Для устранения неоднозначности необходим простой языковой механизм - предложение select. Вот версия класса, в которой предпочтение при динамическом связывании сущности f типа A отдается версии класса C:

class D inherit
C
select f end
feature
...
end

В этом варианте предпочтение отдается версии класса B:

class D inherit
select bf end
C
feature
...
end

Синтаксически предложение select следует за предложениями rename, undefine и redefine, если таковые имеются (выбор осуществляется после переименования и переопределения). Применение этого механизма регламентирует следующее правило:

Правило выделения

Класс, наследовавший две или более различные и эффективные версии компонента дублируемого предка и не переопределивший их, должен включить одну из них в предложение select.

Механизм select устраняет неоднозначность раз и навсегда. Потомкам класса нет необходимости (и они не должны) повторять выделение.

Выделение всех компонентов

Любой конфликт переопределений должен быть разрешен посредством select. Если, объединяя два класса, вы натолкнулись на ряд конфликтов, возможно, вы захотите, чтобы один из классов "одержал верх" (почти) в каждом из них. В частности, так происходит в ситуации, метафорично названной "брак по расчету" (вспомните, ARRAYED_STACK - потомок STACK и ARRAY), в которой классы-родители имеют общего предка. (В библиотеках Base оба класса действительно являются удаленными (distant) потомками общего класса CONTAINER.) В этом случае один из родителей (STACK) служит источником спецификаций, и вам, быть может, захочется, чтобы (почти) все конфликты были разрешены именно в его пользу.

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

SOME_PARENT
select all end

Результат очевиден: все конфликты переопределений, - точнее те из них, что останутся после обработки других select, - разрешатся в пользу SOME_PARENT. Последнее уточнение означает, что вы по-прежнему вправе отдать предпочтение другим родителям в отношении некоторых компонентов.

Сохранение исходной версии при переопределении

(Этот раздел посвящен весьма специфичному вопросу, и при первом чтении книги его можно пропустить.)

Приступая к изучению наследования, мы познакомились с простой конструкцией Precursor, позволявшей переопределяемому компоненту вызывать его исходную версию. Механизм дублируемого наследования дает возможность обратиться к более универсальному (хотя и более "тяжеловесному") решению, пригодному в тех редких случаях, когда базовых средств не хватает.

Вернемся к известному нам классу BUTTON - потомку WINDOW, переопределяющему display:

display is
-- Показ кнопки на экране.
do
window_display
special_button_actions
end

где window_display выводит кнопку как обычное окно, а special_button_actions добавляет элементы, специфические для кнопки, отображая, например, ее границы. Компонент window_display в точности совпадает с WINDOW-вариантом display.

Мы уже знаем, как написать window_display, используя механизм Precursor. Если метод display переопределен в нескольких родительских классах, то желаемый класс можно указать в фигурных скобках: Precursor {WINDOW}. Того же результата можно достичь, прибегнув к дублируемому наследованию, заставив класс Button быть потомком двух классов Window:

indexing
WARNING: "Это первая попытка - данная версия некорректна!"
class BUTTON inherit
WINDOW
redefine display end
WINDOW
rename display as window_display end
feature
...
end

Одна из ветвей наследования меняет имя display, а потому, по правилу дублируемого наследования BUTTON, будет иметь два варианта компонента. Один из них переопределен, но имеет прежнее имя; второй переопределен не был, но именуется теперь window_display.

Этот вариант кода почти корректен, однако в нем не хватает подвыражения select. Если, как это обычно бывает, мы хотим выбрать переопределенную версию, то запишем:

indexing
note: "Это (корректная!)схема дублируемого наследования,%
% использующая оригинальную версию переопределяемого компонента"
class BUTTON inherit
WINDOW
redefine
display
select
display
end
WINDOW
rename
display as window_display
export
{NONE} window_display
end
feature
...
end

Если такая схема должна применяться к целому ряду компонентов, их можно перечислить вместе. При этом нередко возникает необходимость разрешить все конфликты именно в пользу переопределенных компонентов. В этом случае можно воспользоваться select all.

Предложение export (см. лекцию 16) определяет статус экспорта наследуемых компонентов класса. Так, WINDOW может экспортировать компонент display, а BUTTON сделать window_display скрытым (поскольку его клиенты в нем не нуждаются). Экспорт исходной версии наследуемого компонента может сделать класс формально некорректным, если она не соответствует новому инварианту класса.

Для скрытия всех компонентов, полученных "в наследство" по одной из ветвей иерархии, служит запись export {NONE} all.

Такой вариант экспорта переопределенных компонентов и скрытия исходных компонентов под новыми именами весьма распространен, но отнюдь не универсален. Нередко классу наследнику необходимо скрывать или экспортировать оба варианта (если исходная версия не нарушает инвариант класса).

Насколько полезна такая техника дублируемого наследования для сохранения исходной версии компонента при переопределении? Обычно в ней нет необходимости, так как достаточно обратиться к Precursor. Поэтому этот способ следует использовать, когда старая версия нужна не только в целях переопределения, но и как один из компонентов нового класса.

Пример повышенной сложности

Вот более сложный пример применения разных аспектов дублируемого наследования.

Проблема, близкая по духу нашему примеру, возникла из интересного обсуждения в основной книге по C++ [Stroustrup 1991].

Рассмотрим класс WINDOW с процедурой display и двумя наследниками: WINDOW_WITH_BORDER и WINDOW_WITH_MENU. Эти классы описывают абстрактные окна, первое из них имеет рамку, а второе поддерживает меню. Переопределяя display, каждый класс выводит на экран стандартное окно, а затем добавляет к нему рамку (в первом случае) и меню (во втором).

Опишем окно с рамкой и с поддержкой меню. В результате мы породим класс WINDOW_WITH_BORDER_AND_MENU.


Рис. 15.24.  Варианты окна

Переопределим метод display в новом классе; новая версия вначале вызывает исходную, затем строит рамку, а потом строит меню. Исходный класс WINDOW имеет вид:

class WINDOW feature
display is
-- Отобразить окно (общий алгоритм)
do
...
end
... Другие компоненты ...
end

Наследник WINDOW_WITH_BORDER осуществляет вызов родительской версии display и затем отображает рамку. В дублируемом наследовании нет необходимости, достаточно воспользоваться механизмом Precursor:

class WINDOW_WITH_BORDER inherit
WINDOW
redefine display end
feature -- Output
display is
-- Рисует окно и его рамку.
do
Precursor
draw_border
end
feature {NONE} -- Implementation
draw_border is do ... end
...
end

Обратите внимание на процедуру draw_border, рисующую рамку окна. Она скрыта от клиентов класса WINDOW_WITH_BORDER (экспорт классу NONE), поскольку для них вызов draw_border не имеет смысла. Класс WINDOW_WITH_MENU аналогичен:

class WINDOW_WITH_MENU inherit
WINDOW
redefine display end
feature -- Output
display is
-- Рисует окно и его меню.
do
Precursor
draw_menu
end
feature {NONE} -- Implementation
draw_menu is do ... end
...
end

Осталось описать общего наследника WINDOW_WITH_BORDER_AND_MENU этих двух классов, дублируемого потомка WINDOW. Предпримем первую попытку:

indexing
WARNING: "Первая попытка - версия не будет работать корректно!"
class WINDOW_WITH_BORDER_AND_MENU inherit
WINDOW_WITH_BORDER
redefine display end
WINDOW_WITH_MENU
redefine display end
feature
display is
-- Рисует окно,его рамку и меню.
do
Precursor {WINDOW_WITH_BORDER}
Precursor {WINDOW_WITH_MENU}
end
...
end

Заметьте: при каждом обращении к Precursor мы вынуждены называть имя предка. Каждый предок имеет собственный компонент display, переопределенный под тем же именем.

Впрочем, как замечает Страуструп, это решение некорректно: версии родителей дважды вызывают исходную версию display класса WINDOW, что приведет к появлению "мусора" на экране. Для исправления ситуации добавим еще один класс, получив тройку наследников класса WINDOW:

indexing
note: "Это корректная версия"
class WINDOW_WITH_BORDER_AND_MENU inherit
WINDOW_WITH_BORDER
redefine
display
export {NONE}
draw_border
end
WINDOW_WITH_MENU
redefine
display
export {NONE}
draw_menu
end
WINDOW
redefine display end
feature
display is
-- Рисует окно,его рамку и меню.
do
Precursor {WINDOW}
draw_border
draw_menu
end
...
end

Заметьте, что компоненты draw_border и draw_menu в новом классе являются скрытыми, поскольку мы не видим причин, по которым клиенты WINDOW_WITH_BORDER_AND_MENU могли бы их вызывать непосредственно.

Несмотря на активное применение дублируемого наследования, класс переопределяет все унаследованные им варианты display, что делает выражения select ненужными. В этом состоит преимущество спецификатора Precursor в сравнении с репликацией компонентов.

Неплохим тестом на понимание дублируемого наследования станет решение этой задачи без применения Precursor, путем репликации компонентов промежуточных классов. При этом, разумеется, вам понадобится select (см. упражнение 15.10).

В полученном варианте класса присутствует лишь совместное использование, но не репликация компонентов. Расширим пример Страуструпа: пусть WINDOW имеет запрос id (возможно, целого типа), направленный на идентификацию окон. Если идентифицировать любое окно только одним "номером", то id будет использоваться совместно, и нам не придется ничего менять. Если же мы хотим проследить историю окна, то экземпляр WINDOW_WITH_BORDER_AND_MENU будет иметь три id - независимых "номера". Новый текст класса комбинирует совместное использование и репликацию id (изменения в тексте класса помечены стрелками):

indexing
note: "Усложненная версия с независимыми id."
class WINDOW_WITH_BORDER_AND_MENU inherit
WINDOW_WITH_BORDER
rename
id as border_id
redefine
display
export {NONE}
draw_border
end
WINDOW_WITH_MENU
rename
id as menu_id
redefine
display
export {NONE}
draw_menu
end
WINDOW
rename
id as window_id
redefine
display
select
window_id
end
feature
.... Остальное, как ранее...
end

Обратите внимание на необходимость выбора (select) одного из вариантов id.

Дублируемое наследование и универсальность

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

class A [G] feature
f: G;...
end
class B inherit
A [INTEGER]
A [REAL]
end

В классе B по правилу дублируемого наследования компонент f должен использоваться совместно. Но из-за универсализации возникает неоднозначность, - какой результат должен возвращать компонент - real или integer? Та же проблема возникнет, если f имеет параметр типа G.

Подобная неоднозначность недопустима. Отсюда правило:

Универсальность в правиле дублируемого наследования

Тип компонента, совместно используемого в правиле дублируемого наследования, а также тип любого из его аргументов не может быть родовым параметром класса, от которого произошло дублируемое наследование компонента.

Для устранения неоднозначности можно выполнить переименование в точке наследования.

Правила об именах

(В этом разделе мы только формализуем сказанное выше, поэтому при первом чтении книги его можно пропустить.)

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

Конфликты имен: определение и правило

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

Конфликт имен делает класс некорректным за исключением следующих случаев:

1 Оба компонента унаследованы от общего предка, и ни один из них не получен повторным объявлением версии предка.

2 Оба компонента имеют совместимые сигнатуры, и, по крайней мере, один из них наследуется в отложенной форме.

3 Оба компонента имеют совместимые сигнатуры и переопределяются в новом классе.

Ситуация (1) описывает совместное использование при дублируемом наследовании.

Для случая (2) "наследование в отложенной форме" возможно по двум причинам: либо отложенная форма задана родительским классом, либо компонент был эффективным, но порожденный класс отменил его реализацию (undefine).

Ситуации (2) и (3) рассматриваются отдельно, однако, их можно представить как один вариант - вариант соединения (join). Переходя к n компонентам (n >= 2), можно сказать, что ситуации (2) и (3) возникают, когда от разных родителей класс принимает n одноименных компонентов с совместимыми сигнатурами. Конфликт имен не делает класс некорректным, если эти компоненты могут быть соединены, иными словами:

[x]. все n компонентов отложены, так что некому вызвать конфликт определений;

[x]. существует единственный эффективный компонент. Его реализация станет реализацией остальных компонентов;

[x]. два или несколько компонентов эффективны. Класс должен их переопределить. Новая реализация будет использоваться как для переопределяемых компонентов, так и для любых отложенных компонентов, участвующих в конфликте.

И, наконец, точное правило употребления конструкции Precursor. Если в переопределении используется Precursor, то неоднозначность может возникнуть из-за того, что неясно, версию какого родителя следует вызывать. Чтобы решить эту проблему, следует использовать вызов вида Precursor {PARENT} (...), где PARENT - имя желаемого родителя. В остальных случаях указывать имя родителя не обязательно.

Оглавление книги


Генерация: 0.237. Запросов К БД/Cache: 2 / 0
поделиться
Вверх Вниз