Выбрать главу

Использование исходной версии при переопределении

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

Например, класс BUTTON, наследник класса WINDOW, может переопределить компонент display, рисующий кнопку, так чтобы вначале рисовалось окно, а затем появлялась рамка:

class BUTTON inherit

WINDOW

redefine display end

feature -- Вывод

display is

-- Изобразить как кнопку.

do

"Изобразить как нормальное окно"; -- См. ниже

draw_border

end

... Другие компоненты ...

end

где draw_border - это процедура нового класса. Для того чтобы "Изобразить как нормальное окно", нужно вызвать исходную версию display, технически известную как precursor (предшественник) процедуры draw_border.

Это достаточно общий случай, и желательно ввести для него специальное обозначение. Конструкцию

Precursor

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

Поэтому в последнем примере часть "Изобразить как нормальное окно" можно записать просто как

Precursor

Это будет означать вызов исходной версии этой процедуры из класса WINDOW, допустимый при переопределении процедуры классом-наследником WINDOW. Precursor - это зарезервированное имя сущности такое же, как Result или Current, и оно так же пишется курсивом с заглавной первой буквой.

В данном примере переопределяемый компонент является процедурой и поэтому вызов конструкции Precursor - это команда. Этот же вызов может участвовать при переопределении функции в выражении:

some_query (n: INTEGER): INTEGER is

-- Значение, возвращаемое версией родителя, если оно

-- положительно, иначе ноль

do

Result := (Precursor (n)).max (0)

end

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

Заметим, что использование конструкции Precursor не делает компонент-предшественник компонентом данного класса, компонентом является только его переопределенная версия. (В частности, предшествующая версия может не удовлетворять новому инварианту.) Целью конструкции является облегчение переопределения в случае, когда новая версия включает старую.

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

Смысл наследования

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

Но вначале следует поразмышлять над этими фундаментальными понятиями и выяснить их значение для вопроса о качестве ПО и для процесса разработки ПО.

Двойственная перспектива

По-видимому, нигде двойственная роль классов как модулей, с одной стороны, и типов - с другой, не проявляется так отчетливо, как при изучении наследования. При взгляде на класс, как на модуль, наследник описывает расширение модуля-родителя, а при взгляде на него, как на тип, он описывает подтип типа родителя.

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

Рис. 14.11.  Механизмы наследования и их роль

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

Взгляд на класс как на модуль

С этой точки зрения наследование особенно эффективно в качестве метода повторного использования.

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

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

Это приводит к такому стилю разработки ПО, при котором вместо попытки решать каждую новую задачу с нуля поощряется ее решение, основанное на предыдущих достижениях и на расширении их результатов. Его смысл состоит в экономии - зачем повторять то, что уже однажды было сделано? - и в скромности, в духе известного замечания Ньютона, что он смог достичь таких высот только потому, что стоял на плечах гигантов.

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