Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
OOP / books / Osnovi objektno-orientirovannogo programmirovaniya.pdf
Скачиваний:
62
Добавлен:
03.03.2016
Размер:
9.04 Mб
Скачать

B

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 B

C

select f end feature

...

end

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

class D inherit B

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

Соседние файлы в папке books