
- •10. Написание формальных спецификаций
- •Глава 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. Выбор подхода
14.1.2. Структура
При анализе проекта с точки зрения его структурности наиболее важным является оценка соответствия границ его модулей. В связи с этим возникают два важных вопроса. 1.Удалось ли нам выявить абстракцию, обеспечивающую лучшую модуляриза-цию? 2.Не оказалось ли объединенным то, что относится к разным модулям? Единого правила для ответа на этот вопрос не существует. Все, что мы можем сделать, —это привести список симптомов, возникающих при неудачной модуляризации- программы. Мы сконцентрируем внимание на локальных симптомах, т, е. проблемах, которые могут быть обнаружены путем просмотра одного модуля или интерфейса между двумя модулями.
Когерентность процедур. Каждый модуль в проекте должен представлять собой единую связную абстракцию. Эта связность может быть проанализирована просмотром спецификации процедуры. Процедура может выполнять над своими аргументами единичную абстрактную операцию. (Это касается такие и итераторов. Итератор отображает входные данные на последователь-
Этап перехода от проектирования к реализации
ность элементов. Это отображение может быть единичной абстрактной операцией.)
Некоторым процедурам описанная связность не присуща. Их внутренняя организация определяется некоторым произвольно определенным скобочным механизмом. В начале «эпохи структурного программирования» многие люди не могли понять, что хорошая структура программы —это главным образом семантическое понятие. Они искали простые синтаксические определения «хорошей структурированности». Большинство подобных упрощенных дефиниций определяло в качестве размера для процедур произвольную верхнюю 'границу, например одну страницу. Другой пример произвольных ограничений на размеры относится к программам, которые должны сами распределять себе память и которые делятся на модули для облегчения оверлеев. Такие произвольные ограничения часто ведут к программам с полным отсутствием связной когерентной структуры.
Вторая причина отсутствия внутренней связности обусловлена ручной оптимизацией программ. Внимательный программист может заметить, что некоторая группа операторов повторяется несколько раз. В попытке сократить объем используемой памяти программист может объединить эти операторы в процедуру. Однако в дальнейшем подобные оптимизации становятся непродуктивными, поскольку они затрудняют модифицируемость программы.
Имеется два надежных признака полного отсутствия внутренней связности процедуры. Если кажется, что спецификация процедуры лучше всего определяема через описание ее внутренней структуры (т. е. как она работает), то подобная процедура скорее всего бессвязна. Второй хороший критерий подобной связности —• это нахождение подходящего имени для процедуры. Если самое лучшее, что мы могли найти, это «процедураЬ, то скорее всего, что созданный проект неудачный. Если очевидная связь с абстракцией процедуры отсутствует, то необходимо пересмотреть проект с целью исключения данной процедуры.
К числу простейших способов введения внутренней связности относитсяконъюнктивная когерентность.Она задается спецификацией вида А &В &С &...
Конъюнктивная когерентность обычно имеет место там, где последовательность произвольных последовательно выполняющихся действий объединяется в одну процедуру. Типичным примером может служить абстракция, назначение которой заключается в инициализации всех структур данных. Спецификация такой абстракции может быть представлена конъюнкцией:
инициализировать А &инициализировать В & ...
306 Глава 14
Отметим, что подобная структура может усложнить идентификацию абстракций данных, поскольку часть работы для- каждого типа берет на себя одна процедура'.
Для окружения, в котором затраты на обращения к процедурам неоправданно велики, конъкжктивяая когеренттаость может оказаться полезной, поскольку она исключает ряд обращений к процедурам. Однако поскольку между выполияемыми действиями имеется жесткая логическая связь, то процедуры рекомендуется не объединять. Чем больше мы объединяем в одной процедуре, тем труднее ее отлаживать и сопровождать. Далее, при сопровождении программы воз-можны случаи, в которых необходимо выполнить только некоторое подмножество действий —членов конъюнкции. В этом случае разумней всего расчленить исходную процедуру. Вместо этого часто добавля.гот новую ггроцедурную абстракцию. Это ведет к увеличению объем-а и работы программы, а также к дополнительной отладке.
Дизъюнктивная когерентпость задается спецификацией в предложении effects в виде
А 1 В 1 ...
часто под видом конструкции if-then-else или конъюнкции импликаций. Устойчивая процедура содержит дизъюнктивную когерент-ность, отделяя нормальный возврат от исключительных ситуаций. Однако если спецификация ситуации при нормальном возврате из процедуры содержит дизъюнкцию, то такой случай требует разбора. Рассмотрим спецификацию на рис. 14.1. Абстракция geLend
geLend =: proc(a: iliLlist, j: int) returns (i: int) signals (empty)
requires 0 < j < 3
effects if size (a) < I then signals empty else if j == I then i == first: (a) else if j == 2 then i = last (a)
Рис. 14.1, Пример дизъюнктивной когерентности.
может сигнализировать об исключительной ситуации empty или же осуществить нормальный возврат, выполнив одно из двух различных действий. Каждое из этих двух действий может быть рассмотрено как самостоятельная абстракция, т. е. мы могли 'бы иметь две абстракции, как это показано на рис. 14.2. Объединение этих двух процедур имеет несколько недостатков и ни одного достоинства. Во-первых, обращение вида geLend (а, 1) труднее для понимания, чем ge.t_first (а). Во-вторых, возможно возникновение нового класса ошибок,, например обращение вида geLend (а, 3). В-третьих, программа, использующая geLend, менее эффективна, чем программа,, использующая две абстракции, приведенные на рис. 14.2. При любом обращении к geLend пользователь знает, какая из процедур необходима. Однако эта ин-
Этоп перехода от проектирования к реализации
формация должна быть закодирована во втором аргументе обращения, а процедураget.endдолжна проверять этот аргумент, определяя требуемую операцию. Эта дополнительная работа требует дополнительной памяти и времени. Мы можем также реализовать процедуру geLendпри помощи дополнительных абстракцийgeLfirstиgeLlast, этим увеличив число обращений к процедурам.
get. firsts proc(a: int. list) retunis (i: int) signals (empty) effects если 'size (a) < 1, то сигнализирует через empty; в противном случае i = .first (а)
get-last = proc(a: int-list) returns (i: int) signals (empty) effects если size (a) < 1, то сигнализирует через em.pty; в противном случае i = last (а)
Рис. 14.2. Две когерентные процедуры.
Дизъюнктивная когерентность возникает при попытке неразумного обобщения абстракций. Если проект программы содержит две или более сходные программы, то всегда разумно рассмотреть возможность замены их одной, более обобщенной абстракцией. При удачном решении подобное обобщение позволяет сократить размер программы и объем работы .программиста, при этом скорость выполнения или сложность реализации возрастут незначительно. Однако, если результатом явится дизъюнктивно когерентная абстракция, подобную замену лучше не производить.
Процедураjustifyв проекте процедурыformatдля типаTineявляет пример удачного обобщения. Мы могли -бы иметь две процедуры,justify-fromJeftиjustify-from-right, однако это. присело бы только к увеличению размера программы -и усложнению работы. Хотя в спецификации процедурыjustifyи имеется дизъюнкция, она является чересчур скрытой. Большая часть спецификации и большая часть программы не зависит от значения аргументаfrom_right.
Обобщение, которое мы не сделали, предполагало объединение процедурadd.word,add-spaceиadd-tabв одну, например':
add-token =J)roc(l: line, t: token)
гдеtokenесть соответствующий тилone-of. Это не дало бы экономии на тексте программы >и вьгиюданялосъ 'бы 'медленздее,'чеытри отдельные операции.
Наличие избыточной дизъюнктивной 'когерентасстн •свидетедь-ствует о неудаче в выборе абстра-к-ций даннчнгх Т1роекта. В таких случаях комбинация нескольких различных функций в одну [процедуру может быть попыткой изолировать представление •информации, которое должво-быть выделено в отсутствутащенii-UTie. •Фактически при реализаиин 'некоторого типа в отдел ь'йон иротодуре необходимо указание дополнительных аргументов для его расио-знаса-ния.
308 Глава 14
Когерентность типов.Каждая операция над типом должна быть когерентной процедурой или оператором. В дополнение к этому тип должен поддерживать такую абстракцию, которую пользователи могли бы рассматривать как набор значений и близко связанный с этими значениями набор операций. Одним из способов оценки когерентности типа является проверка каждой операции на принадлежность к требуемому типу. Как говорилось в гл. 4, тип должен бытьадекватным,т. е. он должен поддерживать достаточное число операций с тем, чтобы его применение было действительно эффективным. В плохо сконструированных типах операции часто оказываются слабо связанными с соответствующей абстракцией, а реализация не дает особых преимуществ прямого доступа к представлению. В общем случае такие операции из типа лучше удалить. Чем меньше операций имеет доступ к представлению, тем легче при необходимости модифицировать это представление.
Рассмотрим, например, стековый тип, содержащий операциюsqrt.top:
sqrt_top==proc(s: stack) returns (i: real) signals (empty)
effects if size (s) = 0, signals empty; otherwise i == sqrt (top (s)).
Операцияsqrt.topне имеет прямого отношения к стенкам и большая часть ее реализации не зависит от представления стеков. Следовательно, эта операция должна быть удалена из абстракции стека.
Коммуникациимежду модулями. Тщательный анализ объема и вида информации, которой модули обмениваются между собой, позволяет обнаружить серьезные просчеты в проекте. В данной книге мы придавали большое значение специализированным интерфейсам узкого профиля: модуль должен иметь доступ только к той информации, которая необходима для его работы. ЯзыкCLUсоздавался ориентированным именно на интерфейсы узкого профиля, например он не разрешает процедурам ссылаться к глобальным переменным, однако возможность передачи большого объема информации к модулю сохраняется.
Если тип не был идентифицирован, то модулю может быть передано слишком большое количество информации. В отсутствие типа все модули, которые должны взаимодействовать в терминах абстрактных объектов, взаимодействуют в терминах своих представлений. Результатом является то, что модули имеют интерфейсы, большие необходимых. Вместо того чтобы быть связанным только через тип, они располагают также информацией о том, как этот тип реализован. Отметим, что, помимо самой реализации, это включает в себя функцию абстракции и инвариант представления. Также заметим, что все используемые модули должны рас-
' Этап перехода от проектирования к реализации
сматриваться с той точки зрения, что реализация (отсутствующего) типа является правильной. Далее, если реализация типа изменяется, то должен изменяться также и каждый модуль.
Даже если все необходимые типы были определены и реализованы через свои собственные кластеры, по-прежнему некоторые интерфейсы могут быть большими, чем необходимо. Хорошо спроектированные программы часто содержат типы с большим количеством информации. Некоторым модулям доступ ко всей этой информации не требуется. Однако для большинства задач удобна передача абстрактного объекта целиком, особенно если это легко выполнимо. Например, мы можем иметь типstudenLre-cord, который помимо прочих параметров включает в себя имя студента, номер в системе социального страхования и адрес места жительства. Процедуреprint_address, печатающей адрес, необходимо только имя студента и адрес места жительства. Такой процедуре не требуется передавать всю запись о студенте. Вместо этого вызывающая программа может извлечь необходимую информацию из этой записи непосредственно. Это разумно сделать по следующим соображениям:
1.Если print_addressпередается объекту типаstudent.record, то его реализатор должен будет знать, как извлечь необходимую информацию, т. е. какие операции необходимо вызвать. Если спецификацияstudenLrecordизменилась, то реализацияprint'addressтакже может измениться. Ничего этого не требуется, если необходимая информация передается явно.
2.Реализация print_addressможет содержать ошибку, которая приведет к изменениям в записиstudent_record. Ошибки такого рода довольно трудно обнаружить.
3.Если запись student_recordесть запись типаrecordиз языкаCLU(или структураstruct), а не некоторая созданная абстракция записи, то при любом добавлении к этой записи дополнительных полей процедуру print_addressнеобходимо компилировать заново.
14.2. Последовательность разработки программы
В данной книге акцент делался на стратегию разработки программы, ориентированную сверху вниз. После того как мы увидели, что разработка программы представляет собой итеративный процесс, было показано, что каждой итерации, спецификации и проектированию должна предшествовать реализация. Процесс перехода от проектирования к реализации еще не рассматривался. Основной выбор делается между стратегией сверху вниз и стратегией снизу вверх.
Традиционный подход к разработке ориентирован снизу вверх. При таком подходе мы реализуем и тестируем все модули, используемые модулем М, до того, как реализуем и тестируем модуль М.
310 Глава 14
Рассмотрим, например, реализацию проекта с диаграммой модульной зависимости, приведенной на рис. 14.3.Мы можем начать с реализации и тестированияDи Е. Затем можно реализовать и оттестировать модули В и С. При тестировании В и С мы можем использовать Dи Е, тем самым избегая написания заглушек. Это означает, что тестирование отдельного модуля уже не используется. Наконец, мы можем реализовать и оттестировать А. Заметим, что предложенный порядок снизу вверх не является единственным. С тем же успехом можно использовать порядок D, В, Е, С, А.
При разработке сверху вниз мы реализуем и тестируем все модули, использующие модуль М, до реализации и тестирования модуля М. Возможные последовательности сверху вниз включают в себя А, В, С, D,Ей А, С, Е, В, D.Подход снизу вверх уменьшает зависимость от заглушек, а подход сверху вниз уменьшает зависимость от драйверов. Весьма важно, чтобы подход сверху вниз сопровождался тщательным тестированием каждого из модулей в отдельности. Если мы проверяем модуль В только по отношению к использованию его модулем А, то мы сможем проверить только часть поведения модуля В. В дальнейшем, при переходе к модулю А, в В могут быть обнаружены новые ошибки. Следовательно, если мы выбираем модуль А в качестве драйвера для В, то мы должны быть уверены, что модуль А проверяет модуль В полностью.
Ни один из подходов не имеет безусловных преимуществ над другим. Может показаться, что большей частью предпочтительней использовать подход сверху вниз, однако могут найтись причины, по которым подход снизу вверх окажется более подходящим. Мы рекомендуем использовать смешанную стратегию, по которой одна часть системы разрабатывается с использованием одного подхода, а часть —с использованием другого.
Разработка сверху вниз обладает тем преимуществом, что она позволяет обнаружить серьезные ошибки на ранних стадиях проектирования. При тестировании модуля проверяется не только его реализация, но и спецификации модулей, им используемых.
Рис. 14.3. Простая диаграмма модульной зависимости.
Этап перехода от проектирования к реализации
Вычислигль налоги.
Бычислигт доходы Вычислить скидки. Вычислить чистый- «"
/\ 1 \ доход
Зар-уЛитпчтым Вложения Сгландартюю Потатейные '"
Процемп Прочие
Рис. 14.4. Диаграмма модульной зависимости для программы подсчета налоговых отчислений.
При подходе снизу вверх большое количество времени было бы потрачено на реализацию и тестирование модулей, оказавшихся в дальнейшем бесполезными в связи с проблемами, возникшими при проектировании одного из их предшественников согласно диаграмме модульной зависимости. Аналогичная проблема может произойти при подходе сверху вниз, если мы обнаружим, что некоторый жизненно важный модуль не может быть реализован вовсе или с приемлемой эффективностью. Опыт, однако, показывает, что такая ситуация возникает сравнительно редко. Это может произойти по причине того, что абстракции низкого уровня часто схожи с ранее создаваемыми задачами, а абстракции высокого уровня часто склонны к идеосинкразии.
При подходе сверху вниз всегда имеется возможность работать одновременно только с одним модулем. Мы просто заменяем заглушку имитируемым ею модулем. С другой стороны, при подходе снизу вверх мы, наоборот, стараемся объединить одновременно несколько модулей. В большинстве случаев один модуль на более высоком уровне соответствует не одному, а нескольким драйверам. Например, если модуль А помещается в программу на рис. 14.3,то он заменит драйверы для В и С.
Поскольку интеграция системы происходит более плавно, если мы добавляем к ней за один раз по одному интерфейсу, то с этой точки зрения разработка сверху вниз имеет большие преимущества.
Разработка сверху вниз также увеличивает схожесть между различными версиями разрабатываемой программы. Предположим, что программа, подсчитывающая налоговые отчисления, имеет (частичную) диаграмму модульной зависимости, приведенную на рис. 14.4.Разработка сверху вниз позволяет создать для нее
312 Глава 14
много полезных частных реализаций. Например, для многих ока' жется полезной система без процедурinvestmentиitemized.
Даже если неполные частные версии системы и не могут быть продуктивно использованы, их создание на начальной стадии проектирования имеет ряд преимуществ. Во-первых, это позволяет выявить возможные проблемы, связанные с требованиями к программе. Более того, при работе над долгосрочными проектами создание промежуточных неполных версий системы положительно сказывается на моральном состоянии как заказчика, так и разработчика.
Хотя разработка снизу вверх и отдаляет момент создания частной рабочей версии, она позволяет получить на ранней стадии полезные подсистемы. Эти подсистемы обычно имеют более широкую применимость, чем полученные при подходе сверху вниз неполные системы. В частности, это верно для подсистем низкого уровня (например, подсистем ввода—вывода). В ситуации, связанной с разработкой нескольких взаимосвязанных систем, полезно создавать разделяемые подсистемы, используя подход снизу вверх. Более того, иногда оказывается легче построить подсистему низкого уровня, чем имитирующую ее заглушку.
Другим потенциальным преимуществом подхода снизу вверх является меньшая, чем при разработке сверху вниз, зависимость от машинных ресурсов. Особенно это касается ранней стадии процесса разработки. Разработка сверху вниз поглощает больше машинного времени, поскольку на различных этапах отлаживается большая часть системы. Обычно производится создание полных тестов системы, запускаемых всякий раз при добавлении к системе нового модуля. Такие тесты могут помимо добавляемого нового модуля проверять работу многих других частей системы, а это отнимает много машинного времени и памяти, не являющейся на данном этапе необходимой. С другой стороны, подобный систематический подход к тестированию более последователен и позволяет обнаружить больше ошибок, нежели подход adhoc.
Еще одно преимущество разработки снизу вверх заключается в том, что она допускает работу при отсутствии отдельных машинных ресурсов. Рассмотрим создание системы, предназначенной для работы на ЭВМ с объемом памяти 512К. Если эта машина еще не получена, но имеется похожая машина с объемом памяти в 128К, то у нас имеется возможность выполнить значительный объем работы до того, как вопрос с памятью станет проблематичным. При подходе сверху вниз эта проблема скорее всего возникнет гораздо раньше..
При разбиении проблемы на части, соответствующие подходам снизу вверх и сверху вниз, необходимо учитывать как технические, так и нетехнические аспекты. Иногда для реализации необходимо приложить большие усилия до того момента, как будет сформи-
Этап перехода от проектирования к реализации
рована подходящая группа программистов. В таких случаях лучше всего использовать подход сверху вниз. Поскольку диаграммы модульной зависимости всегда сужаются кверху, то в процессе разработки необходимость в программистах увеличивается. С другой стороны, если группа программистов уже набрана, то подход снизу вверх может оказаться более предпочтительным.
В общем случае мы рекомендуем смешанную стратегию, при которой предпочтение отдается подходу сверху вниз. При работе по данной стратегии должны приниматься во внимание как технические, так и нетехнические факторы. Важно отметить, что стратегия развития должна быть явно определена до начала реализации. Некоторые выводы относительно порядка реализации и тестирования могут быть получены анализом программыformat:
1.Вероятно, что процедурыformatиdeclineлучше всего реализовать в первую очередь. Их легко реализовать, и обе они могут быть использованы в качестве драйверов для проверки абстракций более низкого уровня. Тестирование их также легко, поскольку заглушки дляdo.text.lineиdo_command_line, а также операцииcreateиterminateдолжны лишь печатать аргументы, с которыми они вызываются.
2.Затем, возможно, лучше реализовать процедурыdo_com-mandиdo_text_line. Их также легко тестировать, поскольку заглушки для процедур, к которым они обращаются, должны только печатать их аргументы.
3.К этому моменту у нас остается только две абстракции — docиline. Строгий подход сверху вниз обязует нас реализовывать в первую очередь абстракциюdoc. Против такого решения имеются два соображения. Во-первых, большинство процедур из абстракцииdoc(например,add.word) не делает ничего, кроме обращения соответствующих процедур изline. Во-вторых, абстракциюdocтрудно будет проверить без командline$clearиline$output, а написание заглушек для их замены может оказаться не легче их фактической реализации. По этим причинам, вероятно, будет разумнее реализовывать части абстракцийdocиlineодновременно. Для каждой из них предпочтительно начать с операцийcreate,add.word,add.spaceиadd_tabплюсline$clearиlineSword. Эти типы можно реализовать поэтапно, поскольку их представления и инварианты представления уже были выбраны.
4.Мы завершаем реализацию, заканчивая сначала разработку абстракцииdoc, а затемline. РеализацияlineSjustify, вероятно, будет последней операцией. Это самая сложная по реализации и тестированию процедура. Ее тестирование будет гораздо легче, если остальная часть программы будет завершена.
314 Глава 14
14.3. Взаимосвязь абстракции и эффективности
Как мы говорили, хорошо выбранные абстракции упрощают программирование, тестирование, отладку и сопровождение системы. О связи абстракций с эффективностью программы говори» лось мало.
В большинстве случаев хорошо выбранные абстракции оказывают положительное влияние на эффективность. Они значительно облегчают проектирование эффективных алгоритмов. Более того, увеличивая модуляризацию программ, они облегчают модифицирование. Это в свою очередь позволяет сократить узкие места и более легко реагировать на изменения в требованиях к эффективности. Однако введение абстракций порой оказывает на эффективность обратный эффект. Примером может служить серьезная проблема, возникающая в процедуре, которая может захватить доступ к некоторому типу. В этом случае не остается ничего другого, как переделать проект.
Менее серьезные проблемы возникают в тех случаях, когда применительно к используемому языку программирования связанные с использованием абстракций затраты непомерно велики. Мы рекомендуем такой стиль программирования, который ведет к программам с большим числом процедурных вызовов. Обращение к процедуре всегда подразумевает некоторые временные издержки. Их размер зависит от используемого языка программирования и компилятора. В ряде языков программирования (языкCI.LJк ним не относится) на обращение к процедуре всегда тратится некоторое время. Даже если использование процедурных вызовов некритично, возможны временные издержки, связанные с внесением избыточных вычислений. Проблема заключается в том, что при некотором конкретном употреблении процедуры используется информация, не требующаяся в других ее применениях. Например, на рис. 14.5проверка ~stack$empty(s) в теле Р избыточна при обращенииQк Р.
Q •== ргос...
if — stack$empty (х) then Р (x) end
endQ
Р = ргос (s: stack) if "" s•tack$emptу (x) then x: int :== stack$pop (s) % использовать x и возвратиться end end?
Рис. 14.5. Процедурное обращение к Р из Q.
Этап перехода от проектирования к реализации
315
Решением этой проблемы является оптимизация, называемаявнутренней подстановкой.Выполнение такой подстановки означает замену обращения к процедуре телом самой процедуры. В эту процедуру входит замена аргументов в теле процедуры фактическими параметрами вызова. Если тело процедуры содержит локальные идентификаторы, конфликтующие с идентификаторами в вызывающем окружении, то они должны быть переименованы. На рис. 14.6показан результат применения внутренней подстановки для программы на рис. 14.5.При работе с программой на рис. 14.6достаточно хороший компилятор удалит избыточные коды.
Q = ргос...
stack$empty (x) then % начало тела процедуры Р if - stack$empty (x) then P. x: int := stack$pop (x)
% использовать P.x end % конец тела процедуры Р
Рис, 14.6. Замена обращения к Р на ее тело.
Некоторые компиляторы осуществляют внутреннюю оптимизацию автоматически, однако большинство компиляторов этого не делает. Поскольку внутренняя подстановка делается на уровне исходного текста программы, то она может быть выполнена программистами. Ручная оптимизация такого рода полезна для критических частей программ небольшого размера. Попытка использовать это более широко не рекомендуется. Если это все-таки необходимо, то предлагается написать программу, выполняющую такую операцию автоматически. К сожалению, такие программы довольно сложно писать.
14.4. Заключение
В данной главе мы рассмотрели ряд проблем, возникающих на конечном этапе процесса проектирования и при переходе к реализации. К числу наиболее важных задач относится проведение систематической оценки проекта и разработка точного плана выполнения реализации и тестирования модулей, составляющих систему.
316 Глава 14
При анализе проекта необходимо рассмотреть, обеспечивают ли его реализации необходимое поведение и эффективность, а также возможность относительно легкой реализации, тестирования и поддержки предложенной структуры программы. Мы предложили проводить обзор проекта трассированием его некоторым набором символических данных. Был также предложен ряд критериев, которые могут быть использованы при оценке аспектов структуры. Все эти критерии были связаны с границами областей действия модулей.
Мы не приводили жестких правил выбора порядка реализации и тестирования. Были также рассмотрены относительные преимущества принципов разработки и тестирования снизу вверх и сверху вниз. Был сделан вывод о том, что наилучшим решением является смешанная стратегия с предпочтением, отдаваемым подходу сверху вниз.
В обсуждении анализа проектирования был предложен очень краткий анализ задачи создания форматировщика текста, разработанного в гл. 13.Читатель не должен делать вывод о том, что данный анализ должен проводиться только после завершения этапа проектирования. Для больших программ очень важно проводить тщательный анализ на каждой стадии проектирования. Также важно начать анализ того, каким образом организовывать работу на этапе реализации. На принятие решений значительную роль может оказать необходимость в раннем завершении подсистем.
Дополнительная литература
Lampson, Butler W., 1984. Hints for computer system design. IEEE Software 1(1): II—28. Myers, G. J., 1979. № Art of Software Testing. New York: John Wiley &
Sons.
Упражнения
14.1. Выполните обзор проекта некоторой разработанной вами программы. Убедитесь в том, что вы вьючили в него рассмотрение структуры и модифици' руемости программы, а также анализ ее корректности. 14.2. Определите стратегию реализации созданной вами программы. 14.3. Один из студентов предложил заменить в проекте задачи format строковые операции justify, clear и output одной операцией, выполняющей все три указанные функции. Второй студент возразил, что это ведет к ухудшению когерентности программы. В «ч это выразится? В чем преимущества и недостатки слияния этих трех операций или слияния только операций clear и output?
14.4. Предположим, что при разработке задачи format (гл. 13) мы ввели тип данных command, коюрый содержит в себе информацию о командах, включая их формат и назначение. Мы можем затем изменить абстракцию doc таким образом, что она будет представлена одной операцией вместо двух операций seLfill и ist.nofill. Проанализируйте эту альтернативу по отношению как к текущей
Этап перехода от проектирования к реализации
спецификации форматировщика, гак и к дальнейшим модификациям, предполагающим увеличение числа команд.
14.5. Предположим, что в проете задачи format (гл. 13) мы не ввели абстракцию line. Оцените эту альтернативу по отношению к текущей спецификации форматировщика и по отношению к дальнейшим модификациям,
14.6. Предположим, что при разработке задачи format (гл. 13) мы не использовали абстракции line и doc. Оцените подобную альтернативу. Проанализируйте когерентность проекта, а также рассмотренную в разд. 14.1,2 межмодульную взаимосвязь.
14.7. В упражнении 5 гл. 5 была рассмотрена абстракция тар. В ней имелась операция insert, добавляющая к тар строку вместе с относящимся к ней элементом, а также операция change, изменяющая элемент, относящийся к строке. Предположим, что эти две операции были заменены одной операцией, добавляю-щей элемент, если тот еще не существовал, и изменяющий его — в противном случае, Рассмотрите когерентность этой модифицированной абстракции. Сравните модифицированную абстракцию с исходной.
15. Использование других языков
В данной книге рассматривалась методология создания программ. В предложенном подходе для разработки и реализации программ используются абстракции и, в частности, типы данных. Не все языки программирования поддерживают используемые нами типы абстракций, а также не все языки поддерживают именно такие их формы. Оказавшись в такой ситуации, однако, не следует отказываться от методологии. Вместо этого надо попытаться связать методологию и абстракции. Предлагается два способа.
1.Адаптация форм абстракций таким образом, чтобы они лучше вписывались в язык.
2.Введение соглашений по реализации абстракций в языке. Оба этих метода были рассмотрены в гл. 7,где объяснялось, как использовать эту методологию вместе с языком Паскаль. Были использованы следующие методы:
1.Процедурные абстракции были разделены на не возвращавшие результатов процедуры и функции, возвращавшие единичный результат. Далее, для каждого из аргументов спецификации функций и процедур определялся способ передачи параметров (lieзначению или по ссылке).
2,Поскольку в языке Паскаль отсутствуют исключительные сшуации, мы использовали функции, возвращавшие вычисляемый тип, значение которого определяло тип завершения. Кроме этого, была введена процедураfailure, которая могла быть вызвана в ситуациях, соответствующих возникновению ситуацииfailureв языкеCLU.
3.Итераторы были заменены специальными типами, называемыми генераторами и имеющими операции начала итерации, определения факта завершения итерации и операцию возврата к следующему элементу.
4.Параметризация выполнялась при помощи редактирования текста. Параметризованный модуль составлялся в форме, которая не могла компилироваться. Каждый вариант реализации создавался при помощи редактирования текста.
5,Абстракции данных реализовывались, исходя из заданного набора соглашений. Эти соглашения определяли способ поименова-
Использование других языков
иия типа и его операций, а также способ представления объектов. Они также запрещали доступ к представлению вне фрагмента, определявшего этот тип.
6.Наконец, все программы ассемблировались исходя из набора правил размещения модулей.
Первые три метода следуют методологии языка Паскаль, в то время как оставшиеся три являются соглашениями по использованию языка Паскаль.
Аналогичный подход может быть применен к любому языку, хотя при этом будут иметься отличия в некоторых деталях. Перед рассмотрением других языков мы покажем еще один способ представления типов данных в языке Паскаль. Использованный в гл. 7подход был очень близок к языку CLU:все объекты, принадлежащие к абстрактным типам, располагаются в неупорядоченном массиве. В противоположность этому хранилищем всех объектов для большинства встроенных типов языка Паскаль является стек. Ниже будет описан подход, при котором пространство для хранения объектов абстрактных типов отводится в стеке. Такой подход пригоден только для объектов фиксированного размера. Для динамически увеличивающихся объектов нужно использовать неупорядоченный массив.
15.1. Подход с использованием стека
Предположим, что необходимо определить целочисленные наборы, содержащие максимально 100элементов. Назовем такой типintset-100. Поскольку максимальный размер набораintset-100 известен заранее, для хранения его представления можно использовать стек. Представление может иметь, например, следующий вид:
type intseL 100 == record size: 0... 100;
els: array [I ... 100] of integer end;
Здесь, так же как и в гл. 7,мы используем соглашение, касающееся определения представления через определение типа:
typeимя-типа ={определение типа представления^;
В этом случае фактическое пространство для хранения определяется справа от знака равенства. В противоположность этому в разд. 7.2 правая часть равенства содержала указатель на тип с именем typename-rep (представление типа с заданным именем), определение которого и задавало фактическое пространство, отводимое под хранилище.
Типы. для которых хранилище объектов распределяется в стеке, отличаются от 1ипов, использующих неупорядоченный массив.
320 Глава 15
Эти типы не создают и не разрушают операции, однако имеют вместо этого операцию инициализации, также все операции над стекоориентированными типами обращаются к объектам типа по ссылке.
Стекоориентированные типы не имеют операций, аналогичныхcreate, поскольку память для хранения их объектов распределяется всякий раз при анализе его объявления. Например, объявление
vart:intset.lOO;
выделяет в стеке пространство для новой переменной t.Объем распределяемого пространства зависит от типа новой переменной. В этом случае тип tуказывает на то, 410необходимо пространство для записи, содержащей целочисленную компоненту и компоненту типа массив. Разумеется, распределяемое таким образом пространство не несет какого-либо осмысленного представления, поскольку в нем не содержится никаких данных. Эта проблема разрешается введением одной или нескольких операций инициализации, цель которых заключается в инициализации йредстав-ления. Этим гарантируется ее соответствие инварианту пред-
intseL 100^ data type is intset. 100. init,intset-100_ insert, intset_100_size,
intset. 100- delete, intset- 100-member
Описание
Объекты intset-100 представляют собой изменяемые целочисленные наборы, которые содержат максимально 100 элементов. Они размещаются в стеке,
Операции
procedure intset- 100- init (var s: intset-100) modifies s effects Инициализирует s, очищая ее от элементов.
function intset-100. insert (var s; intset-100, x: integer):
intset-100- insert- exceptions modifies s
effects Тип intset-100-insert-exceptions' (normal, noroom). Если size (Spre U {x}) s> 100, то возвращается значение noroom (нет места) и s не модифицируется. В противном случае s модифицируется, включая в себя х в качестве элемента и возвращается normal (нормальный возврат),
procedure intset-100-delete (var s: intset-100, x: integer) modifies s effects s модифицируется так, что x не является ее элементом.
function intset-100-size (var s: intset-100): integer effects Возвращает число элементов в s.
function intset-100-member (var s: intset-100, x: integer): boolean effects Возвращает значение true, если x является элементом из s. В противном случае возвращается значение false.
end intset-100 Рис, 15,1. Спецификация типа intset.ICO.
Использование других языков
ставления. Например, операцияintset- 100-initвыполняет ини циализацию объектовintset-100.К сожалению, нет никаких гарантий, что операции инициализации будут когда-либо вызваны, и следовательно, никаких гарантий, что инвариант представления будет удовлетворен при вызове других операций. Эта проблема является одним из слабых мест предложенного для языка Паскаль подхода с использованием стека.
Аргументintset-100операцииintset-100-initпередается по ссылке, поскольку операцияintset_100- initдолжна модифицировать s.Остальные операции дляintset-100-также извлекают свои аргументыintset-100по ссылке, допуская этим возможность необходимых модификаций, а также по причине боль шей эффективности обращения к большим объектам по ссылке. чем по значению. Наконец, типintset-100,подобно другим стеко-ориентированным типам, не имеет операции разрушения (destroy), поскольку пространство под переменную изintset-100освобождается после объявления о возврате данной переменной.
Спецификация типаintset-100приведена на рис. 15.1.Отметим, что, хотя все операции берут свой аргументintsejt_100 по ссылке, модифицировать этот аргумент могут лишь некоторые из них. Реализация типаintset-100приведена на рис. 15.2.
Важно отметить, что такой подход может быть использован с языком Паскаль только в том случае, если размеры объектов известны на этапе компиляции. Если размеры объектов определены на этапе создания, то для языка Паскаль объекты должны быть распределены в неупорядоченном массиве, даже если их размеры не изменяются динамически.
Итерация над.стекоориентированным набором типаintset-100 может быть выполнена при помощи -типа «генератор». Этот тип сходен с типом, рассмотренным в разд. 7.4.и отличается лишь в некоторых деталях. Например, на рис. 15.3приведена спецификация генератораslOO-elemsдля объектов типаintset-100.Отметим, что, как и самintset-100, slOO-elemsскорее инициализирует,. чем создает объекты, а операция по разрушению отсутствует. Создание также производится через объявление; например,
varg:slOO-elems;
создает инициализированный объект «генератор», который должен быть затем инициализирован через обращениеslOO-elems-init. Эта процедура и функцииslOO-elems-nextиslOO-elems-doneОбращаются к объекту «генератор» по ссылке.
Отметим также, что функции slOO-elems-nextиslOO-elems-doneработают с набором как с дополнительным аргументом Это сделано в целях увеличения эффективности: в этом случае операции получают доступ к объектуintset-100.Генератор может сохранить копию данного объекта в своем представлении (slOO-
^/г II Дисков Б., Гатэг Дж.
322 Глава 15
{Определение представления для типа intset. 100.) type intset. 100 == record size: 0... 100; els: array [I ...100) of integer end; sl00_№sert_exceptions = (normal, noroom); * Инвариант представления есть г.els.(1), ..., г.els (r.size) не содержит дубликатов Функция абстракции есть
А (г) == {r.els [i] I <== i <== г.size} *) {Операции для типа intset. 100,} procedure intzet_100-init (var s: intseL 100); begin
s.size :== 0 end {intset-lOO.init}; lunction intseL 100- insert (var s: intseLlOO; x: integer):
intset. 100-insert, exceptions; var i: integer; not-found: boolean; begin i := 1; not-found := true; while (i <== s.size) and not-found do
it s.els [i] = x then not-found := false else i := i + 1; if not-found then if s.size < 100 then begin s.size := s.size-}- 1; s.els [s.size] := x; intset-100-insert := normal end
else intset-100-insert := norooin else intset-100-insert :== normal end {intset-100-insert};
procedure intset-100-delete (var s: intset-100, x: integer); var i: integer; not-found: boolean; begin
i := i;
not-found := true; while (i <= s.size) and not-found do
if not (s.els [i] = x) then i := i + I else begin s.els [i] := s.els Is.size]; s.size := s.size — 1; not-found := talse end
end {intset-100-delete);
tunction intset-100-member (vars: intset-100; var i: integer; found: boolean; begin i := 1; found := false; while (i <= s.si; if s.els [i] == x
integer): boolean;
found :== false;
while (i <= s.size) and not (found) do if ч pis HI =s x then found :== true else
i:- i= 1;
ч s.eia li i — .. ..-,.. intset _100_member := found end {intset-lOO-member): {Конец реализации intset-100.)
Рис. 15.2. Реализация типа intset-100,
Использование других языков
slOO-elems = generator type is slOO-elems-init, slOO-elems-next, slOO-elems-done Описание slOO-elems представляет собой генератор для объектов intset-100.
Операции
procedure slOO-elems-init (var s: intset-100, varg: slOO-elems) modifies g
effects Инициализирует g генератором, который может быть использован для итераций над элементами из s.
function slOO-elems-done (var s: intset-100, varg: slOO-elems): boolean
requires " есть генератор над s, а после создания g s не модифицировалось. effects Возвращает значение true, если все элементы из s были получены предыдущими обращениями к slOO-elems-next; в противном случае возвращает значение false.
function slOO-elems-next (vars: intset-100, varg: slOO-elems): integer requires g есть генератор няд s, а после создания g s не модифицировалось и не все элементы из s были «обработаны». modifies g
effects Возвращает произвольный элемеш из s, который не был получен ранее, и модифицирует g, отмечая получение этого элемента.
end slOO-elems Рис. 15.3. Спецификация генератора slOO-elems.
elems-initсоздаст копию), однако создание и хранение копии неэффективно. Реализация генератора slOO-elemsприведена н« рис. 15.4.
{Определения представлений для intset-100 и slOO-elems.}
type
intset-100 = record size:. О... 100;
els: array [I ... 100] of integer end;
slOO-eiems == 0 ... 100;
{Операции для slOO-elems}
procedure slOO-elems-init (var s: intset-lOO; var g: slOO-elems); begin
g:=0 end {slOO-elems-init};
function slOO-elems-done (vars: intset-lOO; varg: slOO-elems): boolean; begin
slOO-eiems-done := (g s= s.size) end {slOO-elems-done};
function slOO-elems-next (var s: intset.lOO; var g: slOO.elems): integer; begin g:=g+l; slOO-elems-next := s.els [g] end {slOO-elems-next};
Рис, 15.4. Реализация slOO-elems, i/, II*
324 Глава 15