Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4086 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Дополнительный материал 2:

Универсальность и (versus) наследование

< Дополнительный материал 1 || Дополнительный материал 2: 123456

Эмуляция универсальности с помощью наследования

Обратимся теперь к решению обратной задачи: можно ли добиться эффекта универсальности в стиле 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 "+" ).

Использование закрепленных типов ( like Current ) позволяет избежать недопустимых комбинаций, как поясняется в следующем примере COMPARABLE. На этом этапе замена всех таких типов для RING_ELEMENT не оказывала бы эффекта.

Ограниченная универсальность: подпрограммы

Мы можем написать подпрограмму, такую как 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 в каждом из них.

Эмуляция неограниченной универсальности

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

< Дополнительный материал 1 || Дополнительный материал 2: 123456