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

Принципы проектирования класса

Активные структуры данных

Примеры этой и предыдущих лекций часто используют понятие списка или последовательности, характеризуемой в каждый момент "позицией курсора", указывающей точку доступа, вставки и удаления. Такой вид структуры данных широко применим и заслуживает детального рассмотрения.

Для понимания достоинств такого подхода полезно начать с общего рассмотрения и оценки его ограничений.

Представление связного списка

Обсуждение будет основываться на примере списков. Хотя результаты не зависят от выбора реализации, необходимо иметь некоторое представление, позволяющее описывать алгоритмы и иллюстрировать проблемы. Будем использовать популярный выбор - односвязный линейный список. Наша общецелевая библиотека должна иметь классы со списковыми структурами и среди них класс LINKED_LIST.

Вот некоторые сведения о связных списках, применимые ко всем стилям интерфейса, обсуждаемым далее, - с курсором и без курсора.

Связный список является полезным представлением последовательной структуры с эффективно реализуемыми операциями вставки и удаления элементов. Элементы хранятся в отдельных ячейках, называемых звеньями ( linkables ). Каждое звено содержит значение и ссылку на следующий элемент списка:

Элемент списка - звено (linkable)

Рис. 5.3. Элемент списка - звено (linkable)

Соответствующий класс должен быть универсальным (синонимы: родовым, параметризованным), так как структура должна быть применима к спискам с элементами любого типа. Значение звена, заданное компонентом item, имеет тип G - родовой параметр. Оно может быть непосредственно встроено, если фактический родовой параметр развернутого типа, например, для списка целых, или быть ссылкой в общем случае. Другой атрибут right типа LINKABLE[G] всегда представляет ссылку.

Сам список задается отдельной ячейкой - заголовком, содержащим ссылку first_element на первое звено, и, возможно, дополнительной информацией, например count - текущим числом элементов списка. Вот как выглядит связный список символов:

Связный список (linked list)

Рис. 5.4. Связный список (linked list)

Это представление позволяет быстро выполнять операции вставки и удаления, если есть ссылка, указывающая на звено слева от цели операции. Достаточно выполнить несколько манипуляций над ссылками, как показано на следующем рисунке:

Удаление в связном списке

Рис. 5.5. Удаление в связном списке

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

Технический момент: рисунок не фиксирует в деталях атрибуты LINKED_LIST кроме first_element, показывая просто затененную область. Хотя можно обойтись first_element, классы ниже включают атрибут count. Этот запрос может быть функцией, но неэффективно при каждом проходе по списку подсчитывать число элементов. Конечно, при использовании атрибута каждая операция вставки и удаления должна обновлять его значение. Здесь применим Принцип Унифицированного Доступа - можно менять реализацию, не нанося вреда клиентам класса.

Пассивные классы

Ясно, что нам нужны два класса: LINKED_LIST для списка (более точно, заголовка списка), LINKABLE для элементов списка - звеньев. Оба они являются универсальными.

Понятие LINKABLE является основой реализации, но не столь важно для большинства клиентов. Следует позаботиться об интерфейсе, обеспечивающем модули клиентов нужными примитивами, но не следует беспокоить их такими деталями реализации как представление элементов в звене списка. Атрибуты, соответствующие рисунку, появятся как:

indexing
    description: "Звенья, используемые в связном списке"
    note: "Частичная версия, только атрибуты"
class
    LINKABLE1 [G]
feature {LINKED_LIST}
    item: G
            -- Значение звена
    right: LINKABLE [G]
            -- Правый сосед
end

Тип right можно было бы задавать как like Current, но предпочтительнее на этом этапе сохранить больше свободы в переопределении, поскольку пока непонятно, что может потребовать изменений у потомков LINKABLE.

Для получения настоящего класса следует добавить подпрограммы. Что допустимо для клиентов при работе со звеньями? Они могут изменять поля item и right. Можно также ожидать, что многие из клиентов захотят при создании звена инициализировать его значение, что требует процедуры создания. Вот подходящая версия класса:

indexing
    description: "Звенья, используемые в связном списке"
class LINKABLE [G] creation
    make
feature {LINKED_LIST}
    item: G
            -- Значение звена
    right: LINKABLE [G]
            -- Правый сосед
    make (initial: G) is
            -- Инициализация item значением initial
        do put (initial) end
    put (new: G) is
            -- Замена значения на new
        do item := new end
    put_right (other: LINKABLE [G]) is
            -- Поместить other справа от текущего звена
        do right := other end
end

Для краткости в классе опущены очевидные постусловия процедуры (такие как ensure item = initial для make ). Предусловий здесь нет.

Ну, вот и все о LINKABLE. Теперь рассмотрим сам связный список, внутренне доступный через заголовок. Рассмотрим его экспортируемые компоненты: запрос на получение числа элементов ( count ), пуст ли список ( empty ), значение элемента по индексу i(item), вставка нового элемента в определенную позицию ( put ), изменение значения i -го элемента ( replace ), поиск элемента с заданным значением ( occurrence ). Нам также понадобится запрос, возвращающий ссылку на первый элемент ( void, если список пуст), который не должен экспортироваться.

Вот набросок первой версии. Некоторые тела подпрограмм опущены.

indexing
    description: "Односвязный список"
    note: "Первая версия, пассивная"
class
    LINKED_LIST1 [G]
feature -- Access
    count: G
    empty: BOOLEAN is
            -- Пуст ли список?
        do
            Result := (count = 0)
        ensure
            empty_if_no_element: Result = (count = 0)
        end
    item (i: INTEGER): G is
            -- Значение i-го элемента
        require
            1 <= i; i <= count
        local
            elem: LINKABLE [G]; j: INTEGER
        do
            from
                j := 1; elem := first_element
            invariant j <= i; elem /= Void variant i - j until
                j = i
            loop
                j := j + 1; elem := elem.right
            end
            Result := elem.item
        end
    occurrence (v: G): INTEGER is
            -- Позиция первого элемента со значением v (0, если нет)
        do ... end
feature -- Element change
    put (v: G; i: INTEGER) is
            -- Вставка нового элемента со значением v,
            -- так что он становится i-м элементом
        require
            1 <= i; i <= count + 1
        local
            previous, new: LINKABLE [G]; j: INTEGER
        do
                -- Создание нового элемента
            create new.make (v)
            if i = 1 then
                    -- Вставка в голову списка
                new.put (first_element); first_element := new
            else
                from
                    j := 1; previous := first_element
                invariant
                    j >= 1; j <= i - 1; previous /= Void
                    -- previous - это j-й элемент списка
                variant
                    i - j - 1
                until j = i - 1 loop
                    j := j + 1; previous := previous.right
                end
                    Вставить после previous
                previous.put_right (new)
                new.put_right (previous.right)
            end
            count := count + 1
        ensure
            one_more: count = old count + 1
            not_empty: not empty
            inserted: item (i) = v
            -- For 1 <= j < i,
            -- элемент с индексом j не изменил свое значение
            -- For i < j <= count,
                -- элемент с индексом j изменил свое значение
                -- на то, которое элемент с индексом j - 1
                -- имел перед вызовом
        end
    replace (i: INTEGER; v: G) is
            -- Заменить на v значение i-го элемента
        require
            1 <= i; i <= count
        do
            ...
        ensure
            replaced: item (i) = v
        end
feature -- Removal
    prune (i: INTEGER) is
            -- Удалить i-й элемент
        require
            1 <= i; i <= count
        do
            ...
        ensure
            one_less: count = old count - 1
        end
    ... Другие компоненты ...
feature {LINKED_LIST} -- Implementation
    first_element: LINKABLE [G]
invariant
    empty_definition: empty = (count = 0)
    empty_iff_no_first_element: empty = (first_element = Void)
end

Это хорошая идея попытаться самому закончить определение occurrence, replace и prune в этой первой версии. Убедитесь при этом, что поддерживается истинность инварианта.