Наследование: "откат" в интерактивных системах
Обсуждение
Образец проектирования, представленный в в этой лекции, играет важную практическую роль, позволяя при незначительных усилиях создавать значительно лучшие интерактивные системы. Он также вносит интересный теоретический вклад, освещая некоторые аспекты ОО методологии, заслуживающие дальнейшего исследования.
Роль реализации
Замечательное свойство пользовательского интерфейса, представленного в последнем разделе, состоит в том, что оно непосредственно выведено из реализации, - взяв внутреннее, относящееся к разработке понятие списка истории, мы транслировали его во внешнее, относящееся к пользователю понятие окна истории с привлекательным пользовательским механизмом взаимодействия.
Можно представить, что кто-то мог бы вначале придумать внешнее представление независимо от реализации. Но так не получилось ни в этом изложении, ни при разработке наших программных продуктов. |
Существование такого отношения между функциональностью системы и ее реализацией противоречит всему тому, чему учит традиционная методология разработки ПО. Нам говорят: выводите реализацию из спецификации, но не наоборот! Методы "итеративной разработки" и "жизненного цикла" немногое изменяют в том привычном подходе, когда реализация является рабом первичных концепций, а разработчики ПО должны делать то, что говорят их "пользователи". Здесь мы нарушаем это табу и утверждаем, что реализация может сказать нам, что следует делать системе. В прежние времена посягательство на освященные временем принципы -вокруг чего вращается мир - могло привести на костер.
Наивно верить, что пользователи могут предложить правильные свойства интерфейса. Иногда они могут это сделать, но, чаще всего, они будут исходить из свойств, знакомых им по прежним системам. Это понятно, у них своя работа, своя область, в которой они являются экспертами, и нельзя на них возлагать ответственность за то, что должно быть правильным в программной системе. Некоторые из худших интерактивных систем были спроектированы, находясь под слишком большим влиянием пользователей. Где действительно необходим вклад пользователей - так это их критические комментарии: они могут видеть изъяны в идее, которая могла казаться привлекательной разработчикам. Такой критицизм всегда необходим. Пользователи могут высказывать и блестящие положительные предложения тоже, но не следует быть зависимыми от них. Несмотря на критику иногда разработчикам удается склонить пользователей на свою сторону, возможно, после нескольких итераций и учета замечаний. И это происходит даже тогда, когда предложения вытекают из, казалось бы, чисто реализационных аспектов, как это было со списком истории.
Равенство традиционных отношений представляет важный вклад в объектную технологию. Рассматривая процесс разработки бесшовным и обратимым (см. "Процесс разработки ПО" ;), мы допускаем влияние идей реализации на спецификации. Вместо одностороннего движения от анализа к проектированию и кодированию, приходим к непрерывному циклическому процессу с обратной связью. Реализация не должна рассматриваться как похлебка, низкоуровневый компонент конструирования системы. Разработанная с использованием методов, описанных в данной книге, она может и должна быть четкой, элегантной и абстрактной, ничуть не уступающей всему тому, что можно получить в презирающих реализацию традиционных формах анализа и проектирования.
Небольшие классы
Проект, описанный в этой лекции, может для типичных интерактивных систем включать достаточно много относительно небольших классов, по одному на каждую команду. Нет причин, однако, полагать, что это отражается на размере системы или ее сложности. Структура наследования классов остается простой, хотя она вовсе не должна быть плоской, - команды можно группировать по категориям.
При систематическом ОО-подходе такие вопросы возникают всякий раз, когда приходится вводить классы, представляющие действия. Хотя некоторые ОО-языки дают возможность передавать программы как аргументы в другие программы, такое свойство противоречит базисной идее Метода - функции (программы) не существуют сами по себе, - они лишь часть некоторой абстракции данных. Поэтому вместо передачи операций следует передавать объект, поставляемый вместе с операцией, например, экземпляр COMMAND, поставляемый с операцией execute.
Иногда для операций приходится писать обертывающий класс, что кажется искусственным, особенно для людей, привыкших передавать процедуры в качестве аргументов. Но мне неоднократно приходилось видеть, что класс, введенный с единственной целью инкапсуляции операции, превращался позже в полноценную абстракцию данных с добавлением операций, о которых не шла речь в первоначальном замысле. Класс COMMAND не попадает в эту категорию, он с самого начала рассматривался как абстракция данных и имел два компонента ( execute and undo ). Но, что типично, серьезная работа с командами приводит к осознанию необходимости других компонентов, таких как:
- argument: ANY - для представления аргументов команды (как это было сделано в одной из версий проекта);
- help: STRING - для предоставления справки по каждой команде;
- компоненты, поддерживающие протоколирование и статистику вызова команд.
Еще один пример взят из области численных вычислений. Рассмотрим классическую задачу вычисления интеграла. Как правило, подынтегральная функция f передается как аргумент в программу, вычисляющую интеграл. Традиционная техника представляет f как функцию, при ОО-проектировании мы обнаруживаем, что "Интегрируемая функция" является важной абстракцией со многими возможными свойствами. Для пришедших из мира C, Fortran и нисходящего проектирования необходимость написания класса в такой ситуации кажется простым программистским трюком. Возможно, первое время он неохотно будет принимать эту технику, смиряясь с ней. Продолжая проект, он скоро осознает, что интегрируемая функция - INTEGRABLE_FUNCTION - на самом деле является одной из главных абстракций проблемной области. В этом классе появятся новые полезные компоненты помимо компонента item (a: REAL): REAL, возвращающего значение функции в точке a.
То, что казалось лишь трюком, превращается в главную составляющую проекта.
Библиографические замечания
Механизм undo-redo, описанный в этой лекции, был реализован в конструкторе структурированных документов Cepage, разработанном Жан-Марк Нерсоном и автором в 1982 [M 1982] и позже был интегрирован во многие интерактивные средства ISE (включая ArchiText [ISE 1996], преемника Cepage).
На первой конференции OOPSLA в 1986 Larry Tesler говорил о механизме, основанном на той же идее, встроенном в интерактивный каркас Apple's MacApp.
В работе [Dubois 1997] объясняется в деталях, как применять ОО концепции в численных вычислениях, с приведением такой абстракции как "Integrable function".
Упражнения
У3.1 Небольшая интерактивная система (программистский проект)
Этот небольшой программистский проект является лучшим способом проверки понимания тем этой лекции и ОО-техники в целом.
Напишите текстовый редактор, ориентированный на работу со строками, поддерживающий следующие операции:
- p: печать введенного текста;
- : передвигает курсор к следующей строке, если она есть (используйте код l, если это более удобно);
- передвигает курсор к предыдущей строке, если она есть (используйте код h, если это более удобно);
- i: вставляет новую строку после позиции курсора.
- d: удаляет строку в позиции курсора;
- u: откат последней операции, если она не была Undo; если же это Undo, то выполняется повтор redo.
Можно добавить новые команды или спроектировать более привлекательный интерфейс, но во всех случаях следует создать законченную, работающую систему. (Возможно, вы сразу начнете с улучшений, описанных в следующем упражнении.)
У3.2 Многоуровневый Redo
Дополните одноуровневую схему предыдущего упражнения переопределением смысла операции отката u:
- r: повтор последней операции, если она применима.
У3.3 Undo-redo в Pascal
Объясните, как применить рассмотренную технику в не ОО-языках, подобных Pascal, Ada (используя записи с вариантами) или C (используя структуры и union типы). Сравните с ОО-решениями.
У3.4 Undo, Skip и Redo
С учетом проблем, поднятых в обсуждении, рассмотрите, как можно расширить механизм, разработанный в этой лекции так, чтобы он допускал поддержку Undo, Skip и Redo, а также делал возможным повтор и откат, перемежаемый обычными командами. Обсудите эффект обоих новинок как на уровне интерфейса, так и реализации.
У3.5 Сохранение командных объектов
Рассмотрите аргументы команды независимо от команд и создайте только один командный объект на каждый тип команды.
Если вы выполнили предыдущее упражнение, примените эту технику к решению.
У3.6 Составные команды
В некоторых системах может быть полезным ввести понятие составной команды, выполнение которых включает выполнение нескольких других команд. Напишите соответствующий класс COMPOSITE_COMMAND, потомка COMMAND, убедитесь, что составные команды допускают откат и что компонента составной команды может быть составной командой.
Указание: используйте множественное наследование, представленное для составных фигур (см. лекцию 15 курса "Основы объектно-ориентированного программирования").
У3.7 Необратимые команды
Система может включать необратимые команды либо по самой их природе ("Запуск ракет"), либо по прагматичным причинам больших расходов, связанных с отменой действия команды. Усовершенствуйте решение так, чтобы оно учитывало возможность присутствия необратимых команд. Внимательно изучите алгоритмы и интерфейс пользователя, в частности используйте окно истории.
Указание: введите наследников UNDOABLE и NON_UNDOABLE класса COMMAND.
У3.8 Библиотека команд (проектирование и реализация)
Напишите общецелевую библиотеку команд, предполагающую использование в произвольной интерактивной системе и поддерживающую неограниченный механизм undo-redo. Библиотека должна интегрировать свойства, обсуждаемые в последних трех упражнениях: отделение команд от их аргументов, составные команды, необратимые команды. Возможно также встраивание свойства "Undo, Skip и Redo". Проиллюстрируйте применимость библиотеки, построив на ее основе три демонстрационные системы различной природы, такие как текстовый редактор, графическая система, инструмент тестирования.
У3.9 Механизм истории
Полезным компонентом, встраиваемым в командно-ориентированный инструментарий, является механизм истории, запоминающий выполненную команду и позволяющий пользователю повторно ее выполнить, возможно, модифицировав. Под Unix, например, доступен командный язык C-shell, запоминающий несколько последних выполненных команд. Вы можете напечатать !-2, означающее, что нужно выполнить команду, предшествующую последней. Запись ^yes^no^ означает "выполнение последней команды с заменой yes на no". Другие окружения предлагают схожие свойства.
Механизмы истории, когда они существуют, построены в соответствии с модой. Под Unix многие интерактивные средства, выполняемые под C-shell, такие как текстовый редактор Vi или различные отладчики, будут получать преимущества от такого механизма, но он не будет предлагаться другим системам. Это тем более вызывает сожаление, что те же концепции истории команд и те же ассоциированные свойства полезны любой интерактивной системе независимо от выполняемых ею функций.
Спроектируйте класс, реализующий механизм истории общецелевого назначения, так чтобы любая интерактивная система, нуждающаяся в этом механизме, могла получить его путем простого наследования класса. (Заметьте, множественное наследование здесь необходимо.)
Обсудите расширение этого механизма на общий класс USER_INTERFACE.
У3.10 Тестирование окружения
Тестирование компонентов ПО, например, класса требует определенных свойств при подготовке теста: ввода тестовых данных, выполнения теста, записи результатов, сравнения с ожидаемыми результатами и так далее. Определите общий, допускающий наследование класс TEST, задающий тестирующее окружение. (Обратите внимание, что и здесь важно множественное наследование.)
У3.11 Интегрируемые функции
(Для читателей, знакомых с численными методами.) Напишите множество классов для интегрирования вещественных функций вещественной переменной на произвольном интервале. Сюда должен входить класс INTEGRABLE_FUNCTION, а также отложенный класс INTEGRATOR, описывающий метод интегрирования, и потомки класса, такие как RATIONAL_FIXED_INTEGRATOR.