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

Наследование: "откат" в интерактивных системах

< Лекция 2 || Лекция 3: 123456 || Лекция 4 >

Поиск абстракций

Ключом ОО-решения является поиск правильных абстракций. Здесь фундаментальное понятие буквально напрашивается.

Класс Command

Для нашей проблемы характерна фундаментальная абстракция данных COMMAND, представляющая любую операцию, отличающуюся от Undo и Redo. Выполнение операции это лишь один из многих компонентов, применимых к команде, - команду можно сохранить, тестировать или отменить. Так что нам понадобится класс и вот его первоначальная форма:

deferred class COMMAND feature
    execute is deferred end
    undo is deferred end
end

Класс COMMAND описывает абстрактное понятие команды и потому должен оставаться отложенным. Фактические типы команды будут представлены эффективными потомками этого класса, такими как:

class LINE_DELETION inherit
    COMMAND
feature
    deleted_line_index: INTEGER
    deleted_line: STRING
    set_deleted_line_index (n: INTEGER) is
            -- Устанавливает n номер следующей удаляемой строки
        do
            deleted_line_index := n
       end
    execute is
        -- Удаляет строку
        do
            "Удалить строку с номером deleted_line_index"
            "Записать текст удаляемой строки в deleted_line"
        end
    undo is
            -- Восстанавливает последнюю удаляемую строку
        do
            "Поместить deleted_line в позицию deleted_line_index"
        end
end

Аналогичный класс строится для каждой команды класса.

Что же представляют собой такие классы? Экземпляр LINE_DELETION, как будет показано ниже, является небольшим объектом, несущим всю необходимую информацию, связанную с выполнением команды: строку, подлежащую удалению, ( deleted_line ) и ее индекс в тексте ( deleted_line_index ). Эта информация необходима для выполнения команды undo, если она потребуется, или для повтора redo.

Объект command

Рис. 3.1. Объект command

Атрибуты, такие как deleted_line и deleted_line_index, у каждой команды будут свои, но всегда они должны быть достаточными для поддержки локальных операций execute и undo. Объекты, концептуально описывающие разницу между двумя состояниями приложения: предшествующим и последующим за выполнением команды, дают возможность удовлетворить требование U3 из нашего списка - хранить только то, что строго необходимо.

Структура наследования классов выглядит следующим образом:

Иерархия классов COMMAND

Рис. 3.2. Иерархия классов COMMAND

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

При определении понятия важно указать, какие характеристики оно не покрывает. Здесь концепция команды не включает Undo и Redo; например, не имеет смысла выполнять откат самого Undo (если только не иметь в виду выполнение Redo). По этой причине в обсуждении используется термин операция ( operation ) для Undo и Redo и слово команда ( command ) для операций, допускающих откат и повтор, подобных вставке строки. Нет необходимости в классе, покрывающем понятие операции, так как такие операции, как Undo, имеют только одно связанное с ними свойство - быть выполненными.

Это хороший пример ограничений упрощенного подхода к "поиску объектов", подобному известному методу "Подчеркивание существительных", идея, изучаемая в последней лекции. В спецификациях проблемы существительные command и operation одинаково важны; но одно приводит к фундаментальному классу, второе - вообще не дает класса. Только изучение абстракций в терминах применимых операций и свойств может помочь в поиске классов проектируемой ОО системы.

Основной интерактивный шаг

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

В любой интерактивной системе в модуле, ответственном за коммуникацию с пользователем, должен быть некоторый фрагмент следующего вида:

basic_interactive_step is
        -- Декодирование и выполнение одного запроса пользователя
    do
        "Определить, что пользователь хочет выполнить"
        "Выполнить это (если возможно)"
    end

В традиционных структурированных системах, подобных редактору, эти операции будут частью цикла - базисного цикла программы:

from start until quit_has_been_requested_and_confirmed loop
    basic_interactive_step
end

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

С учетом наших абстракций тело процедуры можно уточнить следующим образом:

"Получить последний запрос пользователя"
"Декодировать запрос"
if "Запрос является нормальной командой (не Undo)" then
    "Определить соответствующую команду в системе"
    "Выполнить команду"
elseif "Запрос это Undo" then
    if "Есть обратимая команда" then
        "Undo последней команды"
    elseif "Есть команда для повтора" then
        "Redo последней команды"
    end
else
    "Отчет об ошибочном запросе"
end

