Предварительный проект / декомпозиция
Поскольку Вы имеете некоторое понятие о том, что должна выполнять Ваша программа, пришла пора начинать проектирование. Первый этап - предварительный проект - сосредоточивается на расчленении задачи на обозримые составляющие.
В этой главе мы обсудим два пути декомпозиции Вашей программы на Форте.
ДЕКОМПОЗИЦИЯ ПО КОМПОНЕНТАМ
С Вами когда-нибудь такое случалось? Вы три месяца планировали отправиться на выходные в поход в горы. Вы сочиняли списки того, что нужно взять с собой и грезили о косогорах.
В то же время Вы решали, что надеть на свадьбу Вашей сестры в следующую субботу. У них будет неформальный стиль, и Вы не хотите выбиваться. Все же свадьба есть свадьба. Может быть, Вам все же стоит взять напрокат смокинг.
Несмотря на все эти планы, лишь только в четверг Вы осознали, что эти два события совпадают. В таких случаях хочется как следует выругаться.
Как такой мысленный ляпсус мог случиться с таким умным человеком, как Вы? Кажется, человеческий мозг в действительности устанавливает связи между воспоминаниями. Новые идеи как-то накладываются на существующие пути родственных мыслей.
В описанном только что несчастном случае не было сделано никакого соединения между двумя отдельно-связанными областями мысли до четверга. Конфликт, по-видимому, возник, когда некоторое новое входное воздействие (что-нибудь тривиальное типа услышанного прогноза погоды на субботу) оказалось связанным одновременно с обеими областями мысли. Молниеностная вспышка осознания прошла между областями, безжалостно преследуемая громоподобной паникой.
Был изобретен простой инструмент для избежания подобных оказий. Он называется календарем. Если бы Вам было нужно записать оба плана на одном его листке, Вы бы увидели отметку о другом плане, то есть то, что Ваш мозг со всем своим запутанным великолепием сделать не смог.
------------------------------------------------------------ СОВЕТ Чтобы увидеть связь между двумя вещами, поставьте их рядом вместе. Чтобы напоминать себе об этой связи, `держите` их рядом вместе. ------------------------------------------------------------
Этот чип имеет "управляющий регистр" и "регистр данных". В плохо спроектированной задаче куски кода по всей программе будут обращаться к коммуникационной микросхеме простым выполнением инструкции OUT для засылки соответствующего байта в командный регистр. Это делает задачу в целом бессмысленно зависящей от определенной микросхемы - что очень рискованно.
Вместо этого программисты на Форте написали бы компонент для управления чипом ввода/вывода. Эти команды имели бы логические имена и удобный интерфейс (обычно стек Форта) для обеспечения их использования остальной частью задачи.
На любой итерации проектирования Вашего продукта Вы бы реализовывали только те команды, которые нужны Вам в дальнейшем - но не все возможные коды, которые можно посылать в "управляющий регистр". Если позже в проектном цикле вы бы обнаружили необходимость дополнительной команды, скажем той, что изменяет скорость передачи, то такая команда была бы добавлена к лексикону чипа ввода/вывода, а не в код, потребный для установки скорости. И нет никакой платы за внесение изменения, если не считать нескольких минут (самое большее) на редактирование и перекомпилирование.
------------------------------------------------------------ СОВЕТ Внутри каждого компонента реализуйте лишь те команды, которые необходимы на данной итерации. (Но не устраняйте возможности для дальнейших добавлений.) ------------------------------------------------------------
Что происходит внутри компонента - это совершенно его дело. Не обязательно будет плохим стилем, если определения внутри компонента будут разделять избыточную информацию.
К примеру, запись в определенной структуре данных имеет длину в четырнадцать байтов. Одно из определений в компоненте продвигает указатель на 14 байтов для установки на следующую запись; другое определение уменьшает указатель на 14 байтов.
Пока это число 14 остается "секретом" компонента и не может быть использовано еще где-либо, Вам не нужно и определять его как константу.
Используйте лишь число 14 в обоих определениях:
: +ЗАПИСЬ 14 ЗАПИСЬ# +! ; : -ЗАПИСЬ -14 ЗАПИСЬ# +! ;
С другой стороны, если это число требуется вне компонента, или если оно используется внутри компонента много раз, то весьма вероятно, что оно будет изменено. Вам следовало бы спрятать его под именем:
14 CONSTANT /ЗАПИСЬ : +ЗАПИСЬ /ЗАПИСЬ ЗАПИСЬ# +! ; : -ЗАПИСЬ /ЗАПИСЬ NEGATE ЗАПИСЬ# +! ;
(Имя /ЗАПИСЬ по соглашению означает "количество байтов на запись".)
ПРИМЕР: КРОШЕЧНЫЙ РЕДАКТОР
Давайте примем разбиение на компоненты за основную цель. Было бы неплохо спроектировать большую задачу прямо в третьей главе, но, увы, нет места, и мы, конечно, вынуждены будем отложить попытку полностью решить такую задачу.
Вместо этого мы возьмем часть большой задачи, которая уже расчленена. Мы будем проектировать компонент путем дальнейшего его разбиения на компонентики.
Представим себе, что мы должны создать свой крошечный редактор, который позволит пользователям менять контекст полей ввода на экране терминала. К примеру, экран может выглядеть таким образом:
Имя участника `Вера Павловна`
Редактор обеспечит для пользователя три режима смены контекста поля ввода:
`Замещение`. Печать обычных символов замещает прежние символы.
`Удаление`. Нажатие комбинации клавиш "CTRL D" удаляет символ, отмеченный курсором и перемещает остальные символы влево.
`Вставка`. Используя комбинацию клавиш "CTRL I" переводим редактор в режим "вставки", где последовательно нажимаемые обычные символы устанавливаются в позицию, отмеченную курсором, сдвигая остальные символы вправо.
Частью концептуальной модели должна также являться обработка ошибок и исключительных ситуаций; например: каковы размеры поля? что происходит в режиме вставки, когда символы пересекут правую границу? и т.д.
Вот и все данное нам описание. Остальное зависит от нас. Давайте попытаемся определить, какие компоненты нам понадобятся. Во-первых, редактор должен реагировать на нажимаемые клавиши.
Поэтому нам понадобится интерпретатор нажатий на клавиши - некая программа, которая при нажатиях ищет соответствие клавишам в списке возможных операций. Такой интерпретатор являет собой один компонент и его лексикон состоит из единственного слова. Поскольку такое слово должно позволять редактирование поля, давайте назовем его РЕДАКТОР.
Операции, вызываемые интерпретатором клавиш, составят второй лексикон. Определения этого лексикона будут выполнять различные требуемые функции. Одно из них может быть названо "СТИРАТЬ", другое "ВСТАВИТЬ" и т.д. Поскольку все эти команды будут вызываться интерпретатором, каждая из них должна обрабатывать одно нажатие клавиши.
Под этими командами должен находиться третий компонент - набор слов, которые реализуют редактируемую структуру данных.
Рис.3-2. Обобщенная декомпозиция задачи создания Крошечного Редактора.
+----------+ | РЕДАКТОР | ++--+--+-+-+ Интерпретатор ____/ | \ \______ нажатий на клавиши +-----/--------|----\-------\-----+ | +--/-+ +----|+ +-\---+ +-\--+ | | | | | | | | | | | Функции | +--+-+ ++---++ +--+--+ +--+-+ | редактирования +-----\---/-----\-----|------/----+ \ / \ | / +-------\---------\---|----/------+ | +---+/ \-----+ \ | / | | | / |\ | +-\-|--/-----+ | Текстовый буфер, | | | | | | | | структуры данных | | | | | +------------+ | и команды | +---+ +-----+ | +---------------------------------+
Наконец, нам будет нужен компонент для демонстрации поля на видеоэкране. Во имя простоты давайте запланируем создание всего одного слова ПОКАЗАТЬ для смены изображения всего поля после каждого нажатия на клавишу.
: РЕДАКТОР BEGIN KEY ПРОВЕРИТЬ ПОКАЗАТЬ ... UNTIL ;
Этот подход отделяет проверку буфера от регенерации дисплея. Отныне мы сконцентрируемся на проверке буфера.
Давайте рассмотрим каждый компонент по отдельности и попытаемся определить каждое слово, которое нам понадобится. Мы можем начать с рассмотрения событий, которые должны происходить внутри каждой из наиболее важных функций редактора: замещения, стирания и вставки.
Мы можем нацарапать нечто вроде нижеследующего на обратной стороне старого ресторанного меню (сейчас не будем обращать особого внимания на обработку исключительных случаев):
`Для Замещения`: ФУНХЦИОНАЛЬНОСТЬ Записывать новый символ в байт, ^ на котором стоит указатель. ФУНКЦИОНАЛЬНОСТЬ Продвинуть указатель (если он ^ не в конце поля). ФУНКЦИОНАЛЬНОСТЬ ^
`Для Стирания`: ФУНКЦИОНСАЛЬНОСТЬ Скопировать на одну позицию ^~~~~~~~~ влево строку, начинающуюся ФУНКЦИОНАЛЬНОСТЬЬ справа от указателя. ~~~~~~~~~ Записать "пробел" в последнюю ФУНКЦИОНАЛЬНОСТЬ позицию в строке. ^ ~
`Для Вставки`: ФУКЦИОНАЛЬНОСТЬ Скопировать вправо на одну ^~~~~~~~~~~~~ позицию строку, начинающуюся ФУККЦИОНАЛЬНОСТЬ от указателя. ^~~~~~~~~~~~~~ Записать новый символ в байт, на ФУНКЦИОНАЛЬНОСТЬ котором установлен указатель. ^ Продвинуть указатель (если не ФУНКЦИОНАЛЬНОСТЬ конец поля). ^
Мы только что "на одной ноге" разработали алгоритмы для задачи.
Наш следующий шаг состоит в исследовании этих глвных процедур для поиска полезных "имен" - процедур или элементов, которые могут быть:
1. возможно, использованными вторично, либо 2. возможно, измененными
Мы поняли, что все три процедуры используют нечто, называемое "указателем". Нам нужно две процедуры:
1. для получения значения указателя (если его отсчет относителен, такая функция будет произволить некоторые расчеты). 2. для продвижения указателя.
Постойте, три процедуры:
3. для перемещения указателя назад.
поскольку мы захотим, чтобы "клавиши управления курсором" перемещали его вперед и назад без редактирования.
Все три эти оператора будут ссылаться на физический указатель где-то в памяти. Как и где он будет храниться (относительно или абсолютно) должно быть спрятано внутри компонента.
Давайте сделаем попытку переписать эти алгоритмы в коде:
: КЛАВИША# ( дает код последней нажатой клавиши) ... ; : ПОЗИЦИЯ ( дает адрес символа по указателю) ... ; : ВПЕРЕД ( продвигает указатель, остановка в конце) ... ; : НАЗАД ( уменьшает указатель, остановка в начале) ... ; : ЗАМЕСТИТЬ КЛАВИША# ПОЗИЦИЯ C! ВПЕРЕД ; : ВСТАВИТЬ СМЕСТИТЬ> ЗАМЕСТИТЬ ; : СТЕРЕТЬ СМЕСТИТЬ< ОЧИСТИТЬ-КОНЕЦ ;
Для копирования текста налево и направо нам пришлось по мере написания придумать два новых имени - СМЕСТИТЬ< и СМЕСТИТЬ>
(произносится "сместить-назад" и "сместить-вперед" соответственно). Оба они, конечно, будут использовать слово ПОЗИЦИЯ, а также должны опираться на элемент, который мы предварительно определили как "знающий" длину поля. Мы можем приняться за это, когда доберемся до написания третьего компонента.
Но посмотрите, что мы уже обнаружили: можно описать "Вставку" как просто "СМЕСТИТЬ> ЗАМЕСТИТЬ".
Другими словами, "Вставка" в действительности `использует` "Замещение" несмотря на то, что они кажутся существующими на одинаковом уровне (по крайней мере, с точки зрения Структурированного Программиста).
Вместо углубления в третий компонент, давайте изложим наши знания о первом компоненте, интерпретаторе клавиш. Во-первых, мы должны разрешить проблему "режима вставки". При этом выясняется, что "вставка" - это не просто нечто, случающееся когда Вы нажимаете на определенную клавишу, как в режиме стирания. Это `другой способ интерпретации` некоторых из возможных нажатий на клавиши.
К примеру, в режиме "замещения" обычный символ записывается в текущую позицию курсора; но в режиме "вставки" остальная часть строки должна быть сдвинута вправо. И клавиша забоя также работает по-другому, когда редактор находится в режиме вставки.
Поскольку имеются два режима, "вставки" и "не-вставки", интерпретатор клавиш должен присваивать клавишам два возможных набора именованных процедур.
Мы можем записать наш интерпретатор нажатий на клавиши в виде таблицы решений (позаботясь о реализации позднее):
`Клавиша` `Не-вставка` `Вставка` Ctrl-D СТЕРЕТЬ ВЫКЛ-ВСТАВКУ Ctrl-I ВКЛ-ВСТАВКУ ВЫКЛ-ВСТАВКУ забой НАЗАД НАЗАД< стрелка-влево НАЗАД ВЫКЛ-ВСТАВКУ стрелка-вправо ВПЕРЕД ВЫКЛ-ВСТАВКУ ввод ВЫХОД ВЫКЛ-ВСТАВКУ любой видимый символ ЗАМЕСТИТЬ ВСТАВИТЬ
Мы поместили возможные типы клавиш в левой колонке, то, что они делают в нормальном режиме - в средней колонке, а для режима "вставки" - в правой колонке.
Для реализации случая нажатия "забоя" в режиме вставки мы добавили новую процедуру:
: НАЗАД< НАЗАД СМЕСТИТЬ< ;
(передвинуть курсор назад к последнему введенному символу, затем сдвинуть все справа налево, перекрывая ошибку).
Эта таблица кажется наиболее логичным изображением задачи на текущем уровне. Мы оставим реализацию для будущего рассмотрения (глава 8).
Теперь продемонстрируем огромную ценность подобного подхода с точки зрения управляемости. Мы подбросим себе задачу - существенное изменение в планах.
ПОДДЕРЖКА ЗАДАЧИ, ОСНОВАННОЙ НА КОМПОНЕНТАХ
Насколько хорошо наш проект поведет себя перед лицом изменений? Вообразим следующий сценарий:
Вы изначально согласились, что можем обновлять видеодисплей простым переписыванием всего поля всякий раз после нажатия на клавишу. Мы даже реализовали такой код на нашем персональном компьютере с его видеопамятью, входящей в основное адресное пространство; код, обновляющий всю строку за время мерцания развертки экрана. Но теперь заказчик хочет, чтобы задача работала в сети на основе телефонных линий, в которой весь ввод/вывод производится весьма неторопливо. Поскольку неоторые из наших полей ввода занимают почти всю ширину экрана, например, 65 символов, было бы слишком долго обновлять всю строку после каждого нажатия на клавишу.
Нам придется изменить задачу так, чтобы обновлять только ту часть поля, которая действительно меняется. При "вставке" и "стирании" это означало бы текст справа от курсора. При "замещении" это означало бы замену только одного символа.
Такое изменение существенно. Функция регенерации изображения, которую мы по-рыцарски отделили от интерпретатора клавиш, ныне зависит от текущей функции редактирования. Как мы обнаружили, для реализации интерпретатора наиболее важны имена:
ВПЕРЕД НАЗАД ЗАМЕСТИТЬ ВСТАВИТЬ СТЕРЕТЬ НАЗАД<
Ни одно из определений не делает ссылок к процессу обновления изображения, поскольку это изначально предполагалось делать позже.
Но не все так плохо, как кажется. При внимательном взгляде процесс ЗАМЕСТИТЬ мог бы легко включать в себя команду для печати нового символа в позиции курсора. А СМЕСТИТЬ< и СМЕСТИТЬ> могли бы иметь команды для распечатки всего текста справа от этой позиции (включая ее саму), а затем возврата курсора дисплея в его текущее положение.
Вот наши пересмотренные определения:
: ЗАМЕСТИТЬ КЛАВИША# ПОЗИЦИЯ C! КЛАВИША# EMIT ВПЕРЕД ; : РАСПЕЧАТАТЬ ( напечатать от текущей позиции до конца поля и вернуть курсор) ... ; : ВСТАВИТЬ СМЕСТИТЬ> РАСПЕЧАТАТЬ ЗАМЕСТИТЬ ; : СТЕРЕТЬ СМЕСТИТЬ< ОЧИСТИТЬ-КОНЕЦ РАСПЕЧАТАТЬ ;
Поскольку имеются всего три функции, изменяющие память, нужны всего три функции для обновления экрана. Такая идея небесспорна. Мы должны быть способны отстаивать такие утверждения для обеспечения корректности программы.
Заметьте, что дополнительная проблема регенерации изображения принуждает ввести дополнительный "указатель": текущего положения курсора на экране. Однако компонентная декомпозиция вдохновила нас рассматривать процесс ЗАМЕСТИТЬ как изменяющий одновременно поле данных и его видеоизображение; то же самое со СМЕСТИТЬ< и СМЕСТИТЬ>. По этой причине кажется естественным сохранить лишь один реальный указатель - относительного типа - из которого мы можем вычислить либо адрес данных в памяти, либо номер колонки на экране.
Поскольку природа указателя полностью спрятана внутри трех процессов: ПОЗИЦИЯ, ВПЕРЕД и НАЗАД, то мы можем немедленно применить такой подход, несмотря на то, что вначале наш подход был другим.
Такое изменение может показаться слишком простым - даже очевидным. Если это так, то потому, что технология обеспечивает гибкую разработку. Если бы мы использовали традиционный подход - делали проектирование в соответствии со структурой или в соответствии с последовательным процессом преобразования данных - наш хрупкий проект был бы вдребезги разбит такими переменами.
Для доказательства такого утверждения нам придется начать все опять заново.
ПРОЕКТИРОВАНИЕ И ПОДДЕРЖКА ЗАДАЧИ ПРИ ТРАДИЦИОННОМ ПОДХОДЕ
Давайте сделаем вид, что мы пока что не изучали проблему создания Крошечного Редактора и имеем вновь минимальное его описание. Мы также начнем с нашего первого допущения того, что можно обновлять изображение перебивкой всего поля после каждого нажатия на клавишу.
В соответствии с нисходящим методом проектирования давайте окинем проблему возможно более широким взглядом. На рисунке 3-3 наша программа изображена в своих простейших терминах. Здесь мы видим, что редактор на самом деле представляет собой цикл, который продолжает получение нажатий на клавиши и выполнение некоторых функций редактирования до тех пор, пока пользователь не надавит клавишу "ввод".
Рис.3-3. Традиционный подход: взгляд с вершины.
| X---------------------- | \/ | +------------------+ | | ПОЛУЧИТЬ НАЖАТИЕ | | | КЛАВИШИ | | +-----+------+-----+ | / \ | / +----\-------+ | клавиша "ввод" / | ОБРАБОТАТЬ | |________________/ | КЛАВИШУ | | +------+-----+ | | | +-------|--------+ | | ОБНОВИТЬ ЭКРАН | | +-------+--------+ | | O
Внутри цикла у нас имеется три модуля: получения символа с клавиатуры, редактирования данных и, наконец, обновления дисплея на предмет соответствия этим данным.
Ясно, что большая часть работы будет происходить внутри "обработки клавиши".
Применение метода последовательной детализации дает показанную на рисунке 3-4 расшифровку задачи "обработка клавиши". Мы обнаружили, что для достижения такой конфигурации потребовалось несколько попыток. Проектирование на этом уровне вынуждает нас учитывать одновременно множество тех вещей, которые мы оставляли на будущее в предыдущей попытке.
Рис.3-4. Структура задачи "Обработка Клавиши".
| ОЧИСТИТЬ ФЛАГ ВЫХОДА | ОЧИСТИТЬ ФЛАГ ВСТАВКИ | |______ пока флаг выхода = ложь | ПОЛУЧИТЬ НАЖАТИЕ КЛАВИШИ | |если клавиша: +-------+-------+--------++--------+---------+---------+ стрелка стрелка любой Ctrl-D Ctrl-I забой ввод влево вправо видимый | | | | | | символ | встав- встав- | | | | | ка? ка? | | | |вставка? | | | | | | / \ | / \ / \ | | | нет \ да | нет \да нет \да | | | / \ | | \ | \ | УМЕНЬШ.
УВЕЛИЧ. ЗАМЕ- ВСТАВ- СТИРА- УСТ. СБР. УМ. УМ. УСТ. УКАЗАТ. УКАЗАТ. ЩЕНИЕ КА НИЕ ФЛАГ ФЛАГ УКАЗ. УКАЗ. ФЛАГ | | \ / | ВСТ. ВСТ. \ СТИ- ВЫХ. | | \ / | \ / \ РАНИЕ | | | \ / | \ / \ / | +-------+--------+-------++----------+----------+-------+ | ОБНОВИТЬ ИЗОБРАЖЕНИЕ |
К примеру, мы должны учитывать все клавиши, которые могут быть нажаты. Что более существенно, нам приходиться принимать во внимание проблему "режима вставки". Такая реализация вынуждает нас вводить ФЛАГ ВСТАВКИ, который изменяется при нажатии на "Ctrl-I". Он используется внутри нескольких линий структуры для определения того, как обрабатывать ту или иную клавишу.
Другой флаг, названный ФЛАГ ВЫХОДА, вроде бы дает хорошую возможность обеспечить структурированный выход из цикла редактирования, если пользователь нажимает клавишу ввода.
К моменту окончания диаграммы нас проверки на режим вставки замучили. Нельзя ли было бы проверять этот режим один раз, в самом начале? Мы делаем в соответствии с этим другой чертеж (рисунок 3-5).
Рис.3-5. Другая структура для "Обработки Клавиши" (*).
Как видно, он оказывается даже еще более ужасным, чем первый. Теперь мы делаем проверку на каждую клавишу по два раза. Хотя, конечно, интересно как, будучи функционально эквивалентными, две структуры оказываются совершенно различными. Одного этого достаточно, чтобы усомниться в том, действительно ли структуры управления так уж сильно связаны с задачей.
Остановясь на первом рисунке, мы в конце концов пришли к наиболее важным модулям - тем, которые делают собственно замещение, вставку и стирание. Еще раз взгляните на нашу расшифровку "Обработки Клавиши" на рисунке 3-4. Давайте остановимся на одной из семи возможных линий процесса выполнения, той, которая возникает при получении видимого символа.
На рисунке 3-6(а) виден исходный структурный путь для видимого символа.
(*) - рисунок не приведен вследствие чрезмерной сложности и обилия мелких деталей. Он аналогичен рис.3-4, но вдвое шире и содержит вдвое больше вертикальных колонок.
Рис.3-6. Одна и та же часть, "детализированная" и "оптимизированная".
_______________________________________________________________ | | а)Исходный проект |б)Расшифрованный проект | в)"оптимизация" | | __любой видимый___ | ___любой видимый_____ | _любой видимый_ символ | символ | символ | | | | | |вставка? | |вставка? | |вставка? | | | | | / \ | / \ | / \ нет да | нет да | нет да / \ | / \ | / \ ЗАМЕЩЕ- ВСТАВКА | | СМЕСТИТЬ | | СМЕСТИТЬ НИЕ | | | ВПРАВО | | ВПРАВО | | | | | | \ / | | | | | | \/ | | | ЗАПИСЬ ЗАПИСЬ | ЗАПИСЬ | | | СИМВОЛА СИМВОЛА | СИМВОЛА | | | В ПОЗ. В ПОЗ. | В ПОЗ. | | | | | | | \ / | УВЕЛИЧ. УВЕЛИЧ. | УВЕЛИЧ. \ / | УКАЗАТ. УКАЗАТ. | УКАЗАТ. \ / | \ / | | \ / | \ / | | | | | | |
Поскольку мы выделили алгоритмы для замещения и вставки символов, то должны детализировать картину, как показано на рисунке 3-6(б). Но посмотрите на возмутительную избыточность кода (обведено кружочками). Большинство знающих программистов поняли бы ненужность такой избыточности и изменили бы структуру так, как показано на рисунке 3-6(в).
Не так уж плохо, не правда ли?
ИЗМЕНЕНИЕ В ПЛАНАХ.
О'кей, приготовились, и вот - грянули изменения. Нам объявили, что эта задача не будет теперь использоваться на дисплее с прямо доступной видеопамятью. Что сделает это изменение со структурой нашего проекта?
Ну, для начала оно разрушает "Обновление Изображения" как независимый модуль. Функция "обновления изображения" ныне распределена между различными структурными линиями внутри "Обработки Клавиши". Структура нашей задачи в целом изменилась. Легко увидеть, как мы могли бы неделями производить нисходящее проектирование только ради того, чтобы обнаружить, что спускались не по тому дереву.
Что происходит, когда мы пытаемся изменить программу? Давайте еще раз взглянем на путь прохождения любого видимого символа.
На рисунке 3-7(а) показано, что происходит с нашим первым проектом, когда добавляется регенерация изображения.
Часть (б) показывает наш "оптимизированный" проект с развернутыми модулями обновления. Заметьте, что мы проверяем теперь флаг вставки дважды внутри этого единственного ответвления внешнего цикла.
Рис.3-7. Добавление регенерации изображения.
___любой видимый___ | ___любой видимый___ символ | символ | | | вставка? | вставка? / \ | / \ нет да | нет да / \ | \ / ЗАМЕЩЕНИЕ ВСТАВКА | \ СМЕСТИТЬ ВПРАВО | | | \ / | | | \/ | | | ЗАПИСАТЬ СИМВОЛ | | | В ПОЗИЦИЮ | | | | | | | УВЕЛИЧИТЬ | | | УКАЗАТЕЛЬ | | | | | | | вставка? | | | / \ | | | нет да | | | / \ ОБНОВИТЬ ОБНОВИТЬ | ПЕЧАТЬ ПЕЧАТЬ ОТ ОДИН ОСТАТОК | СИМВОЛА КУРСОРА ДО СИМВОЛ СТРОКИ | \ КОНЦА ПОЛЯ \ / | \ / \ / | \ ВЕРНУТЬ \ / | \ КУРСОР \ / | \ / \ / | \ / | | | а) б)
Но, что еще хуже, в нашем проекте есть ошибка. Вы можете ее найти?
В обоих случаях, при замещении и вставке, указатель продвигается `до` регенерации. В случае замещения мы показываем новый символ в неправильном месте. В случае вставки мы перебиваем остаток строки без нового символа.
Допустим, такую ошибку легко отследить. Нам нужно только переместить модули регенерации вверх до "увеличения указателя". Дело в другом: как мы ее пропустили? А просто мы были заняты потоком управления - поверхностным элементом проектирования программ.
Наоборот, в нашем по-компонентном проекте правильное решение вытекает естественным образом, поскольку мы "использовали" компонент для регенерации внутри редактирующего компонента. Мы также использовали ЗАМЕЩЕНИЕ внутри слова ВСТАВКА.
Разбивая нашу задачу на компоненты, использующие друг друга, мы достигли не только `элегантности`, но и более прямого пути к `корректности`.
ИНТЕРФЕЙСНЫЙ КОМПОНЕНТ
В терминах компьютерной науки взаимодействие между модулями имеет два аспекта. Во-первых, есть способ, по которому другие модули `вызывают` данный; это - управляющий интерфейс. Во-вторых, есть способ, по которому модули передают и получают данные; это - интерфейс данных.
Благодаря словарной структуре Форта организация управления не представляет трудностей.
Определения вызываются просто по именам. Поэтому мы в этом разделе будем использовать слово "интерфейс", имея в виду интерфейс данных.
Когда дело доходит до итерфейсов данных между модулями, традиционная мудрость говорит только о том, что "интерфейсы должны быть тщательно продуманы и минимально сложны". Причина для такой тщательности, конечно, состоит в том, что каждый из модулей должен держать свой конец такого интерфейса (рисунок 3-8).
Рис.3-8. Традиционный взгляд на интерфейс как на соединение.
МОДУЛЬ 1 МОДУЛЬ 2 +-----------------+ +------------------+ | БУФЕР А {| |} БУФЕР А | | | | | | ВЕЩЬ Б [| |] ВЕЩЬ Б | | | | | |СИНХРОНИЗАЦИЯ В =| |= СИНХРОНИЗАЦИЯ В | +-----------------+\+------------------+ \ \ ИНТЕРФЕЙС ~~~~~~~~~~
Это предопределяет наличие избыточного кода. Как мы видели, избыточность рождает, по крайней мере, две проблемы: неуклюжий код и плохую управляемость. Изменение интерфейса в одном модуле будет сказываться на другом модуле.
Имеется лучший способ обеспечить интерфейс, нежели приведенный. Позвольте мне предложить проектный элемент, который я называю "интерфейсным компонентом". Целью введения такого компонента является реализация и `упрятывание информации` об интерфейсе данных между двумя (или более) компонентами (рисунок 3-9).
Рис.3-9. Использование интерфейсного компонента.
+----------+ +----------+ | МОДУЛЬ 1 | | МОДУЛЬ 2 | +-----+----+ +----+-----+ \ / +-----------\------------------/---------------+ | +---------+ +---------+ | | | БУФЕР А | | ВЕЩЬ Б | ИНТЕРФЕЙСНЫЙ | | +---------+ +---------+ КОМПОНЕНТ | | | | СИНХРОНИЗАЦИЯ В | | ~~~~~~~~~~~~~~~ | +----------------------------------------------+
------------------------------------------------------------ СОВЕТ Как структуры данных, так и команды, принимающие участие в коммуникациях между модулями, должны быть выделены в интерфейсный компонент. ------------------------------------------------------------
Позвольте привести пример из моего недавнего опыта.
Одним из моих хобби является создание форматтеров текста/редакторов. (Я их разработал два, включая тот, на котором пишу эту книгу.)
В моей последней разработке часть форматтера имеет два компонента. Первый считывает исходный документ и решает, где сделать перевод строки, где - разделение между страницами и т.д. Но, вместо посылки строки на принтер или терминал, он сохраняет ее на время в "строчном буфере".
Аналогично, вместо посылки команд управления принтером - для включения курсива, подчеркивания и т.п. - при форматировании, он предопределяет эти команды до тех пор, пока текст действительно не будет выдан. Для такого предопределения я завел другой буфер, названный "буфером атрибутов". Он соотносится, байт к байту, с буфером строки, и в каждом из его байтов содержится набор флагов, показывающих, что соответсвующий символ должен быть подчеркнут, сделан курсивом или как-нибудь еще.
Второй компонент показывает или печатает содержимое буфера строки. Компонент знает, выдается ли строка на терминал или на принтер и дает текст в соответствии с признаками, указанными в буфере атрибутов.
Мы имеем здесь два хорошо определенных компонента - формирователь строки и компонент вывода, каждый из которых поддерживает часть функций форматтера в целом.
Интерфейс данных между этими двумя компонентами чрезвычайно сложен. Он состоит из двух буферов, переменной, показывающей текущее число символов в них и, наконец, - "знаний" о том, что означает вся эта система атрибутов.
На Форте я определил эти элементы все вместе на единственном экране. Буферы определены с помощью CREATE, счетчик - обычная переменная VARIABLE, а атрибуты заданы в виде констант (CONSTANT), как, например:
1 CONSTANT ПОДЧЕРКИВАНИЕ ( маска битов для подчеркивания) 2 CONSTANT КУРСИВ ( маска битов для курсива)
Форматирующий компонент использует фразы типа ПОДЧЕРКИВАНИЕ УСТАНОВИТЬ для установки битов в буфере атрибутов. Компонент вывода использует фразы типа ПОДЧЕРКИВАНИЕ AND для анализа буфера атрибутов.
ОШИБКА В ПРОЕКТЕ.
При проектировании интерфейсного компонента следует спросить себя: "каков набор структур и команд, которые должны использоваться совместно сообщающимися компонентами?" Важно определить, какие элементы принадлежат интерфейсу, а какие должны оставаться внутри одного из компонентов.
При написании своего текстового форматтера я не смог полностью ответить на этот вопрос и сделал ошибку. Проблема была в следующем:
Я допустил возможность использования шрифтов различной ширины: уплотненных, с двойной шириной и т.д. Это означает не только посылку различных сигналов в принтер, но также изменение числа символов, допустимых для одной строки.
У меня в форматтере имеется переменная под именем СТЕНА. СТЕНА показывает правую границу: точку, после которой нельзя располагать текст. Применение различных величин ширины означает пропорциональное изменение содержимого переменной СТЕНА. (В действительности это уже само по себе оказывается ошибкой. Мне следовало бы использовать более качественную единицу измерения, величина которой оставалась бы постоянной для строки. Изменение ширины печати означало бы изменение количества таких единиц на один символ. Но подручными средствами исправлять ошибку ...)
Увы, я использовал переменную СТЕНА также внутри компонента вывода для подсчета количества выводимых символов. Я расчитывал, что эта величина будет меняться в зависимости от того, какую ширину печати я использую.
И я был прав - 99% времени. Но однажды я обнаружил, что при определенных условиях строка из уплотненного текста как-то урезывалась. Последние несколько слов отсутствовали. Причиной оказалось то, что СТЕНА изменялась до того, как у компонента вывода появлялась возможность ее использовать.
В начале я не видел ничего плохого в том, чтобы позволить этому компоненту запросто использовать переменную СТЕНА из форматирующего компонента. Теперь я осознал, что форматтер должен был оставлять другую переменную для компонента вывода для указания последнему числа подготовленных символов в буферах.
Это не дало бы возможности никаким последующим командам изменения ширины изменить содержимое переменной СТЕНА.
Важно было, чтобы два буфера, команды атрибутов и новая переменная были `единственными` элементами, которые могли совместно использоваться обоими модулями. Доступ внутрь модуля из другого модуля может накликать беду.
Мораль этой истории состоит в том, что необходимо делать различие между структурами данных, которые правильно используются внутри единственного компонента и теми, которые могут быть совместно использованы более чем одним компонентом.
Родственное замечание:
------------------------------------------------------------ СОВЕТ Выражайте в реальных единицах любые данные, которые разделяются компонентами. ------------------------------------------------------------
Для примера:
Модуль А измеряет температуру в печи. Модуль Б управляет горелкой. Модуль В контролирует, что дверца закрыта, если печь достаточно горяча.
Информация, интересная всем - это температура печи, выраженная непосредственно в градусах. Хотя модуль А может получать величину, представляющую собой напряжение от термодатчика, он должен преобразовать ее в градусы перед выдачей результата остальной задаче.
РАЗБИЕНИЕ ПО ПОСЛЕДОВАТЕЛЬНЫМ УРОВНЯМ СЛОЖНОСТИ
Мы обсуждали один путь декомпозиции: по компонентам. Другой путь - это путь по последовательным уровням сложности.
Одним из правил Форта является то, что слово для вызова или для ссылки на него должно быть определено заранее. Обычно последовательность, в которой определяются слова, соответствует порядку возрастания функций, которые они должны делать. Такая последовательность приводит к естественной организации исходных текстов. Более мощные команды просто добавляются на вершину элементарных (рисунок 3-10а).
Рис.3-10. Два способа наращивания возможностей.
+- - - - - - - - - - - - - -+ Мощные функции загружены | +----+ +-----+ +-----+ | позже, с использованием | | | | | | | | элементарных | +|-\-+ +/---\+ +-/--|+ | +- | -\- -/- - -\- - / - | -+ +- | - \ / - - - \ -/- - | -+ | | +--/-+ \/ +-|+ | | | | |\ +----/\-+ | | | Элементарные функции | | +----+ \| |/| | | загружены сначала | +|-----+___| | |__| | | |______| +-------+ | +- - - - - - - - - - - - - -+
а) Мощные функции, использующие элементарные слова.
-----------------------------------------------
+- - - - - - - - - - - - - -+ | +----+ +-----+ _ +-----+ | Мощные функции | | |--| |/ \|
б) Элементарные слова, использующие мощные функции.
Вначале идут простейшие, как в букваре. Новичок, пробующий разобраться в проекте, имеет возможность прочитать элементарные части кода по мере движения к более углубленным.
Однако во многих крупных задачах дополнительный выигрыш лучше всего достигается путем улучшения некоторых начальных, корневых частей задачи (рисунок 3-10б). Имея возможность изменять работу слов на нижнем уровне, пользователь может менять возможности всех команд, которые используют корневые слова.
Возвращаясь в качестве примера вновь к текстовому процессору, рассмотрим одну из его примитивных функций - ту, которая переводит новую страницу. Она используется словом, переводящим новую строку; она вызывается, когда в странице кончаются строки. В свою очередь слово, переводящее строку, используется тем, которое форматирует слова в строке; когда очередное слово не влезает в строку, вызывается ПЕРЕВОД-СТРОКИ. Такая иерархия "использования" предполагает, что мы определили ПЕРЕВОД-СТРАНИЦЫ раньше в нашей программе.
В чем же проблема? Один из высокоуровневых компонентов имеет программу, которая должна вызываться словом ПЕРЕВОД-СТРАНИЦЫ. А именно, в случае, если таблица или рисунок появляется в середине текста, а время перевода формата еще не подошло, форматтер откладывает рисунок до следующей страницы, продолжая печатать текст. Такое действие требует возможности как-то "забираться" внутрь слова ПЕРЕВОД-СТРАНИЦЫ таким образом, чтобы в следующий раз оно выдавало бы распечатку отложенного рисунка в вершине новой страницы:
: НОВАЯ-СТРАНИЦА ... ( закончить страницу с подпечаткой) ( начать новую страницу с надпечаткой) ... ?ОТЛОЖЕННОЕ ... ;
Как может ПЕРЕВОД-СТРАНИЦЫ вызывать ?ОТЛОЖЕННОЕ, если последнее определено гораздо позже?
Хотя теоретически возможно организовать загрузку программы так, чтобы мощные функции вводились до корневых слов, такой подход плох по двум причинам.
Во-первых, разрушается естественная организация (по возрастающей мощности). Во-вторых, мощные функции часто используют код, который определен между элементарными словами. Если вы перемещаете мощные программы к началу, Вам приходится смещать туда же и все используемые ими слова, либо дублировать их код. Чрезвычайно бардачно.
Программирование по принципу уменьшения сложности можно организовать, используя технику "векторизации". Вы можете позволить корневой функции вызывать (указывать на) любую из различных программ, которые должны быть определены после самой этой функции. В нашем примере заранее необходимо создать только `имя` программы ?ОТЛОЖЕННОЕ, его определение может быть дано позднее.
В главе 7 рассматривается вопрос о векторизации в Форте.
ОГРАНИЧЕННОСТЬ МЫШЛЕНИЯ ПО УРОВНЯМ
Большинство из нас виновны в преувеличении разницы между "высоким уровнем" и "низким уровнем". Такое разделенме весьма спорно. Оно ограничивет нашу способность к здравым суждениям по проблемам программирования.
Мышление по уровням, в его традиционном виде, вносит искажения тремя способами:
1. Настаивает на том, чтобы разработка следовала структурной иерархии 2. Настаивает на том, чтобы уровни были отделены друг от друга, исключая этим возможность применения преимуществ повторного использования 3. Поощряет синтаксические различия между уровнями (например, ассемблер против "высокоуровневых" языков) и веру в то, что природа программирования как-то меняется, если уходить все дальше от машинного кода.
Давайте разберем одно за другим каждое из этих заблуждений.
С ЧЕГО НАЧАТЬ?
----------------------------------------------------------------
Я спросил Мура, как бы он подошел к разработке конкретной задачи - игры для детей. Когда дитя нажимает цифры на цифровой клавиатуре, от нуля до девяти, на экране появляется такое же количество больших квадратов.
Мур:
Я не начинаю с вершины и не прорабатываю задачу вниз. При получении такой ясной задачи я бы начал с написания слова, которое рисует квадрат.
Я начал бы с низа и закончил бы словом ИДИ, которое бы обслуживало клавиатуру.
Насколько много в этом интуитивного?
Быть может, кое-что есть. Я знаю, куда направляюсь, так что мне именно с этого начинать необязательно. И, к тому же, забавнее рисовать квадратики, чем программировать клавиатуру. Я буду делать то, что приятнее всего для того, чтобы углубиться в задачу. Если мне впоследствии придется стереть все эти детали, то это та цена, которую я плачу.
Вы защищаете подход по принципу "наибольшей приятности"?
Если Вы делаете это в свободной манере, то да. Если бы нам было нужно через два дня показывать это заказчику, я бы делал это по-другому. Я бы начал с самой заметной вещи, а вовсе не с самой забавной. Но все равно не в иерархической последовательности, сверху вниз. Я основываю свой подход на более насущных соображениях типа: произвести впечатление на покупателя, заставаить что-либо работать или показать другим людям, как оно будет работать с тем, чтобы их заинтересовать.
Если определить уровень как "вложенность", тогда да, это хороший путь для декомпозиции задачи. Но я никогда не видел пользы от употребления выражения "уровень". Другим аспектом уровней являются языки, мета-языки, мета-мета-языки. Пытаться разобраться в том, на каком уровне Вы находитесь - на ассемблерном, первом интеграционном или последнем интеграционном уровне - утомительно и мало помогает. Все мои уровни находятся в счастливом смешении.
----------------------------------------------------------------
Проектирование по компонентам делает мало значащим то место, с которого Вы начинаете. Можно было начать с интерпретатора клавиатуры, к примеру. Его целью является получение нажатий на клавиши и преобразование их в числа, с передачей этих чисел вызываемому извне слову. Если Вы замените его словом Форта "." ("точка" распечатывает число со стека), то сможете реализовать интерпретатор клавиатуры, проверить его и отладить без использования программ, имеющих что-либо общее с рисованием квадратов.
С другой стороны, если задача требует поддержки аппаратуры (например, графический пакет), каковой поддержки мы не имеем или не можем купить, то может понадобиться замена ее на что-то доступное, такое, как печать звездочками, для того, чтобы пощупать задачу. Мышление в терминах лексиконов подобно рисованию большой панорамы, состоящей из нескольких полотен. Вы работаете над каждым из полотен по отдельности, вначале набрасывая ключевые элементы сюжета, а затем добавляя цветные мазки здесь и там ... до тех пор, пока вся стена не будет закончена.
------------------------------------------------------------ СОВЕТ Решая, с чего начать проектирование, ищите:
* места, для которых требуется максимум творчества (места, где вероятность изменений наиболее велика) * места, которые дают самую удовлетворительную отдачу (пусть фрукты сочатся) * места, которые могут в дальнейшем наиболее сильно повлиять на другие области или которые определяют, может ли задача вообще быть разрешена * вещи, которые следует продемонстрировать заказчику для установления взаимопонимания * вещи, которые можно показать тем, кто дает деньги, если это нужно для продолжения финансирования. ------------------------------------------------------------
БЕЗ ПРЕДСТАВЛЕНИЯ НЕТ РАЗДЕЛЕНИЯ.
Второй путь, по которому уровни могут затруднять принятие оптимальных решений - это подталкивание к разделению на уровни. Популярная в проектировании конструкция, называемая "объектом" - типична для этой опасной филисофии.
Объект - это порция кода, которую можно вызвать по одному ее имени, но которая может выполнять более, чем одну функцию. Для выбора определенной функции следует вызвать объект и передать ему параметр или группу параметров. Вы можете представить себе параметры как ряд кнопок, которые можно надавливать для того, чтобы объект делал, что Вы хотите.
Выигрыш от проектирования задачи в терминах объектов состоит в том, что, как и компонент, объект упрятывает информацию от остальной задачи, облегчая обзор.
Несмотря на это, имеются несколько осложнений. Во-первых, объект должен содержать сложную структуру решений для определения, какую из функций ему выполнять. Это увеличивает объем объектного кода и снижает производительность. Лексикон же, со своей стороны, дает Вам все нужные функции при прямом вызове их по именам.
Во-вторых, объект обычно проектируется для автономного выполнения. Он не может использовать преимущества использования инструментария из компонентов поддержки. В результате в нем имеется тенденция к дублированию того кода, который появляется и в других частях задачи. Некоторым объектам даже предлагается разбирать входной текст для интерпретации своих параметров. Каждый из них может иметь свой синтаксис. Позорная трата времени и энергии!
Наконец, поскольку объект конструируется так, чтобы иметь конечный перечень возможностей, трудно делать добавления к его ряду готовому кнопок, когда нужна новая. Инструменты внутри объекта не спроектированы для повторного использования.
Идея уровней пронизывает дизайн моего собственного персонального компьютера, IBM PC. Кроме самого процессора (с его собственным набором машинных команд, разумеется), имеются программные уровни:
* набор утилит, написанных на ассемблере и прожженных в системном ПЗУ * дисковая операционная система, вызывающая утилиты * высокоуровневый язык директив, который вызывает операционную систему и утилиты * и, наконец, любая задача, использующая язык.
Утилиты в ПЗУ предоставляют зависимые от аппаратуры программы: работающие с видеоэкраном, дисководами, клавиатурой. Их вызывают, помещая управляющий код в определенный регистр и генерируя подходящее программное прерывание.
К примеру, программное прерывание 10Н вызывает вход в набор программ для работы с изображением. Имеются 16 таких программ. Вы загружаете регистр AH номером желаемой функции.
К сожалению, изо всех 16-ти программ нет ни одной, печатающей строку текста. Для того, чтобы это сделать, Вы должны повторять процесс загрузки регистров и генерации программного прерывания, которое, в свою очередь, должно решать, о какой из программ идет речь и проделывать еще несколько других вещей, которые Вам не требуются - для `каждого отдельного символа`.
Попробуйте написать текстовый редактор, в котором весь экран нужно обновлять при каждом нажатии на клавишу. Работает медленно, как почта! Нельзя улучшить скорость работы, поскольку нельзя повторно использовать никакую информацию внутри видео-программ, кроме той, которая дана для наружного использования. Обоснованной причиной этого является `изоляция` программиста от адресов устройств и других деталей аппаратуры. Ведь все это может измениться при будущих улучшениях.
Единственный способ эффективной реализации ввода/вывода изображения на этой машине - это записывать строки непосредственно в видеопамять. Это можно легко сделать, поскольку руководство по эксплуатации рассказывает об адресах, с которых начинается видеопамять. Однако это разбивает все усилия проектировщиков системы. Ваш код может не пережить замены аппаратуры.
Предполагая `защитить` программиста от деталей, разделение убило цель упрятывания информации. Компоненты же, наоборот, не являются выделенными модулями, но лишь добавлениями, приплюсованными к словарю. Видеолексикон мог бы, в конце концов, давать имя адреса видеопамяти.
Нет ничего неправильного в концепции интерфейсного выбора исполняемых функций между компонентами, если это необходимо. Проблема здесь состоит в том, что видеокомпонент был спроектирован некомплектно. С другой стороны, если бы система была полностью интегрирована - операционная система и драйверы написаны на Форте - видеокомпонент не `должен` был бы быть спроектирован для ответа на любые потребности. Программист конкретной задачи мог бы либо переписать драйвер, либо написать расширение к драйверу с использованием подходящих инструментов из видеолексикона.
------------------------------------------------------------ СОВЕТ Не хороните свои инструменты. ------------------------------------------------------------
ГОРА ЧЕПУХИ.
Заключительным заблуждением, подготовленным уровневым мышлением, является то, что языки программирования должны меняться качественно при "повышении" их уровня.
Мы пытаемся говорить о высокоуровневом коде как о чем-то утонченном, а о низкоуровневом - как о чем-то грубом и простецком.
До некоторой степени такие различия имеют под собой почву, но это лишь результат произвольных архитектурных ограничений, которые все мы принимаем за норму. Мы взращены на ассемблерах, имеющих сжатые мнемоники и ненатуральные синтаксические правила, поскольку они "низкого уровня".
Концепция компонентов восстает против поляризации на высокий и низкий уровень. Весь код должен выглядеть и вести себя одинаково. Компонент - это просто набор команд, которые вместе преобразуют структуры данных и алгоритмы в полезные функции. Эти функции могут быть использованы без знания структур и/или алгоритмов, их составляющих.
Дистанция между этими структурами и настоящим машинным кодом к делу не относится. Код, написанный для манипуляции битами в выходном порту, теоретически, выглядит не более пугающим, нежели код для форматирования докладов.
Даже машинный код должен быть удобочитаем. По-настоящему основанная на Форте машина имела бы синтаксис и словарь, единообразный и идентичный "высокоуровневому" словарю, известному нам сегодня.
РЕЗЮМЕ
В этой главе мы рассмотрели два пути разбиения задачи: на компоненты и в соответствии с возрастающей сложностью.
Особое внимание должно уделяться тем компонентам, которые служат интерфейсами между другими компонентами.
Теперь, если Вы правильно произвели предварительное проектирование, в вашу задачу входит взобраться на кучу управляемых кусочков. Каждый из них представляет задачу, которую надо решить. Выберите Ваш любимый кусок и обращайтесь к следующей главе.
ДЛЯ ДАЛЬНЕЙШЕГО РАЗМЫШЛЕНИЯ
(Ответы приведены в приложении Д)
1. Ниже приведены два подхода к определению интерпретатора клавиатуры. Какой из них предпочли бы Вы? Почему?
А) ( Определение клавиш) HEX 72 CONSTANT ВВЕРХКУРС 80 CONSTANT ВНИЗКУРС 77 CONSTANT ВПРАВОКУРС 75 CONSTANT ВЛЕВОКУРС 82 CONSTANT ВСТАВКА 83 CONSTANT ЗАБОЙ
( Интерпретатор клавиш) : РЕДАКТОР BEGIN ЕЩЕ WHILE KEY CASE ВВЕРХКУРС OF КУРС-ВВЕРХ ENDOF ВНИЗКУРС OF КУРС-ВНИЗ ENDOF ВПРАВОКУРС OF КУРС-ВПРАВО ENDOF ВЛЕВОКУРС OF КУРС-ВЛЕВО ENDOF ВСТАВКА OF УСТ-ВСТАВКУ ENDOF ЗАБОЙ OF СТИРАНИЕ ENDOF ENDCASE REPEAT ;
Б) ( Интерпретатор клавиш) : РЕДАКТОР BEGIN ЕЩЕ WHILE KEY CASE 72 OF КУРС-ВВЕРХ ENDOF 80 OF КУРС-ВНИЗ ENDOF 77 OF КУРС-ВПРАВО ENDOF 75 OF КУРС-ВЛЕВО ENDOF 82 OF УСТ-ВСТАВКУ ENDOF 83 OF СТИРАНИЕ ENDOF ENDCASE REPEAT ;
2. Эта задача - упражнение по упрятыванию информации.
Предположим, имеется район памяти вне словаря Форта, которые мы хотим зарезервировать под структуры данных (по какой-либо причине). Участок начинается с шестнадцатеричного адреса C000. Мы хотим определить последовательности массивов, которые будут находиться в данной памяти.
Мы могли бы сделать что-то вроде:
HEX C000 CONSTANT ПЕРВЫЙ-МАССИВ ( 8 байтов) C008 CONSTANT ВТОРОЙ-МАССИВ ( 6 байтов) C00C CONSTANT ТРЕТИЙ-МАССИВ ( 100 байтов)
Определенные выше имена массивов будут возвращать начальные адреса соответствующих массивов. Однако заметьте, что нам пришлось вычислять правильный начальный адрес для каждого из них, основываясь на знании того, сколько байтов уже зарезервировано. Давайте попытаемся автоматизировать это, введя "указатель резервирования" по имени >ПАМЯТЬ, который указывает на следующий свободный байт. Вначале мы устанавливаем указатель на начало места в памяти:
VARIABLE >ПАМЯТЬ C000 >ПАМЯТЬ !
Теперь мы можем определить каждый из массивов так:
>ПАМЯТЬ @ CONSTANT ПЕРВЫЙ-МАССИВ 8 >ПАМЯТЬ +! >ПАМЯТЬ @ CONSTANT ВТОРОЙ-МАССИВ 6 >ПАМЯТЬ +! >ПАМЯТЬ @ CONSTANT ТРЕТИЙ-МАССИВ 100 >ПАМЯТЬ +!
Заметьте, что после определения каждого из массивов мы увеличиваем указатель на размер этого массива, чтобы показать, что мы отвели для него столько дополнительной памяти.
Для большей удобочитаемости вышеприведенного мы должны добавить такие два определения:
: ТАМ ( -- адрес-следующего-свободного-байта-в-ОЗУ) >ПАМЯТЬ @ ; : ДАТЬ-ОЗУ ( #байтов-для-массива -- ) >ПАМЯТЬ +! ;
Мы можем теперь переписать то же самое как:
ТАМ CONSTANT ПЕРВЫЙ-МАССИВ 8 ДАТЬ-ОЗУ ТАМ CONSTANT ВТОРОЙ-МАССИВ 6 ДАТЬ-ОЗУ ТАМ CONSTANT ТРЕТИЙ-МАССИВ 100 ДАТЬ-ОЗУ
(Опытный Форт-программист, скорее всего, скомбинировал бы все эти операции в единое определяющее слово, однако это не то, к чему я подвожу.)
Наконец, предположим, что у нас имеется 20 таких определений массивов, разбросанных по всему тексту.
Теперь задача: вдруг меняется архитектура нашей системы и мы решаем, что должны отвести эту память так, чтобы она `заканчивалась` на шестнадцатеричном адресе EFFF. Другими словами, мы должны начинать с конца, отводя массивы в обратном порядке. Мы при этом все равно хотим, чтобы имя массива возвращало его `начальный` адрес.
Чтобы проделать это, нам теперь нужно написать:
F000 >ПАМЯТЬ ! ( последний байт EFFF плюс 1) : ТАМ ( -- адрес-следующего-свободного-байта-в-ОЗУ) >ПАМЯТЬ @ ; : ДАТЬ-ОЗУ ( #байтов-под-массив -- ) NEGATE >ПАМЯТЬ +! ; 8 ДАТЬ-ОЗУ ТАМ CONSTANT ПЕРВЫЙ-МАССИВ 6 ДАТЬ-ОЗУ ТАМ CONSTANT ВТОРОЙ-МАССИВ 100 ДАТЬ-ОЗУ ТАМ CONSTANT ТРЕТИЙ-МАССИВ
На этот раз ДАТЬ-ОЗУ `уменьшает` указатель. Все нормально, легко добавить NEGATE в определение ДАТЬ-ОЗУ. Беспокойство вызывает только то, что мы должны ДАТЬ-ОЗУ `до` определения массива, а не после. В нашей программе необходимо найти и исправить двадцать мест.
Слова ТАМ и ДАТЬ-ОЗУ хороши и приятны, но не скрывают информации о том, `как` отводится место. Если бы они это делали, не имело бы значения, в каком порядке их вызывают.
И вот, наконец, наш вопрос: что мы могли бы сделать со словами ТАМ и ДАТЬ-ОЗУ для минимизации влияния изменений в проекте? (Опять же, ожидаемый мною ответ не должен опираться на определяющие слова.)