Наследование: "откат" в интерактивных системах
Аспекты реализации
Давайте займемся деталями, что позволит получить лучшую из возможных реализаций.
Аргументы команды
Некоторым командам нужны аргументы. Например, команде LINE_INSERTION нужен текст вставляемой строки.
Простым решением является добавление атрибута и процедуры в класс COMMAND:
argument: ANY set_argument (a: like argument) is do argument := a end
Тогда любой потомок - командный класс - сможет переопределить argument, задав для него подходящий тип. Чтобы справиться с множественными аргументами, достаточно выбрать массив или списочный тип. Такова была техника, принятая выше, при передаче аргументов процедуре создания объектов командных классов.
Эта техника подходит для всех простых приложений. Заметьте, библиотечный класс COMMAND в среде ISE использует другую технику, немного более сложную, но более гибкую: здесь нет атрибута argument, но процедура execute имеет аргумент в обычном для процедур смысле:
execute (command_argument: ANY) is ...
Причина в том, что в графических системах удобнее позволять различным экземплярам одного и того же командного типа разделять один и тот же аргумент. Удаляя атрибут, мы получаем возможность повторно использовать тот же командный объект во многих различных контекстах, избегая создания нового командного объекта всякий раз, когда пользователь запрашивает команду.
Небольшое усложнение связано с тем, что теперь элементы списка истории уже не являются экземплярами COMMAND - они теперь должны быть экземплярами класса COMMAND_INSTANCE с атрибутами:
command_type: COMMAND argument: ANY
Для серьезных систем стоит пойти на усложнение ради выигрыша в памяти и времени. В этом варианте создается один объект на каждый тип команды, а не на каждую выполняемую команду. Эта техника рекомендуется для производственных систем. Необходимо лишь изменить некоторые детали в рассмотренном ранее классе (см. У3.4).
Предвычисленные командные объекты
Еще до выполнения команды следует получить, а иногда и создать соответствующий командный объект. Для абстрактно написанной инструкции "Создать подходящий командный объект и присоединить его к requested " была предложена схема реализации:
inspect request_code when Line_insertion then create {LINE_INSERTION} requested.make (...) и т.д. (одна ветвь для каждого типа команды)
Как отмечалось, здесь нет нарушения принципа Единственного Выбора: фактически это и есть точка выбора - единственное место в системе, знающее, какое множество команд поддерживается. Но к этому времени у нас выработалось здоровое отвращение к инструкциям if или inspect, содержащим много ветвей. Давайте попытаемся избавиться от них, хотя их присутствие кажется на первый взгляд неизбежным.
Мы создадим широко применимый образец проектирования, который может быть назван множество предвычисленных полиморфных экземпляров (precomputing a polymorphic instance set).
Идея достаточно проста - создать раз и навсегда полиморфную структуру данных, содержащую по одному экземпляру каждого варианта, затем, когда нужен новый объект, просто получаем его из соответствующего входа в структуру.
Хотя для этого возможны различные структуры, например списки, мы будем использовать массив ARRAY [COMMAND], позволяющий идентифицировать каждый тип команды целым в интервале 1 и до command_count - числом типов команд. Объявим:
commands: ARRAY [COMMAND]
и инициализируем его элементы так, чтобы i -й элемент ( 1 <= i <= n ) ссылался на экземпляр класса потомка COMMAND, соответствующего коду i ; например, создадим экземпляр LINE_DELETION, свяжем его с первым элементом массива, так что удаление строки будет иметь код 1.
Подобная техника может быть применена к полиморфному массиву associated_state, используемому в ОО-решении предыдущей лекции для приложения, управляемого панелями. |
Массив commands дает еще один пример мощи полиморфных структур данных. Его инициализация тривиальна:
create commands.make (1, command_count) create {LINE_INSERTION} requested.make; commands.put (requested, 1) create {STRING_REPLACE} requested.make; commands.put (requested, 2) ... И так для каждого типа команд ...
Заметьте, при этом подходе процедуры создания не должны иметь аргументов; если командный класс имеет атрибуты, то следует устанавливать их значения позднее в специально написанных процедурах, например li.make (input_text, cursor_position), где li типа LINE_INSERTION.
Теперь исчезла необходимость применения разбора случаев и ветвящихся инструкций if или inspect. Приведенная выше инициализация служит теперь точкой Единственного Выбора. Теперь реализацию абстрактной операции "Создать подходящий командный объект и присоединить его к requested " можно записать так:
requested := clone (commands @ code)
где code является кодом последней команды. Так как каждый тип команды имеет теперь код, соответствующий его индексу в массиве, то базисная операция интерфейса, ранее написанная в виде "Декодировать запрос", анализирует запрос пользователя и определяет соответствующий код.
В присваивании requested используется клон ( clone ) шаблона команды из массива, так что можно получать более одного экземпляра одной и той же команды в списке истории (как это показано в предыдущем примере, где в списке истории присутствовали два экземпляра LINE_DELETION ).
Если, однако, использовать предложенную технику, полностью отделяющую аргументы команды от командных объектов (так что список истории содержит экземпляры COMMAND_INSTANCE, а не COMMAND ), то тогда в получении клонов нет необходимости, и можно перейти к использованию ссылок на оригинальные объекты из массива:
requested:= commands @ code
В длительных сессиях такая техника может давать существенный выигрыш.
Представление списка истории
Для списка истории был задан абстрактный тип SOME_LIST, обладающий компонентами: put, empty, before, is_first, is_last, back, forth, item и remove_all_right. (Есть также on_item, выраженный в терминах empty и before, и not_last, выраженный в терминах empty и is_last.)
Большинство из списочных классов базовой библиотеки можно использовать для реализации SOME_LIST ; например, класс TWO_WAY_LIST или одного из потомков класса CIRCULAR_LIST. Для получения независимой версии рассмотрим специально подобранный класс BOUNDED_LIST. В отличие от ссылочной реализации списков, подобных TWO_WAY_LIST, этот класс основан на массиве, так что он хранит лишь ограниченное число команд в истории. Пусть remembered будет максимальным числом хранимых команд. Если используется в системе подобное свойство, то запомните (если не хотите получить гневное письмо от меня как от пользователя вашей системы): этот максимум должен задаваться пользователем либо во время сессии, либо в профиле пользователя. По умолчанию он должен выбираться никак не менее 20.
Список BOUNDED_LIST может использовать массив с циклическим управлением, позволяющий использовать ранее занятые элементы, когда число команд переваливает за максимум remembered. Эта техника является общей для представления ограниченных очередей. Массив в этом случае представляется в виде баранки:
Размером capacity массива является remembered + 1 ; это соглашение означает фиксирование одной из позиций (последней с индексом capacity ), оно необходимо для различения пустого и полностью заполненного списка. Занятые позиции помечены двумя целочисленными атрибутами: oldest - является позицией самой старой запомненной команды, и next - первая свободная позиция (для следующей команды). Атрибут index указывает текущую позицию курсора.
Вот как выглядит реализация компонентов. Для put(c), вставляющей команду c в конец списка, имеем:
representation.put (x, next); --где representation это имя массива next:= (next\\ remembered) + 1 index:= next
где операция \\ представляет остаток от деления нацело. Значение empty истинно, если и только если next = oldest ; значение is_first истинно, если и только если index = oldest; и before истинно, если и только если (index\\ remembered) + 1 = oldest. Телом forth является:
index:= (index\\ remembered) + 1 а телом back: index:= ((index + remembered - 2) \\ remembered) + 1
Терм +remembered математически избыточен, но он включен из-за отсутствия стандартного соглашения для операции взятия остатка в случае отрицательных операндов. |
Запрос item возвращает элемент в позиции курсора - representation @ index, - элемент массива с индексом index. Наконец, процедура remove_all_right, удаляющая все элементы справа от курсора, реализована так:
next:= (index remembered) + 1