Здесь реализуется соглашение, что Undo примененное сразу после Undo, означает Redo. Запрос Undo или Redo игнорируется, если нет возможности отката или повтора. В простом текстовом редакторе с клавиатурным интерфейсом, процедура "Декодировать запрос" будет анализировать ввод пользователя, отыскивая такие коды, как control-I (для вставки строки, control-D для удаления) и другие. В графическом интерфейсе будет проверяться выбор команды меню, нажатие кнопки или соответствующих клавиш.

Сохранение последней команды

Располагая понятием объекта command, можно добавить специфику в выполняемые операции, введя атрибуты:

requested: COMMAND
--Команда, запрашиваемая пользователем

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

"Получить и декодировать последний запрос пользователя"
if "Запрос является нормальной командой (не Undo)" then
    "Создать подходящий объект command и присоединить его к requested"
        -- requested создан как экземпляр некоторого потомка
        -- класса COMMAND, такого как LINE_DELETION.
        -- (Эта инструкция детализируется ниже.)
    requested.execute; undoing_mode := False
elseif "Запрос является Undo" and requested /= Void then
    if undoing_mode then
        "Это Redo; детали оставляем читателям"
    else
        requested.undo; undoing_mode := True
    end
else
    "Ошибочный запрос: вывод предупреждения или игнорирование"
end

Булева сущность undoing_mode определяет, была ли Undo последней операцией. В этом случае непосредственно следующий запрос Undo будет означать Redo, хотя непосредственные детали остаются за читателем, (упражнение У3.2); мы увидим полную реализацию Redo в более интересном случае многоуровневого механизма.

Информация, сохраняемая перед каждым выполнением команды, задается в экземпляре некоторого потомка COMMAND, такого как LINE_DELETION. Это означает, что, как и анонсировалось, решение удовлетворяет свойству U3 в списке требований: хранится не все состояние, а только разница между новым состоянием и предыдущим.

Ключом решения - и его уточнением в оставшейся части лекции - является полиморфизм и динамическое связывание. Атрибут requested полиморфен: объявленный как COMMAND он присоединяется к объектам одного из эффективных потомков, таким как LINE_INSERTION. Вызовы requested.execute и requested.undo осмыслены из-за динамического связывания: подключаемый компонент должен быть версией, определенной в соответствующем классе, выполняя, например, откат LINE_INSERTION, LINE_DELETION или команду любого другого типа, определенного тем объектом, к которому присоединен requested во время вызова.

Действия системы

Ни одна из рассмотренных частей структуры не зависела до сих пор от специфики приложения. Фактические операции приложения, основанные на структурах специфических объектов, например, структурах, представляющих текст в текстовом редакторе, - находятся где-то в другом месте. Как же осуществляется соединение?

Ответ основан на процедурах execute и undo классов command, которые должны вызывать компоненты, специфические для приложения. Например, процедура execute класса LINE_DELETION должна иметь доступ к классам, специфическим для текстового редактора, чтобы вызывать компоненты, вырабатывающие текст конкретной строки, задающие ее позицию в тексте.

Результатом является четкое разделение части системы, обеспечивающей взаимодействие с пользователем, и той ее части, которая зависит от специфики приложения. Вторая часть близка к концептуальной модели приложения - обработке текстов, CAD-CAM или чему-нибудь еще. Первая часть, особенно с учетом механизма истории действий пользователя, будет, как поясняется, широко использоваться в самых разных областях приложения.

Как создается объект command

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

if "Запрос является LINE INSERTION" then
    create {LINE_INSERTION} requested.make (input_text, cursor_index)
elseif "Запрос является LINE DELETION" then
    create {LINE_DELETION} requested.make (current_line, line_index)
elseif
    ...

Используемая здесь форма инструкции создания create {SOME_TYPE} x создает объект типа SOME_TYPE и присоединяет его к x. Тип SOME_TYPE должен соответствовать типу объявления x. Это имеет место в данном случае, так как requested имеет тип COMMAND и все классы команд являются потомками COMMAND.

Если каждый тип команды использует уникальный числовой ( integer ) или символьный ( character ) код, то слегка упрощенная форма предыдущей записи может использовать inspect:

inspect
    request_code
when Line_insertion then
    create {LINE_INSERTION} requested.make (input_text, cursor_position)
и т.д.

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

Фактически можно получить более элегантное решение и полностью избавиться от разбора случаев. Мы увидим его в конце презентации.

Многоуровневый откат и повтор: UNDO-REDO

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

Список истории

Что не позволяло нам производить откат на большую глубину? Ответ очевиден - у нас был только один объект - последний созданный экземпляр COMMAND, доступный через requested.

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

Для обеспечения глубины отката достаточно заменить единственный объект requested списком, содержащим выполненные команды, - списком истории:

history: SOME_LIST [COMMAND]

