Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
38
Добавлен:
23.03.2015
Размер:
933.38 Кб
Скачать

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 Soft­ware 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

Соседние файлы в папке Б. Лисков