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

end

(Попробуйте написать put_left. Здесь скрыта ловушка! См. приложение A.)

Правило повторного объявления типов

Примеры, рассмотренные выше, несмотря на все их различия, объединяет необходимость повторного объявления типов. Спуск по иерархии наследования означает специализацию классов, и в соответствии со специализацией изменяются типы функций и типы аргументов подпрограмм, как, например, a в set_alternate и other в put_right; изменяются типы запросов - alternate и right.

Этот аспект повторного объявления выражает следующее правило:

Правило повторного объявления типов

При повторном объявлении компонента можно заменить тип компонента (для атрибутов и функций) или тип формального параметра (для подпрограмм) любым совместимым типом.

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

Любое повторное объявление ведет к специализации, а, следовательно, к изменению типов. Так, с переходом к двунаправленным спискам параметры и результаты функций сменили свой тип на BI_LINKABLE. Отсюда становится понятен тот термин, которым часто описывают политику редекларации типов, - ковариантная типизация (covariant typing), где приставка "ко" указывает на параллельное изменение типов при спуске по диаграмме наследования.

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

Закрепленные объявления

Правило повторного объявления типов способно свести на нет целый ряд преимуществ наследования. Почему это происходит и каково решение данной проблемы?

Несогласованность типов

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

Рис. 16.10.  Добавление элемента

put_right (v: G) is

-- Вставить элемент v справа от курсора.

-- Не передвигать курсор.

require

not after

local

new: LINKABLE [T]

do

create new.make (v)

put_linkable_right (new)

...

ensure

... См. приложение A ...

end

Для вставки нового элемента, имеющего значение v, необходимо предварительно создать элемент типа LINKABLE [G]. Вставка производится закрытой процедурой put_linkable_right, принимающей LINKABLE как параметр (и связывающей его с текущим элементом, используя процедуру put_right класса LINKABLE). Эта процедура осуществляет все нужные манипуляции со ссылками.

У потомков LINKED_LIST, таких как TWO_WAY_LIST или LINKED_TREE, процедура put_right тоже должна быть применимой. Но у них она работать не будет! Хотя алгоритм ее остается корректным, сущность new для них должна иметь другой тип - BI_LINKABLE или LINKED_TREE. Поэтому в каждом потомке нужно переопределять и переписывать целую процедуру, и это притом, что ее тело будет идентично оригиналу, за исключением переопределения new! Для подхода, претендующего на решение проблемы повторного использования, это серьезный порок.

Примеры из практики

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

some_attribute: SOME_TYPE

set_attribute (a: SOME_TYPE) is do ... end

переопределение some_attribute подразумевает соответствующее переопределение set_attribute. В случае с put_right из BI_LINKABLE (не путайте с подпрограммой из LINKED_LIST) повторное определение необходимо, поскольку фактически меняется алгоритм. Но во многих широко распространенных случаях (к примеру, в set_alternate) новый алгоритм идентичен исходному.

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

Рис. 16.11.  Исходная и сопряженная точка

conjugate: POINT is

-- Точка, сопряженная с текущей

do

Result := clone (Current) -- Получить копию текущей точки

Result.move (0, -2*y) -- Перенести результат по вертикали

end

Рассмотрим теперь некий класс, порожденный от POINT, например PARTICLE. К атрибутам частиц, помимо координат, относятся, вероятно, масса и скорость. По идее, функция conjugate применима и к PARTICLE и выдает в результате ту же частицу с противоположным значением координаты y. Но если оставить все как есть, функция работать не будет из-за несоблюдения правила совместимости типов:

p1, p2: PARTICLE; create p1.make (...); ...

p2 := p1.conjugate

Правая часть подчеркнутого оператора имеет тип POINT, левая часть - тип PARTICLE. Правило совместимости типов этого не допускает. Поэтому мы должны переписать conjugate для PARTICLE с единственной целью - обеспечить соблюдение правила.

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

Серьезное затруднение

Изучив класс LINKED_LIST в тексте приложения A, вы поймете, что проблема еще масштабнее. В теле класса содержится множество объявлений со ссылкой на тип LINKABLE [G], а с переходом к двунаправленным спискам почти все они потребуют повторного определения. Так, вариант представления списка включает четыре ссылки на отдельные элементы: