Универсальность и (versus) наследование
Эмуляция универсальности с помощью наследования
Обратимся теперь к решению обратной задачи: можно ли добиться эффекта универсальности в стиле Ada средствами наследования ОО-языка.
Введенная в предшествующих лекциях OO-нотация поддерживает универсальность. Но поскольку здесь сравнивается чистая универсальность с чистым наследованием, необходимо притвориться на некоторое время, что мы забыли о существующем механизме универсальности. В результате решения, представленные в этом разделе, будут существенно более сложными, нежели при использовании полной нотации, описанной в остальной части книги. Необходимо помнить, что приведенные фрагменты программ не являются завершенными и используются только для обсуждения.
Возможно, это покажется удивительным, но проще эмулировать более сложную форму ограниченной универсальности, и именно этот вариант рассмотрен первым.
Эмуляция ограниченной универсальности: обзор
Довольно естественной является идея связывания ограниченного формального родового параметра с некоторым классом, в котором определены ограничивающие операции. Этот класс можно рассматривать как АТД. Расмотрим наши два примера Ada с ограниченными родовыми параметрами - minimum and matrices:
generic type G is private; with function "<=" (a, b: G) return BOOLEAN is <> generic type G is private; zero: G; unity: G; with function "+"(a, b: G) return G is <>; with function "*"(a, b: G) return G is <>;
Можно рассматривать эти предложения как определения двух абстрактных типов данных - COMPARABLE и RING_ELEMENT. Первый характеризуется наличием операции сравнения " <= ", а второй компонентами zero, unity, + and *.
На ОО языке такие типы могут быть непосредственно представлены как классы. Определить эти классы полностью невозможно, так как нет универсального решения для операций " <= ", " + " и т.д. Следовательно, необходимо использовать абстрактные классы, возложив детали реализации на их потомков:
deferred class COMPARABLE feature infix "<=" (other: COMPARABLE): BOOLEAN is deferred end end deferred class RING_ELEMENT feature infix "+" (other: like Current): like Current is deferred ensure equal(other, zero) implies equal(Result, Current) end; infix "*" (other: like Current): like Current is deferred end zero: like Current is deferred end unity: like Current is deferred end end
В отличие от Ada, ОО-нотация позволяет описывать абстрактные семантические свойства, хотя в данный пример включено только одно (постусловие x + 0 = x при любом x для операции infix "+" ).
Ограниченная универсальность: подпрограммы
Мы можем написать подпрограмму, такую как minimum, указав тип COMPARABLE для ее аргументов. Основываясь на образце Ada, функция была бы объявлена следующим образом:
minimum (one: COMPARABLE; other: like one): like one is -- Минимальное из one и other do ... end
При ОО-разработке каждая подпрограмма появляется в классе и связывается с текущим экземпляром класса. Включив minimum в класс COMPARABLE, аргумент one станет неявным текущим экземпляром. Класс будет выглядеть так:
deferred class COMPARABLE feature infix "<=" (other: like Current): BOOLEAN is -- Текущий объект меньше или равен other? deferred end minimum (other: like Current): like Current is -- Минимальное из двух значений: текущего -- и other do if Current <= other then Result := Current else Result := other end end end
Для вычисления минимума двух элементов необходимо объявить их тип как эффективного потомка COMPARABLE, с заданной реализацией операции сравнения <=, например:
class INTEGER_COMPARABLE inherit COMPARABLE creation put feature -- Initialization put (v: INTEGER) is -- Инициализация значением v. do item := new end feature -- Access item: INTEGER; -- Значение, связанное с текущим объектом feature -- Basic operations infix "<=" (other: like Current): BOOLEAN is -- Текущий объект меньше или равен other? do Result := (item <= other.item) end; end
Для нахождения минимума двух целых теперь можно применять функцию minimum к сущностям ic1 и ic2, чьи типы не INTEGER, а INTEGER_COMPARABLE:
ic3 := ic1.minimum (ic2)
Для использования родовых функций infix <= и minimum придется исключить прямые ссылки на целые, заменив их сущностями INTEGER_COMPARABLE, поскольку этого требует атрибут item и подпрограмма put. Более того, придется вводить подобных потомков COMPARABLE, таких как STRING_COMPARABLE и REAL_COMPARABLE, для каждого типа, требующего своей версии minimum.
Заметьте, механизм закрепленных объявлений является основой обеспечения корректности. Если бы аргумент minimum в COMPARABLE был бы объявлен как COMPARABLE, а не like Current, то следующий вызов был бы синтаксически допустим:
ic1.minimum (c)
если c принадлежал бы типу COMPARABLE, но не был бы типом INTEGER_COMPARABLE. Понятно, что такой вызов мог быть некорректным. Все это применимо и к RING_ELEMENT.
Объявление компонентов item и put для всех потомков COMPARABLE, жертвуя при этом прямым использованием простых типов, конечно же, неприятно. При этом приходится идти на потерю производительности: вместо манипулирования целыми или строками приходится использовать объекты обертывающих типов, таких как INTEGER_COMPARABLE. Но, заплатив эту цену - простоту использования и эффективность, мы приобретаем полную эмуляцию ограниченной универсальности средствами наследования. (В заключительной нотации, конечно, ничего платить не требуется.)
Эмуляция ограниченной универсальности (1) Можно эмулировать ограниченную универсальность средствами наследования, используя обертывающие классы и соответственно обертывающие объекты. |
Ограниченная универсальность: пакеты
Предыдущая дискуссия переносится и на пакеты. Для эмуляции абстракции матриц, которую Ada реализует пакетом MATRICES, можно использовать класс:
class MATRIX feature anchor: RING_ELEMENT is do end implementation: ARRAY2 [like anchor] item (i, j: INTEGER): like anchor is -- Значение элемента с индексами (i, j) do Result := implementation.item (i, j) end put (i, j: INTEGER; v: like anchor) is -- Присвоить значение v элементу с индексами (i, j) do implementation.put (i, j, v) end infix "+" (other: like Current): like Current is -- Матричная сумма текущей матрицы matrix и other local i, j: INTEGER do create Result.make (...) from i := ... until ... loop from j := ... until ... loop Result.put ((item (i, j) + other.item (i, j)), i, j) j := j + 1 end i := i + 1 end end infix "*"(other: like Current): like Current is -- Матричное произведение текущей матрицы и other local ... do ... end end
С типом аргумента put и результата item связана интересная проблема: он должен быть RING_ELEMENT, но соответствующим образом переопределен в классах-потомках. Закрепленное объявление дает решение проблемы, но здесь, на первый взгляд, нет атрибута, который мог бы послужить якорем. Это не должно нас останавливать: следует объявить искусственный якорь, называемый anchor. Его единственное предназначение - быть переопределенным в подходящий тип потомка RING_ELEMENT будущими потомками MATRIX (например, BOOLEAN_RING в BOOLEAN_MATRIX и т. д.). Во избежание потерь памяти в экземплярах anchor объявляется как функция, а не как атрибут. Техника искусственного якоря полезна для сохранения согласованности типов, когда, как в данном случае, нет естественного якоря среди атрибутов класса.
Некоторые детали цикла, также как и тело инфиксной операции *, остались вне рассмотрения, но дополнить их просто. Компоненты put и item, применяемые в реализации, пришли из библиотечного класса ARRAY2, описывающего двумерные массивы.
Для определения эквивалента родового пакета Ada, показанного ранее:
package BOOLEAN_MATRICES is new MATRICES (BOOLEAN, false, true, "or", "and");
следует прежде всего объявить соответствующее булево кольцо:
class BOOLEAN_RING_ELEMENT inherit RING_ELEMENT redefine zero, unity end creation put feature -- Initialization put (v: BOOLEAN) is -- Инициализация значением v do item := v end feature -- Access item: BOOLEAN feature -- Basic operations infix "+" (other: like Current): like Current is -- Булево сложение: or do create Result.put (item or other.item) end infix "*"(other: like Current): like Current is -- Булево умножение: and do create Result.put (item and other.item) end zero: like Current is -- Нулевой элемент булева кольца для сложения once create Result.put (False) end unity: like Current is -- Нулевой элемент для булева умножения once create Result.put (True) end end
Заметьте, ноль и единица реализуются однократными функциями.
Тогда для получения родового порождения пакета Ada следует просто определить наследника BOOLEAN_MATRIX от MATRIX, где нужно только переопределить anchor - искусственный якорь; все остальные типы будут следовать автоматически:
class BOOLEAN_MATRIX inherit MATRIX redefine anchor end feature anchor: BOOLEAN_RING_ELEMENT end
Эта конструкция достигает эффекта ограниченной универсальности благодаря использованию наследования, подтверждая для пакетов результаты эмуляции, проиллюстрированные ранее для подпрограмм.
Неограниченная универсальность
Механизм эмуляции неограниченной универсальности остается тем же самым. Этот случай можно рассматривать как специальную форму ограниченной универсальности с пустым множеством ограничений. Как и выше, типы формальных параметров будут рассматриваться как АТД, но без относящихся к ним операций. Эта техника работает, но вряд ли ее стоит применять, поскольку такой пустой тип не соответствует никакой реальности данных.
Попробуем применить эту технику к двум нашим примерам с неограниченной универсальностью - процедуре обмена и очереди, начав с последнего. Нам потребуется класс, назовем его QUEUABLE, описывающий добавляемые и получаемые объекты очереди. Так как это можно делать с любыми объектами, то у класса нет свойств помимо его имени:
class QUEUABLE end
Мы можем теперь объявить класс QUEUE, чьи операции применимы к объектам QUEUABLE. (Помните, что этот класс не предлагается как образец совершенства хорошего ОО-проектирования, мы по-прежнему добровольно играем с не лучшей версией ОО-нотации, не имеющей универсальности.) Постусловия программы опущены для краткости. И хотя в принципе функция item могла бы служить якорем, ее тело не будет изменяться потомками, так что лучше использовать искусственный якорь во избежание ее переопределения.
indexing description: "Очередь, реализованная массивом" class QUEUE creation make feature -- Initialization make (m: INTEGER) is -- Создание очереди, вмещающей m элементов require m >= 0 do create implementation.make (1, m); capacity := m first := 1; next := 1 end feature -- Access capacity, first, next, count: INTEGER item: like item_anchor is -- Старейший (первый пришедший) элемент очереди require not empty do Result := implementation.item (?rst) end feature -- Status report empty: BOOLEAN is -- Пуста ли очередь? do Result := (count = 0) end full: BOOLEAN is -- Заполнен ли массив? do Result := (count = capacity) end feature -- Element change put (x: like item_anchor) is -- Добавление x в конец очереди require not full do implementation.put (x, next); count := count + 1 next := successor (next) end remove is -- Удаление старейшего элемента require not empty do first := successor (first); count := count - 1 end feature {NONE} -- Implementation item_anchor: QUEUABLE is do end implementation: ARRAY [like item_anchor] successor (n: INTEGER): INTEGER is -- Значение, следующее за n, циклически в интервале 1 .. capacity require n >= 1; n <= capacity do Result := (n \\ capacity) + 1 end invariant 0 <= count; count <= capacity; first >= 1; next >= 1 (not full) implies ((first <= capacity) and (next <= capacity)) (capacity = 0) implies full -- Элементы, если они есть, появляются в позициях массива first, ... next - 1 end
Реализации ограниченной очереди, приведенные ранее в этой книге, основывались на технике, сохраняющей один пустой элемент. Здесь же заполняется все доступное пространство, но хранится число элементов очереди. Никаких особых причин в выборе варианта нет, это лишь демонстрация различных приемов реализации. |
Для получения эквивалента родового порождения (для получения очереди с элементами нужного типа) необходимо, как и в примере с COMPARABLE, определить потомков QUEUABLE:
class INTEGER_QUEUABLE inherit QUEUABLE creation put feature -- Initialization put (n: INTEGER) is -- Инициализация значением n do item := n end feature -- Access item: INTEGER feature {NONE} -- Implementation item_anchor: INTEGER is do end end
Подобным образом следует поступить при порождении STRING_QUEUABLE и т. д. Затем следует объявить соответствующих потомков QUEUE, переопределив item_anchor в каждом из них.