Имя SOME_LIST не является именем настоящего класса, - в подлинном ОО стиле АТД мы исследуем, какие операции и свойства необходимы классу SOME_LIST, и позже вынесем заключение, какой же списочный класс из базовой библиотеки (Base library) следует использовать. Принципиальные операции, нужные нам непосредственно, хорошо известны из предыдущего обсуждения:

Список истории

Рис. 3.3. Список истории
  • Put - команда вставки элемента в конец списка (единственное необходимое нам место вставки). По соглашению, put позиционирует курсор списка на только что вставленном элементе.
  • Empty - запрос определения пустоты списка.
  • Before, is_first и is_last - запросы о позиции курсора.
  • Back, forth - команды, передвигающие курсор назад, вперед на одну позицию.
  • Item - запрос элемента в позиции, заданной курсором. Этот компонент имеет предусловие: (not empty) and (not before), которое можно выразить как запрос on_item.

В отсутствие откатов курсор всегда (за исключением пустого списка) будет указывать на последний элемент и is_last будет истинным. Если же пользователь начнет выполнять откат, курсор начнет передвигаться назад по списку вплоть до before, если отменяются все выполненные команды. Когда же начинается повтор, то курсор перемещается вперед.

На рис. 3.3 курсор указывает на элемент, отличный от последнего. Это означает, что пользователь выполнял откат, возможно, перемежаемый повторами. Заметьте, число команд Undo всегда не меньше числа Redo (в состоянии на рисунке оно на два больше). Если в этом состоянии пользователь выберет обычную команду (ни Undo, ни Redo) соответствующий элемент будет вставлен непосредственно справа от курсора. Это означает, что остававшиеся справа в списке элементы будут потеряны, так для них не имеет смысла выполнение Redo. Здесь возникает та же ситуация, которая привела нас в начале лекции к введению понятия операции Skip (см. У3.4). Как следствие, в классе SOME_LIST понадобится еще один компонент - процедура remove_all_right, удаляющий все элементы справа от курсора.

Выполнение Undo возможно, если и только если курсор стоит на элементе с истинным значением on_item. Выполнение Redo возможно, если и только если был сделан откат, для которого еще не выполнена операция Redo, - это означает истинность выражения: (not empty) and (not is_last), которое будем называть запросом not_last.

Реализация Undo

Имея список истории, достаточно просто реализовать Undo:

if on_item then
    history.item.undo
    history.back
else
    message ("Нет команды для отката - undo")
end

И снова динамическое связывание играет основную роль. Список истории history является полиморфной структурой данных:

Список истории с различными объектами command

Рис. 3.4. Список истории с различными объектами command

При передвижении курсора влево каждое успешное значение history.item может быть присоединено к объекту любого доступного типа command. Динамическое связывание гарантирует, что в каждом случае history.item.undo автоматически выберет нужную версию undo.

Реализация Redo

Реализация Redo аналогична:

if not_last then
        history.forth
        history.item.redo
    else
        message ("Нет команды для отката - undo")
    end

Предполагается, что в классе COMMAND введена новая процедура redo. До сих пор считалось верным, что redo - это то же самое, что и execute. Это справедливо в большинстве случаев, но для некоторых команд повторное выполнение может отличаться от выполнения с нуля. Лучший способ справиться с такой ситуацией, не жертвуя общностью, - задать для redo поведение по умолчанию в классе COMMAND:

redo is
            -- Повтор команды, которую можно отменить,
            -- по умолчанию эквивалентно ее выполнению.
    do
        execute
    end

Наличие реализации превращает класс COMMAND в класс, определяющий поведение (см. лекцию 4 курса "Основы объектно-ориентированного программирования"). Он имеет отложенные процедуры execute и undo и эффективную процедуру redo. Большинство из потомков сохранят поведение по умолчанию redo, но некоторые зададут поведение, соответствующее специфике команды.

Выполнение обычных команд

Обычная команда по-прежнему идентифицируется ссылкой requested. Такую команду следует не только выполнить, но и добавить ее в список истории, предварительно удалив все элементы справа от курсора. В результате получим:

if not is_last then remove_all_right end
history.put (requested)
    -- Напомним, put вставляет элемент в конец списка,
    -- курсор указывает на новый элемент
requested.execute

Мы рассмотрели все основные элементы решения. В оставшейся части лекции обсудим некоторые аспекты реализации и извлечем из нашего примера методологические уроки.

< Лекция 2 || Лекция 3: 123456 || Лекция 4 >
Игорь Хан
Игорь Хан
Узбекистан, Ташкент, Ташкентский педагогический институт иностранных языков, 1990