- •Глава II
- •12. Предварительные замечания о процессе разработки программ
- •12.1. Жизненный цикл математического обеспечения
- •12. Предварительные замечания о процессе разработки программ
- •12.1. Жизненный цикл математического обеспечения
- •12.2. Анализ требований
- •12.3. Пример задачи
- •13.1. Обзор процесса проектирования
- •13.2.1. Вводный раздел
- •13.2.2. Разделы абстракций
- •13.8. Абстракция строки
- •13.9. Обзор и обсуждение
- •14. Этап перехода от проектирования к реализации
- •14.1. Оценка проекта
- •14.1.1. Корректность и эффективность
- •14.1.2. Структура
- •15.2. Выбор подхода
13.2.2. Разделы абстракций
Разобьем часть рабочего журнала, предназначенную для отдельных абстракций, еще на пять частей: 1)спецификация функционального поведения а-бстракции; 2)огчисание ограничений по эффективности, которые необходимо со^элтости; 3)идформация о способе реализации; 4)информация с» методах тестирования;5)дополетительная информация, не попадамощая в четыре перечисленные категории.
Спецификация, разумеется, является наиболее важной частью этого списка. Однако о спецификациях говорилось уже достаточно, поэтому здесь мы их касаться не будем,
Ограничения по эффективности обычно распространяются по программе сверху вниз. Требования в спецификации могут ограничивать время выполнения программой отдельных задач, а также объем используемой оперативнойхлвнешней памяти. Для вывода о том, что реализация целевой абстракции будет удовлетворять ограничениям по эффективности, необходимо сделать заключения относительно эффективностла вспомогательных абстракций. Эти заключения лягут в осно-ву ограяичений по эффективности, входящих в те части рабочего журнала, в которых описываются подробности каждой вспомогательной абстракции.
Ограничения по эффективности могут быть представлены множеством способов. Чаще всего они выряжаются в виде функций размера входных данных. Мы можем, на пример, написать в качестве части описания процедуры sortс формальным аргументом х тип list [int]следующее:
Эффективность
худшее время выполнения ==порядок (длина (х) * log(длина (х))
дополнительная оперативная память ==порядок (длина (х)) максимальный временный объем оперативной памяти ==порядок (длина (х))
Проектирование
Это означает
1.Время, необходимое для сортировки списка х никогда не будет больше некоторой постоянной константы, умноженной на произведение длины х и логарифма (по основанию 2)длины х.
2.При возврате из процедуры sortобщий объем оперативной памяти, отведенный в данный момент программе, содержащей эту процедуру, не может стать большим, чем значение, равное некоторой константе, умноженной на длину х.
3.В процессе выполнения процедуры sortобъем отведенной под задачу оперативной памяти не может превысить значения некоторой константы, умноженной на длину х.
Для большинства задач достаточно бывает указать относительно простые ограничения по эффективности, подобные только что рассмотренным. Иногда, однако, полезно ограничить эти константы или даже ввести абсолютные ограничения. Абсолютное значение верхней границы по времени необходимо для задач, работающих в реальном масштабе времени. Аналогично мы накладываем ограничения на объем памяти под программы, выполняющиеся на небольших машинах или машинах, не имеющих виртуальной организации памяти. Если процедура sortдолжна быть реализована в системе реального времени, установленной на машине без внешней памяти, то мы могли бы записать]
Эффективность
худшее время выполнения == 1секунда
дополнительная оперативная память ==порядок (длина (х)) максимальный временный объем оперативной памяти •=•порядок (длина (х))
дополнительная внешняя память ==О временная дополнительная внешняя память ==О (Для процедуры sortабсолютные ограничения по времени будут приемлемы только в том случае, если в спецификации requires на длину входного списка х также накладываются ограничения.)
Раздел рабочего журнала, содержащий информацию о том, как должна быть реализована абстракция, должен содержать список вспомогательных абстракций. Для типов данных этот раздел должен описывать представление, инвариант представления и функцию абстракции, связанную с предполагаемой реализацией. Данный раздел должен также включать описание принципа работы реализации. Это описание может быть опущено, если принцип работы реализации очевиден, однако при использовании сложной «хитроумной» реализации он необходим. Очень часто бывает полезно привести набросок используемого алгоритма, например:
Реализочать sort (х) при помощи рекурсивного алгоритма вида: if длина (х) = 1
then х else merge (merge-sort (первая-половнна (х)), merge.sort (вторая, половина (х)))
276 Глава 13
Мы можем также привести соответствующую ссылку на литературу, например: «Реализация сортировки с использованием метода сортировки слиянием» из книги: Кнут Д. Искусство программирования для ЭВМ, т. 3,разд. 5.2.4.—М.: Мир, 1976. Важным критерием оценки проекта является степень легкости его тестирования. В разделах рабочего журнала для каждой из абстракций должен указываться предполагаемый способ тестирования. Эта информация может предполагать тестировочные данные, описание драйвера, используемого для ввода проверочных данных и проверки результата, а также различные соображения по поводу заглушек, использующихся для подмены вспомогательных абстракций.
В разделе «Дополнительная информация» могут содержаться, например, следующие типичные сведения: 1) пояснения к решениям, описанным в других разделах; 2) обсуждение рассмотренных и отвергнутых альтернатив; 3) потенциальные расширения или иные модификации абстракции; 4) информация о контексте, в котором предполагается использоваться абстракция.
Завершая данный раздел, мы должны отметить, что в том случае, когда объемы по проектированию cлишкo^л велики, бывает полезно структурировать рабочий журнал, дополнительно заведя несколько вспомогательных. В диаграмме модульной зависимости любой подграф может рассматриваться как независимая подсистема. Однако наиболее удобно выбирать подграф, в котором доступ извне делается только к одному узлу.
13.3. Описание проблемы
В оставшейся части главы мы выполним полное проектирование простого форматировщика текста. Эта программа является небольшой и предназначен.-] главным образом для иллюстрации процесса проектирования. В данном разделе приводится спецификация форматировщика. В последующих описывается его проектирование. Реализация приведена в приложении Б. Входные данные для форматировщика текста состоят из последовательности неформатированных строк текста и командных строк.
Каждая строка (за исключением, может быть, последней) завершается символом начала новой строки, а строка с командой начинается с точки, что отличает ее от строк с текстом. Например;
Выключка строк производится только в режиме заполнения. В режиме без заполнения каждая вводимая строка текста выводится без ' модификации.
команда .brвызывает переход к следующей строке..br Как в данном случае.
Проектирование
Программа выдает выравненный по правому краю, разбитый постранично и содержащий абзацные отступы текст. Выключка строк производится только в режиме заполнения. В режиме без заполнения каждая строка выводится без модификации. Команда .br вызывает переход к следующей строке. Как в данном случае.
Выходной текст отстоит от левого края на десять пробелов и разбивается на страницы по 50строк в каждой. В начале каждой страницы делается заголовок из 5строк. Заголовок состоит из 2 пустых строк, строки видаg. п
где п есть номер страницы, и еще двух пустых строк.
Входная строка состоит из последовательности слов и символов разделения слов. К символам, разделяющим слова, относится символ пробела, символ табуляции и символ перехода к новой строке. Все остальные символы являются составляющими слов. Пробелы и символы табуляции располагаются в текущей выходной строке вместе со словами. Например, если во входной строке два слова отделены друг от друга двумя символами пробела и эти слова появляются в той же самой выходной строке, то они будут отделены друг от друга по крайней мере двумя пробелами. Если два слова расположены на различных строках, то пробелы между ними могут не сохраняться.
Форматировщик имеет два основных режима работы. В режиме без заполнения каждая входная строка выводится без модификации. Форматировщик изначально находится в режиме заполнения, в котором он выдает выходные строки длиной 60,символов и обрабатывает входные данные до тех пор, пока на текущую выходную строку еще могут быть помещены слова (символы перехода к новой строке рассматриваются как пробелы). Затем строка выравнивается по правому краю. Выравнивание осуществляется вставкой между словами дополнительных символов пробела до тех пор, пока крайний правый символ последнего слова не окажется в крайней правой позиции строки. Пробелы должны добавляться максимально равномерно, чтобы избежать широких пустых участков (что происходит, например, при добавлении дополнительных пробелов в одном и том же месте строки). Пробелы могут добавляться только между словами справа от символов табуляции. Если добавить пробел невозможно, то выравнивание не производится и выдается сообщение об ошибке. Если длина входного слова превышает длину строки, то строка выводится без изменений, а сообщение об ошибке не выдается.
Входная строка начинается а символа перевода строки, вызывающего переход на новую строку; если текущая выходная строка не пуста, то она не заполняется и не выравнивается, а вы-
278 Глава 13
^Проектирование
водится без изменений. Если текущая выходная строка пуста, то символ перехода на следующую строку не оказывает никакого эффекта. Пустая входная строка (не содержащая символов пробела, табуляции и слов) вызывает переход на следующую строку и вывод пустой строки. Форматировщик воспринимает три команды:
.br вызывает переход на следующую строку
.nf вызывает переход на следующую строку и установку режима без заполнения
.fi вызывает переход на следующую строку и устанавливает режим с заполнением
Неопознанная команда приводит к выдаче сообщения об ошибке и игнорируется. Сообщение описывает ошибку и приводит номер строки, в которой она произошла.
Спецификация форматировщика приведена на рис. 13.2. Поскольку форматировщик предназначен для самых различных текстор, то мы не делаем никаких замечаний по поводу входных данных (т. е. предложение requires отсутствует). Спецификация описывает назначение процедуры, ссылаясь на информацию в данном разделе; спецификация, заносимая в рабочий журнал проектировщика, будет содержать данный текст. Ограничения по эффективности определены следующим образом: время работы определится как порядок (п), где п есть число символов в ins. Добавление пробелов добавит порядок (п) (память под outs), но дополнительный объем временно используемой памяти будет небольшим, гораздо меньше чем порядок (п). Наконец, проект должен предусматривать такие потенциальные модификации, как введение дополнительных команд (например, команды для размещения рисунков и команды смены шрифта), а также работу с различными типами устройств вывода. Также он должен быть достаточно прост и иметь модульную структуру.
format = proc (ins, outs, err: stream) signals (badarg (string)) modifies ins, outs, errs
effects Если ins закрыто на чтение или outs или errs закрыты для записи, то выдается сигнал через badarg (s), где s указывает неправильно открытый аргумент [например, badarg («входной поток»)]. В противном случае программа format продолжает работу так, как это описано в тексте, принимая входные данные из ins, и помещает выходные данные в outs, а сообщение об ошибках — в errs. Перед возвратом ins, outs и errs закрываются.
Рис. 13,2. Спецификация форматировщика.
13.4. Начальная стадия проектирования
В последующих разделах процесс проектирования будет проиллюстрирован на примере разработки программы на языкеCLU,реализующей форматировщик текста. Это представление
'.будет несколько идеализированным; например, не будет допущено .никаких ошибок. Мы обсудим это в разд. 13.9.
Приступая к проектированию, мы начинаем с диаграммы модульной зависимости, содержащей абстракции, определенные в процессе анализа требований. Этот граф может содержать дуги, поскольку некоторые абстракции могут зависеть от других. (Например, в спецификации требований для учетной системы может быть указан процессор команд как процедура и база данных — как тип данных; процессор команд зависит от базы данных, поскольку он делает обращения к операциям из базы данных.) Мы начнем проектирование с занесения этой диаграммы и спецификаций абстракций в рабочий журнал. Затем наметим первую задачу. Поскольку в нашем случае имеется только одна абстракция — format,то она автоматически становится нашей первой задачей.
На первом шаге необходимо определить дополнительные абстракции. Имеется одно эвристическое правило, которое может помочь нам в этом процессе; пусть структура задачи определяет структуру программы. Это означает, что мы должны сосредоточить все внимание на решаемой задаче и попытаться разработать структуру программы.
Изучение структуры программы удобно начинать с составления списка задач, которые должны быть решены. Ниже приводится список для задачи formats
1.Считать входные данные.
2.Интерпретировать их.
3.Получить выходные данные.
Мы не считаем, что окончательная программа будет состоять из частей, соответствующих перечисленным пунктам. Перечисление задач представляет собой только первый шаг. На следующем шаге мы должны использовать этот список как руководство для определения абстракций, которые будет определять структуру программы.
Хотя мы перечислили задачи приблизительно в том порядке, в котором они должны решаться, мы не считаем, что этот порядок сохранится в окончательной программе. В действительности нам предстоит сделать выбор. Мы можем завершить очередную задачу до того, как приступить к следующей, однако наша цель не предполагает интерпретацию всех входных данных до получения выходных. Можно выполнять эту работу постепенно, интерпретируя входные данные и тут же выдавая выходные. Мы выберем последний вариант, поскольку это позволяет удовлетворить ограничениям на объем памяти.
При определении абстракций мы должны скрыть подробности обработки, не принадлежащие текущему уровню проектирования. Хотя для подобного скрытия можно использовать процедуры и
280 Глава 13
актирование
итераторы, для этой цели лучше всегод(у^олктипы данных.' Мы будем использовать типы в подходяш.-^ ^ этого местах: входных, выходных, внутренних и внеш^^ "^ах данных и отдельных элементах данных. Например, мь^ моя»,, использовать тип данных для скрытия подробностей обр^^™' выходных данных (задача 3).Сначала мы должны реши^' ^й уровень ин-капсулирования принять. Мы можемвве^™^тракциюр.ля етроки, скрывающую форматированиео^^.е.пъщстроки, или абстракцию страницы, скрывающую форм^^Р^пие отдельной страницы. Ни один из этих приемов не поз^^^молностью инкапсулировать выходные данные, и хотя по.^"^ ^страгирование представляется весьма заманчивой возмож^^™^ так как при этом облегчается получение выходных дани^^' ^ако оно было бы полезно только в том случае, если бы м^^^^ло ограничиться изменением только одной абстракции (^ ^at).В данном случае нашим решением будет введение тип^лащ\ doc,информированного о характере входных данных, "^^йщего выходные данные в соответствии с ним. Тип docсх^^ ^ ^ким абстрактным устройством вывода и операциями высо^°™ ^овня, хорошо приспособленными для форматирования.
Аналогично тому как тип docинкапсУ-^РУ^ подробности обработки выходных данных, нам хотелось "^ ^Целить и подробности обработки входных данных. Можн^ "^бы изобрести абстракцию признаков, представляющую ос^^^йые элементы во входном тексте, подобные словам и симв^^" ^буляции. Однако для создания подходящего признака м^ ^^ны знать, находимся ли мы в конце строки, поскольку в э"°" ^сте точка интерпретируется иначе. В действительности Р^^^риваемая таким образом проблема предлагает строкоор^^^ованный подход. Если мы примем такую структуру, то зп^^ ^ущее положение в строке будет довольно просто. Поэтому о^Р^Д^им процедуруdoJine,обрабатывающую входные данные по^Р^о. Программаformatбудет отвечать за обработку всехвхор-^^^рок, а процедура doJineбудет отвечать за обработку ^^^^ых строк.
Наконец, мы должны рассмотреть п. 2,ка^^^ся интерпретации входных данных и взаимосвязи между "Р°^Нурой doJ'ine и doc.Простым решением может быть возло^^^ haпроцедуруdoJineзадачи интерпретации входных даннь^ "^зова соответствующих операций для типа doc.Такое peill^™^^ебует, чтобы мы передавали процедуре doJineобъект docв качеств аргумента. Программа formatответственна за порожЛ^^ и окончание процесса выдачи выходных данных. Тип doc"°^рживает операцию create,инициирующую вывод, а также ^"^Р^ю terminate, завершающую его.
Теперь мы можем перейти ко второму ша^У "Р^ктирования, а именно утверждению и документирования ^"^ абстракций.
процессе документирования уточняются пропущенные детали. „екоторые из них просто являются неучтенными, а некоторые Гусловлены ошибками проектирования и требуют модификаций. ругими словами, в процессе документирования выполняется "сьма большой объем работы. Например, при спецификации роцедуры do.lineмы должны точно решить, как позициопи-уется входной поток при вызове и возврате из процедуры. Мы злжны также решить, какой модуль ответствен за проверку соот-этетвия различных потоков аргументов требуемым. Далее, „гобходимо решить, должна ли процедура- doJineвыдавать сосб-дения об ошибке, а также то, каким образом задача formatуз-(ает, что все входные данные были обработаны.
Спецификация процедуры doJineприведена на рис. 13.3, •де указаны также ограничения по эффективности. Эта спецификация содержит ответы на наши вопросы. Программа doJineот-«етственна также и за сообщения об ошибках, исключая проверку «ежимов для insи errs,а также за закрытие их. Insустанавли-йется на начало строки как при вызове do.line,так и при возврате из нее. Процедура doJineсообщает процедуре formatо за-ершении ввода входного потока путем инициирования исключи-ельной ситуации.
o_line==proc (ins: stream, d; doc, err: stream) signals (all. done) requires == can. read (ins) & can-write (errs) &
d не было завершено. modifies ins, errs, d
effects Если ins пуст, то вызывается all-done. В противном случае обрабатывается одна входная строка из ins в d, как это определено в спецификации format, а сообщения об ошибках записываются в errs. Обрабатывается вся строка, включая символ конца строки.
efficiency Время и объем памяти для просмотра и обработки одной строки пропорциональны числу символов в строке,
1'Рис. 13.3. Спецификация процедуры do. line.
I
1Спецификация для docприведена на рис. 13.4.Спецификации Операций приведены только для createи terminate.Остальные ^спецификации будут приведены позднее. Отметим, что тип doo 'предполагает проверку программой formatвозможности записи ^ outs.Этотребование необходимо по той причине, что тип d^c ;должен иметь контроль над записываемой в поток информацией.'В настоящий момент наше представление о типе docдостаточно неполно. При введении абстракций данных мы определяем только те используемые в реализации операции, которые изучаем в настоящий момент. Дополнительные операции будут введены при изучении других использующих данный тип реализаций. Отметим,чтоспецификации docи doJineне ссылаются к процедуре format. ^ Такие ссылки типичны для спецификаций вспомогательных абстракций. Ссылки лучше, чем копирование, поскольку они ведут
Й82 Глава 13
doc == data type is create, terminate requires
Фактический выходной поток, связанный с аргументом outs операции create, не должен модифицироваться вне doc в период времени между созданием объекта doc и моментом прекращения к нему доступа.
Описание
Doc представляет собой абстрактное выводное устройство, выдающее форматированный текст (как было определено в спецификации для format), соответствующее информации, передаваемой ей через операции. Объект doc связан с выходным потоком, передаваемым к операции create. К моменту возврата из операции terminates все выходные данные должны быть помещены в этот поток.
Операции
create = proc (outs: stream) returns (doc)
requires can. write (outs) modifies outs
effects Готовится создать документ для outs, начиная со страницы 1 в режиме с заполнением.
terminate == proc (d; doc) requires terminate (d) до этого не вызывался. modifies d
effects Заканчивает запись информации из d в связанный с ней выход-ной поток, а затем закрывает этот поток.
end doc
efficiency время = порядок (п), где п есть число символов, записываемых в выходной поток. Дополнительная память == порядок (п). Временный используемый объем памяти гораздо меньше, чем порядок (п).
Рис. 13.4. Частичная спецификация для doc.
к созданию более кратких спецификаций, позволяющих создать более ясную абстракцию.
Расширенная диаграмма модульной зависимости приведена на рис. 13.5.Диаграмма содержит узлы для docи doJineи дуги, определяющие их использование. На этом этапе проектирования мы знаем, что процедура formatиспользует тип docи процедуруdoJineи что объект docпередается к doJine.Если абстракция использует объект некоторого типа как аргумент или результат, то она, следовательно, использует этот тип. На приведенной ди-
Рис. 13.5. Расширенная диаграмма модульной зависимости,
Проектирование
аграмме doJineиспользует doc,что показано дугой от doJine к doc,при этом doJineнаходится на более высоком уровне диаграммы, чем doc. Набросок реализации formatприведен на рис. 13.6.
проверить типы аргументов d: doc :== doc$create (outs)
call do_line (ins, d, errs) до тех пор, пока она не выдаст сигнал alL done» Завершить d и закрыть ins и errs.
Рис. 13.6. Набросок реализации процедуры format.
13.5. Обсуждение метода
В процессе проектирования мы использовали простой способ фокусирования внимания на задаче —разбиение задачи на подзадачи, а затем исследование того, каким образом реализовать эти подзадачи. Однако для каждой подзадачи мы не просто вводили соответствующую процедуру, а искали абстракции, особенно типы данных, позволившие учесть подробности задачи. Мы считаем, что это улучшает проектирование, позволяя отложить учет мелких подробностей до более поздних стадий проектирования и сосредоточить внимание на связях между различными задачами.
Мы ввели абстракции для сокрытия подробностей, представляющихся несущественными на текущем уровне. При этом возникает вопрос о том, каким образом решить, что считать существенным на текущем уровне. Данное решение не поддается формализации, однако относительно него можно сделать ряд замечаний. Разумеется, ряд проблем решит реализация, но часть из них тем не менее останется. Наша цель —прийти к небольшим модулям, Реализации процедур и операторов внутри и снаружи типов данных должны иметь размер, не превышающийодной-де^х страниц. Если в программе formatмы решили позаботиться о подробностях ввода и вывода, то модуль получился бы чересчур большим. Кроме этого, различные части реализации модуля должны находиться приблизительно на одном и том же уровне дета.ли-зации. Даже обрабатывая входные данные построчно, программаformatхорошо сбалансирована с этой точки зрения, поскольку эти решения вписываются в структуру программы.
Проектирование программы formatявляется типичным для проектирования абстракции верхнего уровня в системе. Реализации подобных абстракций относятся главным образом к организации вычислений, а подробности выполнения шагов поручаются вспомогательным абстракциям. Также типично введение частично определенных типов данных, подобно типу doc.На ранних стадиях проектирования мы часто знаем, что два модуля должны взаимодействовать друг с другом, однако не знаем, каким образом это должно происходить. В частности, мы знаем, что модули взаи-
284 Глава 13
Проектирмание
модействуют через объекты некоторого типа, но при этом неизвестно, какие операции над этими объектами окажутся полезными. Следовательно, спецификация разделяемого типа является необходимо неполной. Она будет завершена в процессе проектирования.
13.6. Последующие стадии проектирования
Теперь у нас имеется диаграмма модульной зависимости, содержащая несколько абстракций. Каким должен быть следующий шаг? Первое, что можно отметить, это тот факт, что на следующем шаге в качестве целевых годятся не все абстракции. Назовем кандидатами те из них, которые удовлетворяют необходимым требованиям. Очевидно, что сама процедура format не может быть кандидатом, поскольку мы уже спроектировали её реализацию. Кроме этого, doc также не может быть кандидатом, поскольку она незавершена. (Нам придется выбирать в качестве кандидата неполную абстракцию — см. разд. 13.9.) У нас остается единственный кандидат — do_line. Абстракция doJine должна обрабатывать строку текста или строку команды. Эти два случая существенно отличаются друг от друга, поэтому для их обработки мы введем две процедуры, do_text_line и do_command. Хотя обработка команды и строк текста может производиться непосредственно, добавление этих двух процедур может упростить будущие модификации, облегчая локализацию изменяемых участков, например в случае добавления новой команды. Спецификации этих процедур приведены на рис. 13.7. Отме-
do_ text. line == proc (ins: stream, d: doc)
requires can-read (ins) &—empty (ins) & d еще не было завершено. modifies ins, d
effects Считывает одну строку из ins, включая символ конца строки, и обрабатывает ее как текст, согласно определению в спецификации для do-line. do-command = proc (ins: stream, d: doc) signals (error (string)) requires can-read (ins) &— empty (ins) & d еще не было завершено. effects Считывает одну строку из ins, включая символ конца строки. Удаляет первый символ и обрабатывает оставшуюся часть строки как строку команды, как это определено в спецификации do- line. Если команда ошибочна, то об этом сигнализируется через error (s), где s описывает ошибку. Однако, даже если команда ошибочная, обрабатывается вся строка.
Рис. 13.7. Спецификации операций do-text-line и do-command.
тим, что как do_text -line,так и do_commandполагают, что при их вызове insустановлен на первый символ строки. Это означает, что процедура do_lineможет только читать первый символ, не удаляя его, т. е. можно воспользоваться процедурой stream$peekc, а не stream$getc.
Новая диаграмма модульной зависимости приведена на рис. 13.8 Теперь на ней имеется пунктирная дуга от do_lineк doc,посколькуdo_lineне вызывает из docникаких операций. Приступая к ана-
Рис. 13.8. Диаграмма модульной зависимости после проектирования абстракции do-line.
лизу do_line,мы не знаем пока, будет ли использование doo сильным или слабым, поэтому полагаем, что оно будет сильным. Аналогичные предположения можно сделать и относительно do. text-lineи do-command.
Теперь мы располагаем двумя кандидатами — do_text-line и do_command.Какого из них выбрать? Четких и строгих правил здесь не существует: можно остановиться на любом кандидате. Однако имеется несколько причин, позволяющих выделить более предпочтительную кандидатуру:
1.Мы пока не знаем, как реализовывать процедуру. Например, нам может потребоваться реализовать некоторую абстракцию очень эффективно, однако мы не знаем, как достичь этой эффективности, и даже вообще, достижима ли она.
2.Мы не вполне уверены в том, что она нам подходит.
3.Она может оказаться более предпочтительной для нашего проекта, чем другая, поэтому изучение ее реализации облегчит проектирование.
4.Нам хотелось бы целиком закончить работу с какой-то одной, уже начатой частью проекта.
Первые два правила касаются исследования еще нерешенных целиком вопросов. Выбор абстракции по одной из этих причин позволит быстро обнаружить невыявленные ошибки проекта. Если подобная неопределенность имеет место, то желательно исследовать ее как можно быстрее. Два других правила прямо противоположны: завершение работы в какой-то одной области может и не.упростить дальнейшую работу, однако весьма полезно (не говоря уже о психологической стороне вопроса) сократить общее число нерешенных задач.
В нашем примере различие между двумя кандидатами невелико, поэтому мы можем достаточно произвольно выбрать процедуруdo_text_line.Основной вопрос заключается в том, как передавать информацию docо входных данных. Одним из способов может быть передача некоторого опознавателя, кодирующего эту информацию. Однако такой подход ведет к дополнительной работе;
286 Глава 13
процедура do_text_lineдолжна кодировать информацию, которая затем должна быть декодирована в doc.Гораздо лучше обеспечить в docоперации для каждого такого случая. Это решение более эффективно и по пространственно-временным характеристикам.
К docпередается информация следующих типов: слова, пробелы, символы табуляции, конец строки, перевод строки (если первый символ в строке есть пробел или символ табуляции) и пустая строка (если вся строка пуста). Каждый из этих типов должен обрабатываться отдельной операцией: add_word, add.space, add_tab, add.newline, breakJineи skip_line.Преимущество такой схемы заключается также и в том, что она легко расширяема.
Другой вопрос касается представления слов: могут ли слова быть строками, или нам необходима абстракция данных? Мы могли бы использовать абстракцию данных, если бы это облегчило дальнейшие модификации, однако в данном случае это не так. Например, абстракция слова не поможет в добавлении дополнительных шрифтов, поскольку изменение шрифта обычно приводит к одновременному изменению нескольких слов. Поэтому для слов мы будем использовать строки. Оставшаяся часть процедуры do_text_lineкасается просмотра входных данных, однако она достагочно проста и не требует вспомогательных абстракций.
Процедура do^commandдолжна отыскивать команду во входном потоке и отделять ее от оставшейся части строки. Основной вопрос здесь заключается в том, должна ли процедура распознавать команду или просто передавать строку для обработки ее операцией из doc.В данном случае подходит любое из решений. Однако по мере добавления дополнительных команд, возможно, потребуется синтаксический анализатор (поскольку команды могут иметь аргументы). Это анализ удобно возложить на процедуруdo_command,поскольку docжелательно освободить от подобных функций. Следовательно, процедура do.commandдолжна распознавать команды. Затем она может вызывать из docнеобходимые операции. К ним относятся breakJine, set_fillи seLnofill.
Спецификация docприведена на рис. 13.9.Диаграмма модульной зависимости показана на рис. 13.8.
13.7. Абстракция doc
На данный момент абстракция docявляется единственным кандидатом, поэтому мы можем приступить к рассмотрению ее представления. Для выбора представления необходимо решить следующий вопрос: выдавать ли отформатированный текст построчно или же целиком после полной обработки? Поскольку дожидаться конца обработки нет необходимости, мы выберем построчный подход. Это также позволит соблюсти предъявляемые кформатиров-щику пространственно-временные ограничения.
Проектирование
doc = data type is add.word, add.space, add_tab, add.newline, break.line,
skip-line, set-till, set-nofill, terininate
requires
фактический выходной поток, связанный с аргументом outs операции create, не должен модифицироваться вне doc в период времени между созданием объекта doc и моментом прекращения к нему доступа,
Описание
Doc представляет собой абстрактное выводное устройство, выдающее форматированный текст (как было определено в спецификации для format), соот" ветствующее информации, передаваемой ей через операции. Объект doc связан с выходным потоком, передаваемым к операции create. К моменту возврата из операции terminates все выходные данные должны быть помещены в этот поток.
Операции
create = proc (outs: stream) returns (doc)
requires can-write (outs). modifies outs
effects Готовится создать документ для outs, начиная со страницы в режиме с заполнением.
add-word == proc (d: doc, w: string) requires d не был закрыт. modifies d effects Добавляет w к d.
add. space = proc (d: doc) requires d не был закрыт. modifies d effects Добавляет к d пробел.
add. tab = proc (d: doc) requires d не был закрыт. modifies d effects Добавляет к d символ табуляции.
ad<Lnewline = proc (d; doc) requires d не был закрыт. modifies d effects Добавляет к d символ конца строки.
skip. line == proc (d: doc) requires d не был закрыт. modifies d effects Добавляет к d пустую строку,
break- line =prec (d: doc) requires d не был закрыт. modifies d effects Добавляет к d символ перевода строки.
set_fill == proc (d: doc) requires d не был закрыт. modifies d
effects Добавляет к d символ перевода строки и производит переход в режим с заполнением.
288 Глава 13
Проектирование
set-nofill = proc (d: doc) requires d не был закрыт. modifies d
effects Добавляет к d символ перевода строки и производит переход в режим без заполнения.
terminate = proc (d: doc) reduires terminate (d) до этого не вызывался. modifies d
effects Заканчивает запись информации из d в связанный с ней выходной поток, а затем закрывает этот поток.
end doc
Эффективность. Время = порядок (п), где п есть число символов, записываемых в выходной поток. Дополнительная память == порядок (п). Временный используемый объем памяти гораздо меньше, чем порядок (п).
Рис. 13.9. Полная спецификация для doc,
Представление должно содержать по крайней мере следующее: связанный с ним выходной поток, номер текущей страницы, номер текущей строки, текущий режим (с заполнением или без) и буфер. Буфер необходим по причине того, что вывод не может быть всегда построчным: в режиме с заполнением перед выводом необходимо набрать полную строку символов.
Абстракция docдолжна выполнять две различные функции! обрабатывать строки и организовывать строки в страницы. Большинство сложностей связано с обработкой строк, а именно заполнением и выравниванием строки в режиме с заполнением. Введение абстракции lineпозволит изолировать обработку строки. Инкапсуляция этих подробностей позволит нам не рассматривать их в реализации doc.
Мы можем изолировать подробности обработки на нескольких различных уровнях. К одной из возможностей относится абстракция высокого уровня, которая выравнивает и выводит строку автоматически после ее заполнения. Такая абстракция должна быть информирована о текущем установленном режиме (с заполнением или без), а также знать максимальную длину строки. Отметим, что вывод строки не может быть изолирован полностью; абстракцию docнеобходимо информировать о выводе строки с тем, чтобы она могла позаботиться о форматировании страницы. Альтернативный подход предполагает наличие абстракции более низкого уровня, следящей за словами и пробелами между ними, но не выполняющей выключку строк. Однако подобная абстракция дает небольшие преимущества по сравнению с простым хранением информации в массиве. Мы воспользуемся абстракцией строкиl.ne—третьим подходом, промежуточным по отношению к двум т)лько что рассмотренным. Этот тип будет обеспечивать выключкуi.;рок, но только по команде. Информация о текущем режиме обработки и длине строки будет сохраняться в doc,а не в line.Это решение кажется подходящим, поскольку эта информация от-
носится к документу в целом, т. е. к информации на уровне доку-. мента. Отметим, что мы добились неплохого распределения проблем: абстракция lineзаботится обо всем, что касается строк, а абстракция docзаботится обо всем том, что касается форматирования документа.
Такая абстракция промежуточного уровня потенциально более подходяща для будущих модификаций: например, в том случае, если мы захотим изменить размер левого края или подготавливать для вывода сразу несколько строк. Эти возможности могут быть реализованы отделением абстракции строки lineот физических строк. В частности, вывод строки должен происходить не автоматически и не включать в себя отступ от левого края.
Для определения операций для lineмы должны рассмотреть реализации операций для doc.Для обработки пробелов и символов табуляции будут полезны line§add-spaceи line$add_tab.Однако большинство взаимодействий между абстракцией lineи docпроисходит в doc$add_word.Приблизительная схема этой операции имеет следующий вид:
if режим с заполнением и на строке нет места для слова
then выровнять и вывести строку end добавить слово к строке
Имея эту схему, мы можем определить необходимые операции. Нам нужны операции для добавления слова к строке и для выяснения того, может ли данное слово поместиться на строке. Необходимо также уметь выровнять и вывести строку. Выравнивание и вывод должны быть отдельными операциями с тем, чтобы в дальнейшем мы могли, например, произвести перед выводом выравнивание сразу нескольких строк. Операция вывода должна уметь осуществлять запись строки прямо в выходной поток, не возвращая при этом символьное представление, поскольку для отдельных представлений строк это более эффективно (например, если представление не есть строка символов).
Выравнивание должно выполняться способом, позволяющим исключить скопления пробелов в тексте в виде вертикальных полос. Поскольку такие скопления относятся к смежным строкам, то мы должны предусмотреть это в doc.Не всегда имеется возможность добавлять пробелы равномерно, поэтому некоторые слова будут отстоять друг от друга на большее число пробелов, чем другие. Мы можем избежать скоплений пробелов, добавляя их в последующие строки поочередно то слева, то справа. Поэтому в представлении для docдолжна поддерживаться информация о том, куда помещаются пробелы для текущей строки. Для каждой новой страницы мы можем начинать добавлять их справа.
Последний вопрос касается создания объектов типа «строка». Очевидно, что для начала нам необходима операция create.После
10 Лисков Б,, Гатэг Дж.
290 Глава 13
line = data type is create, add. word, add.space, add.tab, length, justify, output,
clear
Описание
Line представляет собой изменяемую строку текста, состоящую из символов, соответствующих словам, символам табуляции и пробелам. Операция justify выравнивает строку по правому краю на заданную длину, а операция output записывает строку в выходной поток.
Операции
create == proc ( ) returns (line) effects Возвращает новую, пустую строку.
add. word = proc (1: line, w: string)
modifies I effects Добавляет символы из w к концу I,
add_ space = proc (1: line)
modifies I effects Добавляет пробел в конец 1.
add_tab = proc (1: line) modifies I
effects Добавляет к концу 1 символ табуляции. Это предполагает добавление к концу 1 от 1 до 8 пробелов. После этого длина (I post) mod 8 = 1.
length = proc (1: line) effects Возвращает число символов в 1.
justify = proc (1: line, len: int, from-right: bool)
modifies I
effects Выравнивает 1, как это было описано в спецификации format так, чтобы в 1 содержалось len символов, если это возможно. Выравнивание производится добавлением дополнительных пробелов справа от крайней правой метки табуляции. Пробелы добавляются по возможности равномерно, начиная с правого края, если from-right имеет значение true, и слева — в противном случае, Если выравнивание невозможно (например, если в 1 только одно слово), то 1 оставляется без изменений.
= proc (1: line, outs: stream) requires can-write (outs). modifies outs effects Переписывает символы из 1 в outs.
clear == proc (1: line) modifies I effects Повторно инициализирует 1, очищая ее.
end line
Эффективность. Добавляемое пространство имеет порядок (п), где п есть число символов в строке. Время, расходуемое на выравнивание и вывод, должно быть порядка (п). Время, расходуемое на другие операции, должно быть постоянным,
Рис. 13.10. Спецификация для line.
вывода строки мы можем создать для следующей строки другой объект, но такой подход бесполезен, поскольку он приводит к накоплению «мусора» (старых строк), хранить которые нет не-
output
лДрмктирование
% Doc осуществляет вывод построчно, используя буфер, представляющий % объект из одной строки. '" Текущая строка
гер = record [line: line,
fill: bool, from-right: bool,
lines- per- page
lineno: int, pageno: int, outs: stream] 50
% Истинно в режиме с заполнением % Истинно, если дополнительные про-% белы добавляются справа % Число строк на текущей странице % Номер текущей страницы % Выходной поток
% Хранится в виде константы, поэтому легко % может быть изменено
% Типичный объект doc представляет собой последовательность символов % из всех обработанных строк, завершаемую символами из текущей строки. % Функция абстракции имеет следующий вид: % Символы в d.outs li символы в d.line % Инвариант представления есть can-write (г.outs) & I <r.lineno <= lines-per-page & 0 < = r .pageno & r .page- no ;> 0 ==>
Текущая страница имеет соответствующий заголовок $ r.line-no строк, не считая заголовка. &у предыдущих страниц р (т, е. страниц с номерами 1, .,., r.page-no
р имеет соответствующий заголовок & р имеет lines-per-page строк, не считая заголовка & за р следует символ перехода к следующей странице.
% Внутренняя процедура output-line, ответственная за форматирование стра-% ницы по мере обработки строк. output- line = proc (d: rep)
modifies d
effects Записывает d.line и символ конца строки в d.outs, увеличивает на единицу d.lineno и d,pageno, а также заботится о заголовках и переходах к следующей странице. Рис. 13,11. Реализация документации для абстракции doc.
обходимости. Лучше инициализировать и использовать старый объект повторно. Это позволяет сделать операция clear.Спецификация для lineприведена на рис. 13.10.
Операция doc$add_wordне является единственной операцией в doc,позволяющей вводить строки. Это могут делать также операции addJine, break_line, skip-lineи terminate.Вывод строк предполагает также форматирование страницы. Для выполнения этих функций удобна процедура output_line.Поскольку вне реализации docпроцедура output_lineвряд ли окажется полезной, она может быть внутренней по отношению к реализации. Это означает, что мы не добавляем ее к диаграмме модульной зависимости, а проектируем ее реализацию как часть реализации doc.
Процедура output_lineответственна за все операции форматирования, поэтому мы должны принять решение относительно того, как следить за номерами страниц и выявлять необходимость перехода к новой странице. Разумно хранить номер текущей
10*
292 Глава 13
Рис. 13.12. Диаграмма модульной зависимости с добавленным модулем line.
страницы и число строк в ней в самом представлении. При получении всех строк для страницы мы можем начать готовиться к следующей. Однако генерировать символ перехода к следующей странице и заголовок этой страницы необходимо только в том случае, если следующая страница не пуста.
Реализация абстракции docприведена на рис. 13.11.Отметим, что приводится не только представление, инвариант представления и функция абстракции, но также и спецификация внутренней программы output „line.Расширенная диаграмма модульной зависимости приведена на рис. 13.12.