Человеку свойственно ошибаться - чтобы окончательно все запутать, дайте ему компьютер. Чем быстрее становятся наши интерактивные системы, тем проще выполнить совсем не желанные действия. Вот почему хотелось бы иметь способ стереть прошлое, но не "большой красной кнопкой", стирающей все, - одной из компьютерных шуток, а иметь Большую Зеленую Кнопку, нажатие которой избавляет нас от сделанных ошибок.
В интерактивных системах эквивалентом Большой Зеленой Кнопки является операция отката Undo, дающая пользователю системы возможность отменить действие последней выполненной команды.
Исходная цель механизма отката - справиться с потенциально опасными ошибками ввода (напечатан не тот символ, нажата не та кнопка). Откат позволяет достичь большего. Помимо освобождения от нервного напряжения и боязни сделать что-то не то, он поощряет использовать стратегию " Что-Если ", - стиль взаимодействия, при котором пользователи сознательно испытывают различные варианты ввода, анализируя полученные результаты, зная при этом, что всегда есть возможность вернуться к предыдущему состоянию.
Каждая хорошая интерактивная система должна обеспечивать подобный механизм. (По этой причине на клавиатуре моего компьютера есть кнопка Undo, хотя она вовсе не зеленая и не особенно большая. Жаль только, что не все разработчики ПО предусматривают ее использование.)
В некоторых системах откат ограничен одним уровнем. Если не делать двух ошибок подряд, то этого достаточно. Но если вы пошли не по той дороге и хотите вернуться назад, то нужен многоуровневый откат.
Нет никаких причин ограничиваться одним уровнем. Как только механизм отката разработан, распространение его на несколько уровней не представляет особого труда, что и будет показано в этой лекции. И, пожалуйста, говорю уже как потенциальный пользователь, не ограничивайте число уровней отката, а если уж вынуждены это сделать, то пусть ограничение задает сам пользователь, во всяком случае по умолчанию оно должно быть не меньше 20. Затраты на откат невелики, если применять описываемую здесь технику.
Многоуровневый откат может быть избыточным. Потому необходима независимая операция повтора , в которой нет нужды, если откат ограничен одним шагом.
Хотя при разумных усилиях механизм undo-command, что может и не придти в голову, если не думать об откате при проектировании системы.
Практичный механизм undo- требует учета нескольких требований. Это свойство следует включить в интерфейс пользователя. Для начала можно полагать, что множество доступных операций обогащено двумя новыми командами: Undo и . (Для них может быть введена соответствующая комбинация горячих клавиш, например control-U и control-R.) Команда Undo отменяет эффект последней еще не отмененной команды; повторно выполняет команду, отмененную при откате. Следует определить соглашения для попыток отката на большее число шагов, чем их было сделано первоначально, при попытках повтора, когда не было отката, - такие запросы можно игнорировать или выдавать undo-. В конце лекции мы увидим, что возможен лучший вариант интерфейса.)
Второе, что следует учитывать, действия не всех команд могут быть отменены. В некоторых ситуациях этого нельзя сделать фактически: после выполнения команды "запуск ракет" (которую может отдать, как известно, лишь президент) или менее драматичной "отпечатать страницу" действия этих команд необратимы. В других ситуациях эффект от действия команды может быть устранен, но ценой слишком больших усилий. Например, текстовые редакторы, как правило, не позволяют отменить действия команды Save, записывающей текущее состояние документа в
Контрпримером является часто используемое мной приложение, обрабатывающее документы, которое изредка сообщает, что запрашиваемую команду нельзя отменить, хотя причины этого ясны только самой программе. Интересно, что в каком-то смысле это утверждение ложно, - фактически вы можете отменить эффект команды, но не через Undo, а через команду "Вернуться к последней сохраненной версии документа". Это наблюдение приводит к следующему правилу: всякий раз, когда команду законно следует признать необратимой, не поступайте, как в приведенном выше примере, выводя сообщение "Эта команда будет необратимой". Вместо выбора двух возможностей - Continue anyway и Cancel - предоставьте пользователю три: сохранить документ и затем выполнить команду, выполнить без сохранения, отмена команды. |
Наконец, можно попытаться предложить общую схему "Undo,
(1) Добавить строку в конец текста. (2) Удалить вторую строку.
После отмены обеих команд пользователь захотел пропустить выполнение первой и повторно выполнить только вторую (
Механизм undo-
Первое требование следует из того, что ничего проблемно-специфического в откатах и повторах нет. Только для облегчения обсуждения мы будем использовать в качестве примера знакомый каждому инструмент - текстовый редактор, (подобный Notepad или Vi), позволяющий пользователям вводить тексты и выполнять такие команды, как: INSERT_LINE, DELETE_LINE, GLOBAL_REPLACEMENT (одного слова в тексте другим) и другие. Но это только пример, и ни одна из концепций, обсуждаемых ниже, не является характерной только для текстовых редакторов.
Второе требование означает, что Undo и
If "Последняя команда была INSERT_LINE" then
"Undo эффект INSERT_LINE"
elseif "Последняя команда была DELETE_LINE" then
"Undo эффект DELETE_LINE"
и т.д.
Мы знаем (см. лекцию 3 курса "Основы объектно-ориентированного программирования"), как плохи такие структуры, противоречащие принципу Единственного Выбора и затрудняющие расширяемость системы. Пришлось бы изменять программный текст при всяком добавлении новой команды. Хуже того, код каждой ветви отражал бы код соответствующей команды, например, первая ветвь должна бы знать достаточно много о том, что делает команда INSERT_LINE. Это было бы свидетельством изъянов проекта.
Третье требование заставляет нас бережно относиться к памяти. Понятно, что механизм undo-
Вследствие третьего требования исключается такое очевидное решение, как сохранение полного состояния системы перед выполнением каждой команды. Такое решение можно было бы тривиально написать, используя свойства STORABLE (см.лекцию 8 курса "Основы объектно-ориентированного программирования"), но оно было бы нереалистичным, так как просто пожирало бы память. Нужно придумать что-то более разумное.
Последнее требование поддержки произвольного числа уровней отката уже обсуждалось. В данном случае оказывается проще рассмотреть откат на один уровень и затем обобщить решение на произвольное число уровней.
Этими требованиями заканчивается презентация проблемы. Хорошей идеей, как обычно, является попытка самостоятельного поиска решения, прежде чем продолжить чтение этой лекции.
Ключом ОО-решения является поиск правильных абстракций. Здесь фундаментальное понятие буквально напрашивается.
Для нашей проблемы характерна фундаментальная абстракция данных COMMAND, представляющая любую операцию, отличающуюся от Undo и
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, если она потребуется, или для повтора .
(рис 3.1) Объект commandАтрибуты, такие как deleted_line и deleted_line_index, у каждой команды будут свои, но всегда они должны быть достаточными для поддержки локальных операций execute и undo. Объекты, концептуально описывающие разницу между двумя состояниями приложения: предшествующим и последующим за выполнением команды, дают возможность удовлетворить требование U3 из нашего списка - хранить только то, что строго необходимо.
Структура наследования классов выглядит следующим образом:
(рис 3.2) Иерархия классов COMMANDГраф показан плоским (все потомки COMMAND находятся на одном уровне), но ничто не мешает добавить некую структуру, группируя команды по типам, где каждая категория может иметь общие специфические черты.
При определении понятия важно указать, какие характеристики оно не покрывает. Здесь концепция команды не включает Undo и
Это хороший пример ограничений упрощенного подхода к " |
Вначале посмотрим, как выглядит поддержка отката одного уровня. Обобщение на произвольное число уровней будет сделано позже.
В любой интерактивной системе в модуле, ответственном за коммуникацию с пользователем, должен быть некоторый фрагмент следующего вида:
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, означает
Располагая понятием объекта 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
Булева сущность |
Информация, сохраняемая перед каждым выполнением команды, задается в экземпляре некоторого потомка COMMAND, такого как LINE_DELETION. Это означает, что, как и анонсировалось, решение удовлетворяет свойству U3 в списке требований: хранится не все состояние, а только разница между новым состоянием и предыдущим.
Ключом решения - и его уточнением в оставшейся части лекции - является requested полиморфен: объявленный как COMMAND он присоединяется к объектам одного из эффективных потомков, таким как LINE_INSERTION. Вызовы requested.execute и requested.undo осмыслены из-за динамического связывания: подключаемый компонент должен быть версией, определенной в соответствующем классе, выполняя, например, откат LINE_INSERTION, LINE_DELETION или команду любого другого типа, определенного тем объектом, к которому присоединен requested во время вызова.
Ни одна из рассмотренных частей структуры не зависела до сих пор от специфики приложения. Фактические операции приложения, основанные на структурах специфических объектов, например, структурах, представляющих текст в текстовом редакторе, - находятся где-то в другом месте. Как же осуществляется соединение?
Ответ основан на процедурах execute и undo классов command, которые должны вызывать компоненты, специфические для приложения. Например, процедура execute класса LINE_DELETION должна иметь доступ к классам, специфическим для текстового редактора, чтобы вызывать компоненты, вырабатывающие текст конкретной строки, задающие ее позицию в тексте.
Результатом является четкое разделение части системы, обеспечивающей взаимодействие с пользователем, и той ее части, которая зависит от специфики приложения. Вторая часть близка к
После декодирования запроса система должна создать соответствующий объект 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
request_code
when Line_insertion then
create {LINE_INSERTION} requested.make (input_text, cursor_position)
и т.д.
Обе формы являются ветвящимся множественным выбором, но они не нарушают принцип Единственного Выбора. Как отмечалось при его обсуждении, если система предусматривает выбор, то некоторая часть системы должна знать полный список альтернатив. Оба рассмотренных варианта задают точку единственного выбора. Принцип запрещает лишь распространение этого знания на большое число модулей. В данном случае нет никакой другой части системы, которой нужен был бы доступ к списку команд; каждый командный класс имеет дело лишь с одной командой.
Фактически можно получить более элегантное решение и полностью избавиться от разбора случаев. Мы увидим его в конце презентации.
Поддержка отката произвольной глубины и сопровождающего его повтора представляет прямое расширение рассмотренной нами схемы.
Что не позволяло нам производить откат на большую глубину? Ответ очевиден - у нас был только один объект - последний созданный экземпляр COMMAND, доступный через requested.
Фактически мы создавали столь много объектов, сколько команд выполнял пользователь. Но поскольку в нашем проекте присутствует только одна ссылка на командный объект - |
Для обеспечения глубины отката достаточно заменить единственный объект 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 всегда не меньше числа SOME_LIST понадобится еще один компонент - процедура remove_all_right, удаляющий все элементы справа от курсора.
Выполнение Undo возможно, если и только если курсор стоит на элементе с истинным значением on_item. Выполнение (not empty) and (not is_last), которое будем называть запросом not_last.
Имея список истории, достаточно просто реализовать Undo:
if on_item then
history.item.undo
history.back
else
message ("Нет команды для отката - undo")
end
И снова history является полиморфной структурой данных:
(рис 3.4) Список истории с различными объектами commandПри передвижении курсора влево каждое успешное значение history.item может быть присоединено к объекту любого доступного типа command. history.item.undo автоматически выберет нужную версию undo.
Реализация
if not_last then
history.forth
history.item.redo
else
message ("Нет команды для отката - undo")
end
Предполагается, что в классе COMMAND введена новая процедура . До сих пор считалось верным, что - это то же самое, что и execute. Это справедливо в большинстве случаев, но для некоторых команд повторное выполнение может отличаться от выполнения с нуля. Лучший способ справиться с такой ситуацией, не жертвуя общностью, - задать для COMMAND:
redo is
-- Повтор команды, которую можно отменить,
-- по умолчанию эквивалентно ее выполнению.
do
execute
end
Наличие реализации превращает класс COMMAND в класс, определяющий поведение (см. лекцию 4 курса "Основы объектно-ориентированного программирования"). Он имеет отложенные процедуры execute и undo и эффективную процедуру . Большинство из потомков сохранят поведение по умолчанию , но некоторые зададут поведение, соответствующее специфике команды.
Обычная команда по-прежнему идентифицируется ссылкой requested. Такую команду следует не только выполнить, но и добавить ее в список истории, предварительно удалив все элементы справа от курсора. В результате получим:
if not is_last then remove_all_right end
history.put (requested)
-- Напомним, put вставляет элемент в конец списка,
-- курсор указывает на новый элемент
requested.execute
Мы рассмотрели все основные элементы решения. В оставшейся части лекции обсудим некоторые аспекты реализации и извлечем из нашего примера методологические уроки.
Давайте займемся деталями, что позволит получить лучшую из возможных реализаций.
Некоторым командам нужны аргументы. Например, команде LINE_INSERTION нужен текст вставляемой строки.
Простым решением является добавление атрибута и процедуры в класс COMMAND:
argument: ANY
set_argument (a: like argument) is
do argument := a end
Тогда любой потомок - командный класс - сможет переопределить , задав для него подходящий тип. Чтобы справиться с множественными аргументами, достаточно выбрать массив или списочный тип. Такова была техника, принятая выше, при передаче аргументов процедуре создания объектов командных классов.
Эта техника подходит для всех простых приложений. Заметьте, библиотечный класс COMMAND в среде ISE использует другую технику, немного более сложную, но более гибкую: здесь нет атрибута , но процедура execute имеет аргумент в обычном для процедур смысле:
execute (command_argument: ANY) is ...
Причина в том, что в
Небольшое усложнение связано с тем, что теперь элементы списка истории уже не являются экземплярами COMMAND - они теперь должны быть экземплярами класса COMMAND_INSTANCE с атрибутами:
command_type: COMMAND argument: ANY
Для серьезных систем стоит пойти на усложнение ради выигрыша в памяти и времени. В этом варианте создается один объект на каждый
Еще до выполнения команды следует получить, а иногда и создать соответствующий командный объект. Для абстрактно написанной инструкции "Создать подходящий командный объект и присоединить его к requested " была предложена схема реализации:
inspect
request_code
when Line_insertion then
create {LINE_INSERTION} requested.make (...)
и т.д. (одна ветвь для каждого типа команды)
Как отмечалось, здесь нет нарушения принципа Единственного Выбора: фактически это и есть точка выбора - единственное место в системе, знающее, какое множество команд поддерживается. Но к этому времени у нас выработалось здоровое отвращение к инструкциям if или , содержащим много ветвей. Давайте попытаемся избавиться от них, хотя их присутствие кажется на первый взгляд неизбежным.
Мы создадим широко применимый образец проектирования, который может быть назван множество предвычисленных полиморфных экземпляров (precomputing a polymorphic instance set).
Идея достаточно проста - создать раз и навсегда полиморфную структуру данных, содержащую по одному экземпляру каждого варианта, затем, когда нужен новый объект, просто получаем его из соответствующего входа в структуру.
Хотя для этого возможны различные структуры, например списки, мы будем использовать массив ARRAY [COMMAND], позволяющий идентифицировать каждый 1 и до command_count - числом
commands: ARRAY [COMMAND]
и инициализируем его элементы так, чтобы i -й элемент ( 1 <= i <= n ) ссылался на экземпляр класса потомка COMMAND, соответствующего коду i ; например, создадим экземпляр LINE_DELETION, свяжем его с первым элементом массива, так что удаление строки будет иметь код 1.
(рис 3.5) Массив шаблонов командПодобная техника может быть применена к полиморфному массиву |
Массив 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 или . Приведенная выше инициализация служит теперь точкой Единственного Выбора. Теперь реализацию абстрактной операции "Создать подходящий командный объект и присоединить его к requested " можно записать так:
requested := clone (commands @ code)
где code является кодом последней команды. Так как каждый
В присваивании requested используется клон ( ) шаблона команды из массива, так что можно получать более одного экземпляра одной и той же команды в списке истории (как это показано в предыдущем примере, где в списке истории присутствовали два экземпляра 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 будет максимальным числом хранимых команд. Если используется в системе подобное свойство, то запомните (если не хотите получить гневное письмо от меня как от пользователя вашей системы): этот максимум должен задаваться пользователем либо во время сессии, либо в
Список BOUNDED_LIST может использовать массив с циклическим управлением, позволяющий использовать ранее занятые элементы, когда число команд переваливает за максимум remembered. Эта техника является общей для представления ограниченных очередей. Массив в этом случае представляется в виде баранки:
(рис 3.6) Ограниченный циклический список, реализуемый массивомРазмером массива является remembered + 1 ; это соглашение означает фиксирование одной из позиций (последней с индексом ), оно необходимо для различения пустого и полностью заполненного списка. 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
Терм |
Запрос item возвращает элемент в , - элемент массива с индексом index. Наконец, процедура remove_all_right, удаляющая все элементы справа от курсора, реализована так:
next:= (index remembered) + 1
Покажем, как выглядит возможный интерфейс пользователя, поддерживающий механизм undo-
Хотя горячие клавиши доступны для Undo и history. Однажды открытое, оно регулярно обновляется при выполнении команд. В отсутствие откатов оно выглядит так:
(рис 3.7) Окно истории до выполнения откатовОно отображает список выполненных команд. При выполнении новой команды, она появится в конце списка. Текущая активная команда (отмеченная курсором) подсвечена, как показано на рисунке для " change relation label ".
Для отката достаточно щелкнуть по кнопке со стрелкой $$\uparrow$$ или использовать горячие клавиши (Alt-U). Если передвинуть курсор вверх (для списка это переход назад - back) то после нескольких операций Undo, окно примет вид, показанный на рис. 3.8.
В этом состоянии есть выбор:
forth.
(рис 3.8) Окно истории в процессе откатов и повторовМожно выполнить нормальную команду. Как мы знаем, из истории удалятся все команды, для которых был откат, но не было повтора; для списка это означает удаление элементов справа от курсора и вызов remove_all_right; все команды ниже подсвеченной исчезнут.
Образец проектирования, представленный в в этой лекции, играет важную практическую роль, позволяя при незначительных усилиях создавать значительно лучшие интерактивные системы. Он также вносит интересный теоретический вклад, освещая некоторые аспекты ОО методологии, заслуживающие дальнейшего исследования.
Замечательное свойство пользовательского интерфейса, представленного в последнем разделе, состоит в том, что оно непосредственно выведено из реализации, - взяв внутреннее, относящееся к разработке понятие списка истории, мы транслировали его во внешнее, относящееся к пользователю понятие окна истории с привлекательным пользовательским механизмом взаимодействия.
Можно представить, что кто-то мог бы вначале придумать |
Существование такого отношения между функциональностью системы и ее реализацией противоречит всему тому, чему учит традиционная
Наивно верить, что пользователи могут предложить правильные свойства интерфейса. Иногда они могут это сделать, но, чаще всего, они будут исходить из свойств, знакомых им по прежним системам. Это понятно, у них своя работа, своя область, в которой они являются экспертами, и нельзя на них возлагать ответственность за то, что должно быть правильным в программной системе. Некоторые из худших интерактивных систем были спроектированы, находясь под слишком большим влиянием пользователей. Где действительно необходим вклад пользователей - так это их критические комментарии: они могут видеть изъяны в идее, которая могла казаться привлекательной разработчикам. Такой критицизм всегда необходим. Пользователи могут высказывать и блестящие положительные предложения тоже, но не следует быть зависимыми от них. Несмотря на критику иногда разработчикам удается склонить пользователей на свою сторону, возможно, после нескольких итераций и учета замечаний. И это происходит даже тогда, когда предложения вытекают из, казалось бы, чисто реализационных аспектов, как это было со списком истории.
Равенство традиционных отношений представляет важный вклад в объектную технологию. Рассматривая процесс разработки бесшовным и обратимым (см. лекцию 10;), мы допускаем влияние идей реализации на спецификации. Вместо одностороннего движения от анализа к проектированию и кодированию, приходим к непрерывному циклическому процессу с обратной связью. Реализация не должна рассматриваться как похлебка, низкоуровневый компонент конструирования системы. Разработанная с использованием методов, описанных в данной книге, она может и должна быть четкой, элегантной и абстрактной, ничуть не уступающей всему тому, что можно получить в презирающих реализацию традиционных формах анализа и проектирования.
Проект, описанный в этой лекции, может для типичных интерактивных систем включать достаточно много относительно небольших классов, по одному на каждую команду. Нет причин, однако, полагать, что это отражается на размере системы или ее сложности. Структура наследования классов остается простой, хотя она вовсе не должна быть плоской, - команды можно группировать по категориям.
При систематическом ОО-подходе такие вопросы возникают всякий раз, когда приходится вводить классы, представляющие действия. Хотя некоторые ОО-языки дают возможность передавать программы как аргументы в другие программы, такое свойство противоречит базисной идее Метода - функции (программы) не существуют сами по себе, - они лишь часть некоторой абстракции данных. Поэтому вместо передачи операций следует передавать объект, поставляемый вместе с операцией, например, экземпляр COMMAND, поставляемый с операцией execute.
Иногда для операций приходится писать обертывающий класс, что кажется искусственным, особенно для людей, привыкших передавать процедуры в качестве аргументов. Но мне неоднократно приходилось видеть, что класс, введенный с единственной целью COMMAND не попадает в эту категорию, он с самого начала рассматривался как абстракция данных и имел два компонента ( execute and undo ). Но, что типично, серьезная работа с командами приводит к осознанию необходимости других компонентов, таких как:
argument : ANY - для представления аргументов команды (как это было сделано в одной из версий проекта);help: STRING - для предоставления справки по каждой команде;Еще один пример взят из области численных вычислений. Рассмотрим классическую задачу вычисления f передается как аргумент в программу, вычисляющую f как функцию, при ОО-проектировании мы обнаруживаем, что "Интегрируемая функция" является важной абстракцией со многими возможными свойствами. Для пришедших из мира C, INTEGRABLE_FUNCTION - на самом деле является одной из главных абстракций проблемной области. В этом классе появятся новые полезные компоненты помимо компонента item (a: REAL): REAL, возвращающего значение функции в точке a.
То, что казалось лишь трюком, превращается в главную составляющую проекта.
Механизм undo-
На первой конференции OOPSLA в 1986 Larry Tesler говорил о механизме, основанном на той же идее, встроенном в интерактивный каркас Apple's MacApp.
В работе [Dubois 1997] объясняется в деталях, как применять ОО концепции в численных вычислениях, с приведением такой абстракции как "Integrable function".
Этот небольшой программистский проект является лучшим способом проверки понимания тем этой лекции и ОО-техники в целом.
Напишите текстовый редактор, ориентированный на работу со строками, поддерживающий следующие операции:
p: печать введенного текста;l, если это более удобно);h, если это более удобно);i: вставляет новую строку после d: удаляет строку в u: откат последней операции, если она не была Undo; если же это Undo, то выполняется повтор Можно добавить новые команды или спроектировать более привлекательный интерфейс, но во всех случаях следует создать законченную, работающую систему. (Возможно, вы сразу начнете с улучшений, описанных в следующем упражнении.)
Дополните одноуровневую схему предыдущего упражнения переопределением смысла операции отката u:
u: откат последней операции, отличной от Undo и
Добавьте операцию повтора
r: повтор последней операции, если она применима.Объясните, как применить рассмотренную технику в не ОО-языках, подобных Pascal, Ada (используя записи с вариантами) или C (используя структуры и union типы). Сравните с ОО-решениями.
С учетом проблем, поднятых в обсуждении, рассмотрите, как можно расширить механизм, разработанный в этой лекции так, чтобы он допускал поддержку Undo,
Рассмотрите аргументы команды независимо от команд и создайте только один командный объект на каждый
Если вы выполнили предыдущее упражнение, примените эту технику к решению.
В некоторых системах может быть полезным ввести понятие составной команды, выполнение которых включает выполнение нескольких других команд. Напишите соответствующий класс COMPOSITE_COMMAND, потомка COMMAND, убедитесь, что составные команды допускают откат и что компонента составной команды может быть составной командой.
Указание: используйте множественное наследование, представленное для составных фигур (см. лекцию 15 курса "Основы объектно-ориентированного программирования").
Система может включать
Указание: введите наследников UNDOABLE и NON_UNDOABLE класса COMMAND.
Напишите общецелевую библиотеку команд, предполагающую использование в произвольной интерактивной системе и поддерживающую неограниченный механизм undo-
Полезным компонентом, встраиваемым в командно-ориентированный инструментарий, является механизм истории, запоминающий выполненную команду и позволяющий пользователю повторно ее выполнить, возможно, модифицировав. Под Unix, например, доступен командный язык C-shell, запоминающий несколько последних выполненных команд. Вы можете напечатать !-2, означающее, что нужно выполнить команду, предшествующую последней. Запись ^yes^no^ означает "выполнение последней команды с заменой yes на no". Другие окружения предлагают схожие свойства.
Механизмы истории, когда они существуют, построены в соответствии с модой. Под Unix многие интерактивные средства, выполняемые под C-shell, такие как текстовый редактор Vi или различные отладчики, будут получать преимущества от такого механизма, но он не будет предлагаться другим системам. Это тем более вызывает сожаление, что те же концепции истории команд и те же ассоциированные свойства полезны любой интерактивной системе независимо от выполняемых ею функций.
Спроектируйте класс, реализующий механизм истории общецелевого назначения, так чтобы любая интерактивная система, нуждающаяся в этом механизме, могла получить его путем простого наследования класса. (Заметьте, множественное наследование здесь необходимо.)
Обсудите расширение этого механизма на общий класс USER_INTERFACE.
Тестирование компонентов ПО, например, класса требует определенных свойств при подготовке теста: ввода тестовых данных, выполнения теста, записи результатов, сравнения с ожидаемыми результатами и так далее. Определите общий, допускающий наследование класс TEST, задающий тестирующее окружение. (Обратите внимание, что и здесь важно множественное наследование.)
(Для читателей, знакомых с численными методами.) Напишите множество классов для интегрирования вещественных функций вещественной переменной на произвольном интервале. Сюда должен входить класс INTEGRABLE_FUNCTION, а также отложенный класс INTEGRATOR, описывающий метод интегрирования, и потомки класса, такие как RATIONAL_FIXED_INTEGRATOR.
Человеку свойственно ошибаться - чтобы окончательно все запутать, дайте ему компьютер. Чем быстрее становятся наши интерактивные системы, тем проще выполнить совсем не желанные действия. Вот почему хотелось бы иметь способ стереть прошлое, но не "большой красной кнопкой", стирающей все, - одной из компьютерных шуток, а иметь Большую Зеленую Кнопку, нажатие которой избавляет нас от сделанных ошибок.
В интерактивных системах эквивалентом Большой Зеленой Кнопки является операция отката Undo, дающая пользователю системы возможность отменить действие последней выполненной команды.
Исходная цель механизма отката - справиться с потенциально опасными ошибками ввода (напечатан не тот символ, нажата не та кнопка). Откат позволяет достичь большего. Помимо освобождения от нервного напряжения и боязни сделать что-то не то, он поощряет использовать стратегию " Что-Если ", - стиль взаимодействия, при котором пользователи сознательно испытывают различные варианты ввода, анализируя полученные результаты, зная при этом, что всегда есть возможность вернуться к предыдущему состоянию.
Каждая хорошая интерактивная система должна обеспечивать подобный механизм. (По этой причине на клавиатуре моего компьютера есть кнопка Undo, хотя она вовсе не зеленая и не особенно большая. Жаль только, что не все разработчики ПО предусматривают ее использование.)
В некоторых системах откат ограничен одним уровнем. Если не делать двух ошибок подряд, то этого достаточно. Но если вы пошли не по той дороге и хотите вернуться назад, то нужен многоуровневый откат.
Нет никаких причин ограничиваться одним уровнем. Как только механизм отката разработан, распространение его на несколько уровней не представляет особого труда, что и будет показано в этой лекции. И, пожалуйста, говорю уже как потенциальный пользователь, не ограничивайте число уровней отката, а если уж вынуждены это сделать, то пусть ограничение задает сам пользователь, во всяком случае по умолчанию оно должно быть не меньше 20. Затраты на откат невелики, если применять описываемую здесь технику.
Многоуровневый откат может быть избыточным. Потому необходима независимая операция повтора , в которой нет нужды, если откат ограничен одним шагом.
Хотя при разумных усилиях механизм undo-command, что может и не придти в голову, если не думать об откате при проектировании системы.
Практичный механизм undo- требует учета нескольких требований. Это свойство следует включить в интерфейс пользователя. Для начала можно полагать, что множество доступных операций обогащено двумя новыми командами: Undo и . (Для них может быть введена соответствующая комбинация горячих клавиш, например control-U и control-R.) Команда Undo отменяет эффект последней еще не отмененной команды; повторно выполняет команду, отмененную при откате. Следует определить соглашения для попыток отката на большее число шагов, чем их было сделано первоначально, при попытках повтора, когда не было отката, - такие запросы можно игнорировать или выдавать undo-. В конце лекции мы увидим, что возможен лучший вариант интерфейса.)
Второе, что следует учитывать, действия не всех команд могут быть отменены. В некоторых ситуациях этого нельзя сделать фактически: после выполнения команды "запуск ракет" (которую может отдать, как известно, лишь президент) или менее драматичной "отпечатать страницу" действия этих команд необратимы. В других ситуациях эффект от действия команды может быть устранен, но ценой слишком больших усилий. Например, текстовые редакторы, как правило, не позволяют отменить действия команды Save, записывающей текущее состояние документа в
Контрпримером является часто используемое мной приложение, обрабатывающее документы, которое изредка сообщает, что запрашиваемую команду нельзя отменить, хотя причины этого ясны только самой программе. Интересно, что в каком-то смысле это утверждение ложно, - фактически вы можете отменить эффект команды, но не через Undo, а через команду "Вернуться к последней сохраненной версии документа". Это наблюдение приводит к следующему правилу: всякий раз, когда команду законно следует признать необратимой, не поступайте, как в приведенном выше примере, выводя сообщение "Эта команда будет необратимой". Вместо выбора двух возможностей - Continue anyway и Cancel - предоставьте пользователю три: сохранить документ и затем выполнить команду, выполнить без сохранения, отмена команды. |
Наконец, можно попытаться предложить общую схему "Undo,
(1) Добавить строку в конец текста. (2) Удалить вторую строку.
После отмены обеих команд пользователь захотел пропустить выполнение первой и повторно выполнить только вторую (
Механизм undo-
Первое требование следует из того, что ничего проблемно-специфического в откатах и повторах нет. Только для облегчения обсуждения мы будем использовать в качестве примера знакомый каждому инструмент - текстовый редактор, (подобный Notepad или Vi), позволяющий пользователям вводить тексты и выполнять такие команды, как: INSERT_LINE, DELETE_LINE, GLOBAL_REPLACEMENT (одного слова в тексте другим) и другие. Но это только пример, и ни одна из концепций, обсуждаемых ниже, не является характерной только для текстовых редакторов.
Второе требование означает, что Undo и
If "Последняя команда была INSERT_LINE" then
"Undo эффект INSERT_LINE"
elseif "Последняя команда была DELETE_LINE" then
"Undo эффект DELETE_LINE"
и т.д.
Мы знаем (см. лекцию 3 курса "Основы объектно-ориентированного программирования"), как плохи такие структуры, противоречащие принципу Единственного Выбора и затрудняющие расширяемость системы. Пришлось бы изменять программный текст при всяком добавлении новой команды. Хуже того, код каждой ветви отражал бы код соответствующей команды, например, первая ветвь должна бы знать достаточно много о том, что делает команда INSERT_LINE. Это было бы свидетельством изъянов проекта.
Третье требование заставляет нас бережно относиться к памяти. Понятно, что механизм undo-
Вследствие третьего требования исключается такое очевидное решение, как сохранение полного состояния системы перед выполнением каждой команды. Такое решение можно было бы тривиально написать, используя свойства STORABLE (см.лекцию 8 курса "Основы объектно-ориентированного программирования"), но оно было бы нереалистичным, так как просто пожирало бы память. Нужно придумать что-то более разумное.
Последнее требование поддержки произвольного числа уровней отката уже обсуждалось. В данном случае оказывается проще рассмотреть откат на один уровень и затем обобщить решение на произвольное число уровней.
Этими требованиями заканчивается презентация проблемы. Хорошей идеей, как обычно, является попытка самостоятельного поиска решения, прежде чем продолжить чтение этой лекции.
Ключом ОО-решения является поиск правильных абстракций. Здесь фундаментальное понятие буквально напрашивается.
Для нашей проблемы характерна фундаментальная абстракция данных COMMAND, представляющая любую операцию, отличающуюся от Undo и
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, если она потребуется, или для повтора .
(рис 3.1) Объект commandАтрибуты, такие как deleted_line и deleted_line_index, у каждой команды будут свои, но всегда они должны быть достаточными для поддержки локальных операций execute и undo. Объекты, концептуально описывающие разницу между двумя состояниями приложения: предшествующим и последующим за выполнением команды, дают возможность удовлетворить требование U3 из нашего списка - хранить только то, что строго необходимо.
Структура наследования классов выглядит следующим образом:
(рис 3.2) Иерархия классов COMMANDГраф показан плоским (все потомки COMMAND находятся на одном уровне), но ничто не мешает добавить некую структуру, группируя команды по типам, где каждая категория может иметь общие специфические черты.
При определении понятия важно указать, какие характеристики оно не покрывает. Здесь концепция команды не включает Undo и
Это хороший пример ограничений упрощенного подхода к " |
Вначале посмотрим, как выглядит поддержка отката одного уровня. Обобщение на произвольное число уровней будет сделано позже.
В любой интерактивной системе в модуле, ответственном за коммуникацию с пользователем, должен быть некоторый фрагмент следующего вида:
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, означает
Располагая понятием объекта 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
Булева сущность |
Информация, сохраняемая перед каждым выполнением команды, задается в экземпляре некоторого потомка COMMAND, такого как LINE_DELETION. Это означает, что, как и анонсировалось, решение удовлетворяет свойству U3 в списке требований: хранится не все состояние, а только разница между новым состоянием и предыдущим.
Ключом решения - и его уточнением в оставшейся части лекции - является requested полиморфен: объявленный как COMMAND он присоединяется к объектам одного из эффективных потомков, таким как LINE_INSERTION. Вызовы requested.execute и requested.undo осмыслены из-за динамического связывания: подключаемый компонент должен быть версией, определенной в соответствующем классе, выполняя, например, откат LINE_INSERTION, LINE_DELETION или команду любого другого типа, определенного тем объектом, к которому присоединен requested во время вызова.
Ни одна из рассмотренных частей структуры не зависела до сих пор от специфики приложения. Фактические операции приложения, основанные на структурах специфических объектов, например, структурах, представляющих текст в текстовом редакторе, - находятся где-то в другом месте. Как же осуществляется соединение?
Ответ основан на процедурах execute и undo классов command, которые должны вызывать компоненты, специфические для приложения. Например, процедура execute класса LINE_DELETION должна иметь доступ к классам, специфическим для текстового редактора, чтобы вызывать компоненты, вырабатывающие текст конкретной строки, задающие ее позицию в тексте.
Результатом является четкое разделение части системы, обеспечивающей взаимодействие с пользователем, и той ее части, которая зависит от специфики приложения. Вторая часть близка к
После декодирования запроса система должна создать соответствующий объект 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
request_code
when Line_insertion then
create {LINE_INSERTION} requested.make (input_text, cursor_position)
и т.д.
Обе формы являются ветвящимся множественным выбором, но они не нарушают принцип Единственного Выбора. Как отмечалось при его обсуждении, если система предусматривает выбор, то некоторая часть системы должна знать полный список альтернатив. Оба рассмотренных варианта задают точку единственного выбора. Принцип запрещает лишь распространение этого знания на большое число модулей. В данном случае нет никакой другой части системы, которой нужен был бы доступ к списку команд; каждый командный класс имеет дело лишь с одной командой.
Фактически можно получить более элегантное решение и полностью избавиться от разбора случаев. Мы увидим его в конце презентации.
Поддержка отката произвольной глубины и сопровождающего его повтора представляет прямое расширение рассмотренной нами схемы.
Что не позволяло нам производить откат на большую глубину? Ответ очевиден - у нас был только один объект - последний созданный экземпляр COMMAND, доступный через requested.
Фактически мы создавали столь много объектов, сколько команд выполнял пользователь. Но поскольку в нашем проекте присутствует только одна ссылка на командный объект - |
Для обеспечения глубины отката достаточно заменить единственный объект 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 всегда не меньше числа SOME_LIST понадобится еще один компонент - процедура remove_all_right, удаляющий все элементы справа от курсора.
Выполнение Undo возможно, если и только если курсор стоит на элементе с истинным значением on_item. Выполнение (not empty) and (not is_last), которое будем называть запросом not_last.
Имея список истории, достаточно просто реализовать Undo:
if on_item then
history.item.undo
history.back
else
message ("Нет команды для отката - undo")
end
И снова history является полиморфной структурой данных:
(рис 3.4) Список истории с различными объектами commandПри передвижении курсора влево каждое успешное значение history.item может быть присоединено к объекту любого доступного типа command. history.item.undo автоматически выберет нужную версию undo.
Реализация
if not_last then
history.forth
history.item.redo
else
message ("Нет команды для отката - undo")
end
Предполагается, что в классе COMMAND введена новая процедура . До сих пор считалось верным, что - это то же самое, что и execute. Это справедливо в большинстве случаев, но для некоторых команд повторное выполнение может отличаться от выполнения с нуля. Лучший способ справиться с такой ситуацией, не жертвуя общностью, - задать для COMMAND:
redo is
-- Повтор команды, которую можно отменить,
-- по умолчанию эквивалентно ее выполнению.
do
execute
end
Наличие реализации превращает класс COMMAND в класс, определяющий поведение (см. лекцию 4 курса "Основы объектно-ориентированного программирования"). Он имеет отложенные процедуры execute и undo и эффективную процедуру . Большинство из потомков сохранят поведение по умолчанию , но некоторые зададут поведение, соответствующее специфике команды.
Обычная команда по-прежнему идентифицируется ссылкой requested. Такую команду следует не только выполнить, но и добавить ее в список истории, предварительно удалив все элементы справа от курсора. В результате получим:
if not is_last then remove_all_right end
history.put (requested)
-- Напомним, put вставляет элемент в конец списка,
-- курсор указывает на новый элемент
requested.execute
Мы рассмотрели все основные элементы решения. В оставшейся части лекции обсудим некоторые аспекты реализации и извлечем из нашего примера методологические уроки.
Давайте займемся деталями, что позволит получить лучшую из возможных реализаций.
Некоторым командам нужны аргументы. Например, команде LINE_INSERTION нужен текст вставляемой строки.
Простым решением является добавление атрибута и процедуры в класс COMMAND:
argument: ANY
set_argument (a: like argument) is
do argument := a end
Тогда любой потомок - командный класс - сможет переопределить , задав для него подходящий тип. Чтобы справиться с множественными аргументами, достаточно выбрать массив или списочный тип. Такова была техника, принятая выше, при передаче аргументов процедуре создания объектов командных классов.
Эта техника подходит для всех простых приложений. Заметьте, библиотечный класс COMMAND в среде ISE использует другую технику, немного более сложную, но более гибкую: здесь нет атрибута , но процедура execute имеет аргумент в обычном для процедур смысле:
execute (command_argument: ANY) is ...
Причина в том, что в
Небольшое усложнение связано с тем, что теперь элементы списка истории уже не являются экземплярами COMMAND - они теперь должны быть экземплярами класса COMMAND_INSTANCE с атрибутами:
command_type: COMMAND argument: ANY
Для серьезных систем стоит пойти на усложнение ради выигрыша в памяти и времени. В этом варианте создается один объект на каждый
Еще до выполнения команды следует получить, а иногда и создать соответствующий командный объект. Для абстрактно написанной инструкции "Создать подходящий командный объект и присоединить его к requested " была предложена схема реализации:
inspect
request_code
when Line_insertion then
create {LINE_INSERTION} requested.make (...)
и т.д. (одна ветвь для каждого типа команды)
Как отмечалось, здесь нет нарушения принципа Единственного Выбора: фактически это и есть точка выбора - единственное место в системе, знающее, какое множество команд поддерживается. Но к этому времени у нас выработалось здоровое отвращение к инструкциям if или , содержащим много ветвей. Давайте попытаемся избавиться от них, хотя их присутствие кажется на первый взгляд неизбежным.
Мы создадим широко применимый образец проектирования, который может быть назван множество предвычисленных полиморфных экземпляров (precomputing a polymorphic instance set).
Идея достаточно проста - создать раз и навсегда полиморфную структуру данных, содержащую по одному экземпляру каждого варианта, затем, когда нужен новый объект, просто получаем его из соответствующего входа в структуру.
Хотя для этого возможны различные структуры, например списки, мы будем использовать массив ARRAY [COMMAND], позволяющий идентифицировать каждый 1 и до command_count - числом
commands: ARRAY [COMMAND]
и инициализируем его элементы так, чтобы i -й элемент ( 1 <= i <= n ) ссылался на экземпляр класса потомка COMMAND, соответствующего коду i ; например, создадим экземпляр LINE_DELETION, свяжем его с первым элементом массива, так что удаление строки будет иметь код 1.
(рис 3.5) Массив шаблонов командПодобная техника может быть применена к полиморфному массиву |
Массив 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 или . Приведенная выше инициализация служит теперь точкой Единственного Выбора. Теперь реализацию абстрактной операции "Создать подходящий командный объект и присоединить его к requested " можно записать так:
requested := clone (commands @ code)
где code является кодом последней команды. Так как каждый
В присваивании requested используется клон ( ) шаблона команды из массива, так что можно получать более одного экземпляра одной и той же команды в списке истории (как это показано в предыдущем примере, где в списке истории присутствовали два экземпляра 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 будет максимальным числом хранимых команд. Если используется в системе подобное свойство, то запомните (если не хотите получить гневное письмо от меня как от пользователя вашей системы): этот максимум должен задаваться пользователем либо во время сессии, либо в
Список BOUNDED_LIST может использовать массив с циклическим управлением, позволяющий использовать ранее занятые элементы, когда число команд переваливает за максимум remembered. Эта техника является общей для представления ограниченных очередей. Массив в этом случае представляется в виде баранки:
(рис 3.6) Ограниченный циклический список, реализуемый массивомРазмером массива является remembered + 1 ; это соглашение означает фиксирование одной из позиций (последней с индексом ), оно необходимо для различения пустого и полностью заполненного списка. 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
Терм |
Запрос item возвращает элемент в , - элемент массива с индексом index. Наконец, процедура remove_all_right, удаляющая все элементы справа от курсора, реализована так:
next:= (index remembered) + 1
Покажем, как выглядит возможный интерфейс пользователя, поддерживающий механизм undo-
Хотя горячие клавиши доступны для Undo и history. Однажды открытое, оно регулярно обновляется при выполнении команд. В отсутствие откатов оно выглядит так:
(рис 3.7) Окно истории до выполнения откатовОно отображает список выполненных команд. При выполнении новой команды, она появится в конце списка. Текущая активная команда (отмеченная курсором) подсвечена, как показано на рисунке для " change relation label ".
Для отката достаточно щелкнуть по кнопке со стрелкой $$\uparrow$$ или использовать горячие клавиши (Alt-U). Если передвинуть курсор вверх (для списка это переход назад - back) то после нескольких операций Undo, окно примет вид, показанный на рис. 3.8.
В этом состоянии есть выбор:
forth.
(рис 3.8) Окно истории в процессе откатов и повторовМожно выполнить нормальную команду. Как мы знаем, из истории удалятся все команды, для которых был откат, но не было повтора; для списка это означает удаление элементов справа от курсора и вызов remove_all_right; все команды ниже подсвеченной исчезнут.
Образец проектирования, представленный в в этой лекции, играет важную практическую роль, позволяя при незначительных усилиях создавать значительно лучшие интерактивные системы. Он также вносит интересный теоретический вклад, освещая некоторые аспекты ОО методологии, заслуживающие дальнейшего исследования.
Замечательное свойство пользовательского интерфейса, представленного в последнем разделе, состоит в том, что оно непосредственно выведено из реализации, - взяв внутреннее, относящееся к разработке понятие списка истории, мы транслировали его во внешнее, относящееся к пользователю понятие окна истории с привлекательным пользовательским механизмом взаимодействия.
Можно представить, что кто-то мог бы вначале придумать |
Существование такого отношения между функциональностью системы и ее реализацией противоречит всему тому, чему учит традиционная
Наивно верить, что пользователи могут предложить правильные свойства интерфейса. Иногда они могут это сделать, но, чаще всего, они будут исходить из свойств, знакомых им по прежним системам. Это понятно, у них своя работа, своя область, в которой они являются экспертами, и нельзя на них возлагать ответственность за то, что должно быть правильным в программной системе. Некоторые из худших интерактивных систем были спроектированы, находясь под слишком большим влиянием пользователей. Где действительно необходим вклад пользователей - так это их критические комментарии: они могут видеть изъяны в идее, которая могла казаться привлекательной разработчикам. Такой критицизм всегда необходим. Пользователи могут высказывать и блестящие положительные предложения тоже, но не следует быть зависимыми от них. Несмотря на критику иногда разработчикам удается склонить пользователей на свою сторону, возможно, после нескольких итераций и учета замечаний. И это происходит даже тогда, когда предложения вытекают из, казалось бы, чисто реализационных аспектов, как это было со списком истории.
Равенство традиционных отношений представляет важный вклад в объектную технологию. Рассматривая процесс разработки бесшовным и обратимым (см. лекцию 10;), мы допускаем влияние идей реализации на спецификации. Вместо одностороннего движения от анализа к проектированию и кодированию, приходим к непрерывному циклическому процессу с обратной связью. Реализация не должна рассматриваться как похлебка, низкоуровневый компонент конструирования системы. Разработанная с использованием методов, описанных в данной книге, она может и должна быть четкой, элегантной и абстрактной, ничуть не уступающей всему тому, что можно получить в презирающих реализацию традиционных формах анализа и проектирования.
Проект, описанный в этой лекции, может для типичных интерактивных систем включать достаточно много относительно небольших классов, по одному на каждую команду. Нет причин, однако, полагать, что это отражается на размере системы или ее сложности. Структура наследования классов остается простой, хотя она вовсе не должна быть плоской, - команды можно группировать по категориям.
При систематическом ОО-подходе такие вопросы возникают всякий раз, когда приходится вводить классы, представляющие действия. Хотя некоторые ОО-языки дают возможность передавать программы как аргументы в другие программы, такое свойство противоречит базисной идее Метода - функции (программы) не существуют сами по себе, - они лишь часть некоторой абстракции данных. Поэтому вместо передачи операций следует передавать объект, поставляемый вместе с операцией, например, экземпляр COMMAND, поставляемый с операцией execute.
Иногда для операций приходится писать обертывающий класс, что кажется искусственным, особенно для людей, привыкших передавать процедуры в качестве аргументов. Но мне неоднократно приходилось видеть, что класс, введенный с единственной целью COMMAND не попадает в эту категорию, он с самого начала рассматривался как абстракция данных и имел два компонента ( execute and undo ). Но, что типично, серьезная работа с командами приводит к осознанию необходимости других компонентов, таких как:
argument : ANY - для представления аргументов команды (как это было сделано в одной из версий проекта);help: STRING - для предоставления справки по каждой команде;Еще один пример взят из области численных вычислений. Рассмотрим классическую задачу вычисления f передается как аргумент в программу, вычисляющую f как функцию, при ОО-проектировании мы обнаруживаем, что "Интегрируемая функция" является важной абстракцией со многими возможными свойствами. Для пришедших из мира C, INTEGRABLE_FUNCTION - на самом деле является одной из главных абстракций проблемной области. В этом классе появятся новые полезные компоненты помимо компонента item (a: REAL): REAL, возвращающего значение функции в точке a.
То, что казалось лишь трюком, превращается в главную составляющую проекта.
Механизм undo-
На первой конференции OOPSLA в 1986 Larry Tesler говорил о механизме, основанном на той же идее, встроенном в интерактивный каркас Apple's MacApp.
В работе [Dubois 1997] объясняется в деталях, как применять ОО концепции в численных вычислениях, с приведением такой абстракции как "Integrable function".
Этот небольшой программистский проект является лучшим способом проверки понимания тем этой лекции и ОО-техники в целом.
Напишите текстовый редактор, ориентированный на работу со строками, поддерживающий следующие операции:
p: печать введенного текста;l, если это более удобно);h, если это более удобно);i: вставляет новую строку после d: удаляет строку в u: откат последней операции, если она не была Undo; если же это Undo, то выполняется повтор Можно добавить новые команды или спроектировать более привлекательный интерфейс, но во всех случаях следует создать законченную, работающую систему. (Возможно, вы сразу начнете с улучшений, описанных в следующем упражнении.)
Дополните одноуровневую схему предыдущего упражнения переопределением смысла операции отката u:
u: откат последней операции, отличной от Undo и
Добавьте операцию повтора
r: повтор последней операции, если она применима.Объясните, как применить рассмотренную технику в не ОО-языках, подобных Pascal, Ada (используя записи с вариантами) или C (используя структуры и union типы). Сравните с ОО-решениями.
С учетом проблем, поднятых в обсуждении, рассмотрите, как можно расширить механизм, разработанный в этой лекции так, чтобы он допускал поддержку Undo,
Рассмотрите аргументы команды независимо от команд и создайте только один командный объект на каждый
Если вы выполнили предыдущее упражнение, примените эту технику к решению.
В некоторых системах может быть полезным ввести понятие составной команды, выполнение которых включает выполнение нескольких других команд. Напишите соответствующий класс COMPOSITE_COMMAND, потомка COMMAND, убедитесь, что составные команды допускают откат и что компонента составной команды может быть составной командой.
Указание: используйте множественное наследование, представленное для составных фигур (см. лекцию 15 курса "Основы объектно-ориентированного программирования").
Система может включать
Указание: введите наследников UNDOABLE и NON_UNDOABLE класса COMMAND.
Напишите общецелевую библиотеку команд, предполагающую использование в произвольной интерактивной системе и поддерживающую неограниченный механизм undo-
Полезным компонентом, встраиваемым в командно-ориентированный инструментарий, является механизм истории, запоминающий выполненную команду и позволяющий пользователю повторно ее выполнить, возможно, модифицировав. Под Unix, например, доступен командный язык C-shell, запоминающий несколько последних выполненных команд. Вы можете напечатать !-2, означающее, что нужно выполнить команду, предшествующую последней. Запись ^yes^no^ означает "выполнение последней команды с заменой yes на no". Другие окружения предлагают схожие свойства.
Механизмы истории, когда они существуют, построены в соответствии с модой. Под Unix многие интерактивные средства, выполняемые под C-shell, такие как текстовый редактор Vi или различные отладчики, будут получать преимущества от такого механизма, но он не будет предлагаться другим системам. Это тем более вызывает сожаление, что те же концепции истории команд и те же ассоциированные свойства полезны любой интерактивной системе независимо от выполняемых ею функций.
Спроектируйте класс, реализующий механизм истории общецелевого назначения, так чтобы любая интерактивная система, нуждающаяся в этом механизме, могла получить его путем простого наследования класса. (Заметьте, множественное наследование здесь необходимо.)
Обсудите расширение этого механизма на общий класс USER_INTERFACE.
Тестирование компонентов ПО, например, класса требует определенных свойств при подготовке теста: ввода тестовых данных, выполнения теста, записи результатов, сравнения с ожидаемыми результатами и так далее. Определите общий, допускающий наследование класс TEST, задающий тестирующее окружение. (Обратите внимание, что и здесь важно множественное наследование.)
(Для читателей, знакомых с численными методами.) Напишите множество классов для интегрирования вещественных функций вещественной переменной на произвольном интервале. Сюда должен входить класс INTEGRABLE_FUNCTION, а также отложенный класс INTEGRATOR, описывающий метод интегрирования, и потомки класса, такие как RATIONAL_FIXED_INTEGRATOR.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.