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

7.4. Генераторы

В языке Паскаль имеется оператор for, который позволяет выполнить итерацию в некотором поддиапазоне типов скалярных величин (за исключением типа real), но в нем совсем нет меха­низма для реализации итераторов, определенных пользователем.

.Использование языка Паскаль

Как отмечено в гл. 6, это может вызвать неудобства. Например, чтобы суммировать элементы в очереди целых чисел intqueue, мы могли бы написать программу, аналогичную программе, приведенной на рис. 7.9. Эта реализация была бы несколько ме­нее неуклюжа, если бы мы к очереди целых чисел intqueue доба­вили функцию q.copy, но все равно она была бы неудовлетвори­тельной. В этой программе имеются потери и по времени, и по пространству. По крайней мере они состоят из копирования всей очереди.

sum := О t := q.new; while not (qJsempty (q)) do

begin

i := q_remfirst (q); sum := sum + i; q_append (t, i) end;

while not (isempty (t)) do q_append (q, q_remfirst (t));

Рис. 7.9. Суммирование элементов очереди без использования итератора,

Чтобы избежать таких потерь, мы создадим тип некоторого специального вида, называемый генератором. Генераторы ими­тируют работу итераторов. У каждого генератора есть три функ­ции и одна процедура. Эти три функции создают объект генера­тора, получают следующий элемент и проверяют, все ли элементы были выданы. Процедура уничтожает объект генератора.

Например, для очереди целых чисел мы могли бы создать генератор qelems. Функция qelems.create выполняет некоторую работу, которая соответствует той инициализации, которая де­лается до выполнения первого оператора yield в итераторе языка CLU. Эта функция возвращает некоторый объект с типом qelems. Вторая функция qelems_next выдает каждый раз один элемент при обращении к ней и модифицирует объект генератора qelems, чтобы указать, что этот элемент уже был выдан. Последняя функ­ция qelems.done используется для определения того, были ли выданы все элементы. Процедура qelems, destroy освобождает пространство, которое было выделено при работе функций qe-lems.create и qelems_next.

Спецификация генератора qelems приведена на рис. 7.10. Обе функции qelems_done и qelems_next требуют, «чтобы очередь, используемая для создания элементов qi, не модифицировалась после того, как создан элемент qi». Это аналогично общему тре­бованию предложения requires итераторов языка CLU, в которых требуется, чтобы объект, по которому совершается итерация, не модифицировался в теле данного цикла.

162 Глава 7

qelems = generator type is qelems_create, qelems_done,

qelerns-next, qelems_destroy

Описание

Генераторы qelems используются для выполнения итераций по элементам оче­редей целых чисел intqueue. Реализация может зависеть от представления оче­реди целых чисел intqueue.

Операции

function qelems_create (qJntqueue): qelems

effects Возвращается некоторый объект qelems, который может быть ис­пользован для выдачи всех элементов в очереди q. function qelems_done (qi: qelems): boolean

requires Чтобы очередь целых чисел intqueue, используемая для создания элементов qi, не модифицировалась после того, как создан эле­мент qi.

effects Возвращается значение true, если все элементы в очереди целых чисел intqueue, используемой для создания элементов qi, были «выданы» после того, как был создан элемент qi. В противном слу­чае возвращается значение false. function qelems.next (qi: qelems): integer

requires Чтобы очередь целых чисел intqueue, используемая для создания элементов qi, не модифицировалась после того, как создан эле­мент qi, и чтобы существовал некоторый элемент в очереди целых чисел intqueue, который еще не был выдан. modifies oi

effects Еозгращается некоторый элемент в очереди чисел intqueiie, исполь­зуемый для создания элемента qi. Этот элемент не был выдан после того, как был создан элемент qi. Элемент qi модифицируется для того, чтобы отметить тот факт, что данный элемент был «выдан». proctewe qelems_destroy (qi: qelems) modifies qi

effects Освобождается вся память из неупорядоченного массива, зани­маемая элементом qi. end qelems

Рис. 7,i0. Спецификация типа данных генератор для очередей.

Используя тип данных qelems, мы можем заново реализовать суммирование следующим образом:

sum := О

qi := qelems_create (q); tt-hi'le not (qelems. done (qi)) do

sum := sum + qelems. next (qi); qelems- destroy (qi)

Другие циклы реализуются аналогичным образом. На рис. 7. II приводится реализация операций над типом данных qelems. Эта реализация совместима с реализацией очереди целых чисел intqueue, приведенной на рис. 7.2. На самом деле осуществляется доступ к представлению очереди целых чисел intqueue. Такой доступ типичен для генераторов, использующих такие совокупносчи, как очереди целых чисел intqueue. Они в дей­ствительности являются частью данного типа совокупности, как итераторы являются частью языка CLU. Отметим, что пред-

Использование языка Паскаль

qelems = {qelemsJ-ep; qelems_rep == ^intqueue_elem; function qelems_create (q: intqlieue): qelems; var qi: qelems; begin new (qi); qit := qf; qelems .create :== qi end {функция qelems_create}; lunction qelems .done (qi: qelems): boolean; begin

qelems_done := (qi^ == nil) end {функция qelems_done}; function qelems_next (qi: qelems): integer; begin if q^ = nil then failure ("К функции qelems_next обратились с пустой очередью

генератора") else begin qelems_next := qi-("l-.val; ФТ ••= qiTt-next end

end {функция qelems_next}; procedure qelems_destroy (qi: qelems); begin

dispose (qi); end {процедура qelems_destroy};

Рис. 7.11. Реализация генератора для типа данных очередь целых чисел int­queue.

ставление имеет дополнительный уровень косвенности дла того, чтобы гарантировать, что совместимое использование работает правильно.

7.5. Полный пример

В этом разделе приводится конкретная иллюстрация того, как подготовить программу на языке Паскаль, используя под­ход, который мы описали. Предположим, что у нас есть некото­рая библиотека абстракций, в которую включен тип данных intbintree, приведенный на рис. 7.12, а мы хотим реализовать некоторую функцию, которая организует поиск по бинарному дереву целых чисел методом в ширину.

Поиск в ширину обычно реализуется с использованием оче­реди деревьев. Сначала помещаем в пустую очередь то дерево, по которому будет выполняться поиск. Затем проверяем, содер­жит ли корневой узел этого дерева тот элемент, который мы ищем. Если он не содержит его, удаляем из очереди это дерево и помещаем в очередь его правое и левое поддеревья. Затем этот процесс повторяется для каждого из этих деревьев. Если в конце

154 Глава 7

Использогание языка Паскале

intbintree == data type is bt_new, bt_isempty, bt_append_left, bt_append_right, bt.left, bt_right, bt_rootval, bt_destroy

Описание

Бинарные деревья целых чисел intbintree используются для хранения значе­ний типа целые числа. Элемент может присутствовать в нескольких узлах такого дерева. И промежуточные узлы, и листья имеют значения. Бинарные деревья целых чисел intbintree изменяемы.

Операции

function bt_new; intbintree

effects Возвращается .пустое бинарное дерево. function btJsempty (bt: intbintree): boolean effects Возвращается значение true, если дерево bt пусто, и значение

false — в противном случае.

procedure bt_append_left (bt: intbintree; е: integer)

modifies bt effects Добавляется некоторый узел со значением е как самый левый

узел дерева bt.

procedure bt.append_right (bt: intbintree; е: integer)

modifies bt effects Добавляется некоторый узел со значением е как самый правый

узел дерева bt.

function btJeft (bt: intbintree): intbintree requires Чтобы дерево bt было не пустым.

effects Возвращается поддерево дерева bt, чей корень является левым ближайшим потомком корня дерева bt. Это поддерево может быть пустым.

function bt_right (bt: intbintree): intbintree requires Чтобы дерево bt было не пустым,

effects Возвращается поддерево дерева bt, чей корень является пра­вым ближайшим потомком корня дерева bt. Это поддерево может быть пустым.

function bt_rootval (bt: intbintree): integer requires Чтобы дерево bt было не пустым. effects Возвращается значение корневого узла дерева bt. .procedure bt_destroy (bt; intbintree)

modifies bt

effects Освобождается вся память неупорядоченного массива, зани­маемая деревом bt. end intbintree

Рис. 7.12. Спецификация бинарного дерева целых чисел intbintree.

мы получим пустую очередь, то в дереве нужного элемента не было.

На рис. 7.13 приведена реализация функции breadth.first. search. При этом предполагается, что была выполнена конкрет­ная реализация абстракции queue [etype] при помощи замены etype на intbintree и задания имени btq получившемуся типу. Отметим, что функция breadth. firsLsearch использует про­цедуру btq.destroy для освобождения пространства, занимаемого создаваемой очередью. Если это не будет сделано, то память, занимаемая элементами локальной переменной q, была бы поте-

lunction breadth Jiist ^search (bt: intbiiitree; е: integer.): boolean; var q: btq; t: intbintree; found: boolean; begin found ;= false; if not (btJsempty (bt)) then begin q := btq_new; btq_append (q, btq);

while (not (btqJsempty (q)) and not (found)) do begin t := btq_re;nfirst (q); if bt.rootval (t) == e then found :== true else begin if not (btJsempty (bt.left (t))) then btq_append (q, btJeft (t)); if not (btJsempty (bt_right (t))) then btq_append (q, bt_right (t))! end end btq .destroy (q) end;

breadthJirst-search :== found end {функция breadthJirst_search}

Рис. 7.13. Реализация функции breadth, first, search,

ряна. С другой стороны, нет обращения к процедуре bt.dest­roy (t). Прежде чем выйти из операции, мы должны вызвать про­цедуру destroy для освобождения тех объектов, которые были вы­делены динамически в процессе работы этой функции. Хотя функ­ция breadth.first. search делает объявление локальной переме « ной типа бинарного дерева целых чисел intbintree, она не создает ни одного объекта данного типа.

Теперь у нао есть все части, необходимые для получения функции поиска в ширину, которую можно было бы вставить в большую программу. Способ, которым мы объединим эти части, зависит от используемого языка Паскаль. Во многих диалектах языка Паскаль (включая и стандартный язык Паскаль) порядок объявления переменных внутри блока имеет ограничения — сперва делаются все объявления const. затем все объявления type, далее все объявления var и, наконец, все объявления опе­раций. Из этого еледует, что объявление представления неко­торого абстрактного типа не будет соседствовать с реализациями операций над данным типом.

Далее мы предполагаем, что используем язык Паскаль, в ко­тором снято это ограничение и допускается любой порядок объ­явлений. Если для языка Паскаль, у которого более строгие огра­ничения, надо затратить большие усилия по программированию,

156 Глава 7

то, вероятно, выгодно написать некоторую программу, которая переводит программы, написамые как здесь без ограничений, в программы, соответствующие данному компилятору. Такая программа сама может быть быстро написана на языке Паскаль.

В языке Паскаль требуется, щобы все идентификаторы были объявлены до того, как они используются. Для написания вза­имно рекурсивных операций исильзуют^я формальные объявле­ния. Формальное объявление задает заголовок некоторой опе­рации, не задавая ее тела. Обвдц формат программы на языке Паскаль, соответствующий ограничениям на объявление, а затем на использование, всегда будет следующим: 1) реализация про­цедуры failure; 2) реализации всех абстрактных типов, которые будут использоваться в данной программе, включая и генераторы; 3) реализации всех операций, которые не являются частью не-когарого абстрактного тела, а также объявления констант, ти­пов и переменных и формальныесбъявления; 4) тело данной про­граммы.

Структура программы, соответствующей нашему примеру, была бы следующей: 1) реализация процедуры failure; 2) реализа­ция бинарного дерева целых чисе.т intbintree; 3) реализация оче­реди queue [intbintree]; 4) реализация функции breadth.first. serch; 5) другие объявления и тцо программы.

76. Обзор главы

Подход к программированию, рассматриваемый бэтой книге, в большой степени независим от используемого языка программи­рования. Мы предполагаем, что, хотя выбор языка программиро­вания для реализации программы окажет некоторое влияние на проектирование самой программы, это влияние не будет домини­рующим. В частности, нет необходимости ограничивать типы йостракций, используемые при проектировании, теми типами, ко-тсрые непосредственно поддерживаются в данном языке програм­мирования.

В нашем случае мы обратились к языку Паскаль — языку, который имеет относительно примитивные механизмы абстракций. Мы определили, как надо модифицировать абстракции процедур, итераций и данных, чтобы соответствовать языку Паскаль, и пред­ложили соглашения по программированию, которые могут быть использованы для реализации абстракций данных, генераторов, полиморфных абстракций и обработки исключительных ситуаций па языке Паскаль. Наш подход 8 языку Паскаль может быть относительно просто адаптирован для других языков программи­рования (в течение нескольких лет мы читали курс, в котором использовался язык ПЛ/1). Мы рассмотрим такую адаптацию ч гл. 15.

Использование языка Паскаль

Дополнительная литература

Garland, Stephen J. 1986. Introduction to Computer Science with Applica­tions in Pascal. Mass.: Addison-Wesley Publishing Co.

Упражнения

7.1. Реализуйте тип бинарного дерева целых чисел intbintree, заданный на рис. 7.12.

7.2. Специфицируйте некоторую абстракцию bintree [type], основанную на абстракции бинарного дерева целых чисел intbintree, заданную на рис. 7,12.

7.3. Реализуйте очередь целых чисел intqueue (рис. 7.1) так, чтобы доступ к первому и последнему элементам и их удаление могли быть сделаны за постоян­ное время. Надо ли изменять реализацию функции breadth-first-search для того, чтобы использовать преимущества этой лучшей реализации очереди целых чисел inttjueue?

7.4. Специфицируйте и реализуйте некоторую абстракцию обобщенного дерева, т. е. такого дерева, в котором может быть любое конечное число ветвей у каждого узла. (Указание: в своей реализации вы можете рассмотреть для каж­дого узла использование очереди его детей.)

7.5. Модифицируйте функцию breadth-first-search (рис. 7.13) так, чтобы она работала с обобщенным деревом, которое специфицировано в результате Вашего ответа на упражнение 7.4.

7.6. 'Специфицируйте и реализуйте некоторую абстракцию стека и исполь­зуйте ее для реализации некоторой функции, которая выполняет поиск в ширину.

7.7. Специфицируйте и реализуйте некоторый генератор, который воз­вращает все листья в бинарном дереве целых чисел intbintree (рис. 7.12).

7.8. Что реализовано в программе, приведенной на рис. 7,14? (Предпола­гается, что сделаны спецификации, приведенные на рис, 7.1 и 7.10.)

program test: var i, sum: integer; q: intqueue; qi, qil: qelems; begin q := q-new;

for i := I to 3 do q.append (q, i); sum :== 0 qi := qelems_create (q); while not (qelems-done (qi)) do begin qil '.= qelems-create (q); while not (qelems-done (qil)) do

sum := sum + qelems-next (qil); qelems-destroy (qil); sum :== sum+ qelems_next (qi); end; qelems-destroy (qi); q .destroy (q); write Ln (sum) end {программа test)

Рис. 7.14. Программа на языке Паскаль»

8. Спецификации

В данной книге мы указывали важность.спецификаций на всех этапах разработки программного обеспеченияГПри этом подчер­кивался тот факт, что корректное^прлменение спецификаций яв­ляется ключевым моментом на пуйГ к эффективному программи­рованию. Без спецификаций абстракции слишком сложны для того, чтобы оказаться действительно полезными^ В данной главе мы рассмотрим смысл спецификаций и некоторые критерии, ко­торые необходимо учитывать при их написании. Мы также рас­смотрим два основных назначения спецификаций.

8.1. Спецификации и необходимые наборы спецификаций

Назначение спецификации состоит в определении поведения абстракции. Пользователи полагаются н^.это поведение, а разра­ботчики должны обеспечить его. Говорят,'' что-реализация, обес­печивающая описываемое поведение, удовлетворяет заданной спецификации. ' ,

Определим/значение спецификации через набор' >ёсех про­граммных модулей, ей удовлетворяющих. Назовем это необходи­мым набором спецификаторов для данной спецификации. В ка­честве примера рассмотрим спецификацию

р = ргос (у: int) returns (х: int)

requires у j> 0 effects x >• у '.' ''•^'",'"•...!^,'^•'•.•''^

Она-удовлетворяется любой процедурой, с именем р, которая при вызове с аргументом, большим нуля, возвращает значение, большее этого аргумента. Члены необходимого набора включают в себя:

р = ргос (у: int) returns (int) return (у + 1) end р р = ргос (у: int) returns (int) return (у * 2) end р р = ргос (у: int) returns (int) return (у + 3) end р

Подобно каждой спецификации, данной спецификации удовлетво» ряет неограниченное число программ.

Спецификг.ции

Важно пешмняь, что спецификация, ее набор спецификаторов и некоторый член этого набора представляют собой весьма отлич­ные друг от друга вещи, так же отличающиеся друг от друга, . как отличается программа от набора всех выполняемых ею воз­можных вычислений и работы этой программы с каким-то одним набором значений."^

.2. Некоторые критерии) к спецификациям

применяемые

3 Хорошие спецификации могут иметь различные формы, од­нако все они имеют схожие атрибуты. В данном разделе рассма­триваются три важных атрибута — ограниченность, обобщенность и простота.

(" 8.2.1. Ограниченность

Lf Имеется большое различие между знанием о пригодности не­которых членов набора спецификаторов, и знанием того, что под­ходящими являются все члены. Это аналогично отличию знания о возможности работы программы с некоторыми наборами вход­ных значений, от знания возможности ее работы со всеми вход­ными значениями. Мы будем говорить о подобном отличии при обсуждении тестирования программ (см. гл. 9).'./Хорошая специ­фикация должна быть ограничена настолько, чтобы позволить выявить любую реализацию, не устраивающую пользователей данной абстракции. Это требование лежит в основе почти всех применений спецификаций. '/'

В общем случае обсуждение того, является ли спецификация достаточно ограниченной, предполагает рассмотрение задач, воз­лагаемых на членов набора спецификаторов,^меется ряд распро­страненных ошибок, которые почти всегда приводят к неадек­ватно ограниченным абстракциям^? К одной из таких ошибок' относитс^неполное задание требований в предложении requires.^/ Например, на рис. 8.1 приведены три спецификации итератора elems для портфеля целых чисел. (Портфель (bag) представляет собой обычный набор данных за тем исключением, что элементы в нем могут повторяться. Например, портфель bag [int] может содержать, число 3 дважды. Портфели иногда называют мульти-наборами.) В первой спецификации не дается ответа на вопрос, что произойдет, если b изменится в цикле, использующем elems. Она, следовательно, допускает реализации с резко отличающимся поведением. Например, приведет ли изменение b к изменению зна­чений, возвращаемых elems?

160 Глава S

elems = iter (b: bag [t]) yields (e: t) effects Поочередно выдает каждый элемент из Ь.

elems == iter (b: bag [t]) yields (e: t)

requires b не изменяется в цикле, использующем elems. effects Поочередно выдает каждый элемент из b.

elems = iter (b: bag [t]) yields (e: t)

requires b не изменяется в цикле, использующем elems. effects Выдает все элементы из b в произвольном порядке. Каждый эле­мент из b выдается столько же раз, сколько раз он встречается в b.

Рнс. 8.1. Три спецификации для elems.

Одним из способов решения данной проблемы является вве­дение требования о том, чтобы переменная b не изменялась вну­три использующего elems цикла. Это требование задано во вто­рой спецификации. Эта спецификация может бьпь, а может и не быть достаточно ограниченной, поскольку она не накладывает ограничений на порядок, в котором возвращаются элементы. Лучше, если спецификация будет определять порядок или содер­жать фразу «в произвольном порядке». Кроме того, в специфшка-ции не указано, что делается в том случае, если какой-нибудь элемент встречается в b более одного раза. В ней даже не говорится явно, что elems возвращает только те элементы, которые содер­жатся в b. В третьей спецификации рассмотренные недостатки устранены.

К другим ошибкам относится отсутствие полного списка ис­ключительных ситуаций и описания .поведения программы для граничных случаев. Например, пусть операция string^indexs получает в качестве аргументов строки si и s2, и, если si является подстрокой в s2, возвращает индекс вхождения в s2 первого сим­вола из si. Например: &iring,$'ndexs ("ab", "abc"») == 2

Спецпфикаиия, которая содержит только эту информацию, не будет достаючно ограниченной, поскольку она не объясняет, что произойдет в том случае, если si не является подстрокой в s2, пли если она встречается в s2 несколько раз, или если si или s2 пусты. Спецификация на рис. 8.2 является достаточно полной.

Сказанное позволяет сделать вывод о том, что написание до­статочно эффективных спецификацнй должно производиться с большой осторожностью. Полезную роль здесь может сыграть язык формальных спецификаций (формальные спецификации рас­сматриваются в гл. 10).

iiidexs = proc (si, s2: string) returns (i: int)

effects Если si входит в s2 как подстрока, то i есть последний индекс вхождения si в s2. i равно 0, если si не входит в s2, и рапно 1, если si есть пустая строка. indexs ("be", "abcbc") = 2 indexs (" ", "a") = I

Рис, 8.2, Спецификация операции strin^indexs,

Спецификации

^8.2.2. Обобщенность

Хорошаяуспецификация должна быть достаточно обобщенной, позволяя сократить до минимума число исключаемых, однако приемлемых программ.^Важность критерия обобщенности может быть не столь очевидна, как критерия ограниченности./Жела-тельно не только сократить ограничения на число приемлемых реализаций, но и обеспечить возможность использования наиболее удачных (более эффективных или «элегантных») из нихУНапример, спецификация

sqrt = proc (sq: real, e: real) returns (root: real)

requires sq^O & e;> .001. effects 0 ^ (root * root —sq) ^ e.

ограничивает разработчика алгоритмами, которые отыскивают приближения, больщ^е или равные фактическому значению квад­ратного корня;, Эт^Чотраничение может привести к неоправдан­ному снижению эффективности.

Желательно создавать спецификации максимально обобщен­ными. Это привело нас к «дефинитивному» стилю, который, и ис­пользован в данной книге.у Такая, спецификация яйй^Ь^ечис-ля^ свойства, которыми должны обладать спецификаторы. Аль­тернативой дефинитивной спецификации является операционная спецификация^Вместо описания свойств спецификаторов опера­ционная спецификация приводит способ конструирования их. Например,

search = proc (a: array [int], х: int) returns (i: int) signais (not.in) effects Анализирует a[low], a[low+ 1],...,поочгредно ii возвращает индекс элемента, равного х. Сигнализирует not-in, если ни один из эле­ментов не равен х.

есть операционная спецификация операции serach, в то время как

search = proc (a: array [int], х: int) returns (i: int) signals (not-in) effects Возвращает i, такое, что a[i] = х; х. Сигнализирует not_in, если такое i отсутствует.

есть дефинитивная спецификация. Первая спецификация объяс­няет, как реализовать операцию search, а вторая просто описывает свойства, которыми должны обладать входные и выходные дан­ные. Дефинитивная спецификация не только является более ко­роткой, но и предоставляет разработчику большую свободу, поз­воляя ему выбрать способ анализа элементов массива в удобном для него порядке.

"У Операционные спецификации имеют ряд преимуществ.1^Что более существенно./они &равнительно легко составляются опыт­ными программистами—главным образом по причине того, что их составление очень сильно напоминает сам процесс программи-

(> Листов Б., Гатэг Дж.

162 Глава 8

рования. Они обычно имеют больший размер, чем дефинитивные спецификации, и часто ведут к избыточности.У Например, опера­ционная спецификация операции search указывает, какой ин­декс должен быть возвращен в том случае, если х встречается в массиве а более одного раза. В качестве другого примера можно рассмотреть попытку написать операционную спецификацию для процедуры извлечения квадратного корня.

Хорошей проверкой на обобщенность является анализ каж­дого требования спецификации в предложениях requires и ef­fects и уточнение их реальной необходимости. Если это не так, то они должны быть исключены или ослаблены. Помимо этого, к лю­бой части спецификации, являющейся более операционной, не­жели дефинитивной, следует относиться g осторожностью.

8.2.3. Простота

Когда мы говорим о том, что делает программу «хорошей», то мы рассматриваем не только выполняемые ею вычисления, но и особенности самого текста программы, например, насколько удачно она разбита на модули и насколько хорошо составлены комментарии. Аналогично при анализе спецификации мы должны рассмотреть не только свойства набора спецификаторов, но и свойства самой спецификации, например, насколько она удобна для чтения.

Хорошая спецификация должна облегчать работу е другими людьми.УСпецификация может быть достаточно ограниченной и достаточно обобщенной^ т. е. иметь необходимый смыслы/однако этого недостаточно. Если этот смысл трудно понимаем читателями программы, то польза от такой спецификации весьма невелика.

Имеются два пути, ведущие к непониманию спецификации. Одни читатели могут изучать ее и прийти к тому, что они не по­нимают спецификацию. Например, читатель второй специфика­ции elems на рис. 8.1 может быть смущен тем фактом, что эле­мент встречается в портфеле более одного раза. Это не очень хо­рошо, однако гораздо менее опасно, чем в том случае, когда люди считают, что они поняли спецификацию, а в действительности это не так. В таком случае пользователь и разработчик понимают спецификацию каждый по-своему, что ведет к модулям, которые не могут работать совместно. Например, разработчик процедуры elems может решить выдавать каждый элемент столько раз, сколько раз он встречается в портфеле, а пользователь при этом считает, что элемец^выд^ется только один раз. „ Простота^' Шляется важным, однако аморфным критерием.'. Легко сказать, что хорошую спецификацию будет легче понять, и гораздо сложнее сказать, как достичь этого. Имеется множество факторов, оказывающих влияние на простоту, среди которых

Спецификации

W,'' \

раткость, избыточность и структурированность/являются, по­жалуй, наиболее важными.

'-Наиболее краткое представление не всегда является наилуч­шим, однако на начальном этапе оно, как правило, является луч­шим. Имеется ряд соображений, оправдывающих увеличение спе­цификации добавлением к ней избыточной информации или уров­ней структурированности, однако важно избегать ненужного многословия. Обычно по мере возрастания объема спецификации увеличивается вероятность появления в ней ошибок, а также по­является возможность ее неверного трактования. </ Важно не путать объем с полнотой^Спецификации, порожден­ные «потоком сознания», приводят к неполноте и громоздкости.. Созданные без учета критериев спецификации, подобно програм­мам, имеют размеры, большие необходимыхх/Вместо внесения до­бавлений в спецификацию разумнее^отложить локальные изме­нения и\ изыскать пути подтверждения достоверности этой ин­формации. Написание короткой полной спецификации занимает больше времени, чем написание длинной и полной, однако автор спецификации может оставить эту задачу читателям специфика­ции.

Любая содержащая избыточный терст спецификация является менее точной, чем она могла бы бытб^Избыточность не должна присутствовать без серьезных причин и может быть оправдана в двух ситуациях: в случае, если велика вероятность неправиль­ного понимания спецификации, и в случае необходимости выяв­ления ошибок. V ... .

Роль, выполняемая спецификацией, во многом схожа с ролью, выполняемой записной книжкой. Она '(предназначена не только для содержания информации, но и для эффективной работы с этой информацией. Избыточность может быть использована для снижения вероятности пропуска важных подробностей.УСтарая поговорка «скажи им, что ты хочешь им сказать, и скажи им, что ты им сказал» имеет некоторую педагогическую ценность. ^Идея состоит в том, чтобы^представить информациюнесколькими способами, внося избыточность без повторений. Рассмотрим, на­пример, '' • , •.. ,

р = proc(sl, s2: set) returns (b: bool) . effects b имеет значение true, если si есть подмножество s2, и false —в про­тивном случае.

р== proc(sl, s2: set) returns (b: bool)

effects b = yx [x ^ si имплицирует x ^ s2]. p = proc (si, s2: set) returns (b: bool)

effects b имеет значение true, если si есть подмножество s2, и false —в пр»« тивном случае, т.е. b = ух [х (- si имплицирует х ^ s2].

Первая спецификация кратка и вполне понятна большинству читателей. Однако некоторые из них могут задаться вопросом: 6*

164 Глава 8

было ли понятие «подмножество» выбрано достаточно продуманно или автор имел в виду соответствующее подмножество? Вторая спецификация, которую прочесть чуть сложнее, не оставляет ни­каких сомнений по этому вопросу. Возникает другой вопрос; почему составитель спецификации, полагая процедуру р провер­кой на наличие подмножества, не указал это явно? Третья спе­цификация отвечает на оба этих вопроса.

Констатация одного и того же факта двумя различными спосо­бами дает возможность читателям лучше понять спецификацию. Это позволяет избежать неверного понимания и сокращает об­щее время изучения спецификации. С этой точки зрения удачным примером может служить спецификация процедуры indexes, при­веденная на рис. 8.2.

Спецификация, описывающая одну и ту же вещь несколькими способами, облегчает понимание ее различными читателями. Очень часто критическая спецификация представляет собой концепцию с названием, мало знакомым большинству читателей. Рассмотрим, например,

pv = proc (inc, r: real, n: int) returns (value: real)

requires inc > 0 & r •> 0 & n :> 0.

effects Возвращает имеющееся значение годового дохода для inc за пе­риод n лет в проценте прироста без риска величиной г.

То есть value == inc+ (inc/(l +г)+...+ (inc/(l + г)"~'). Например, pv (100, .10,3) = 100+ 100/1.1 + 100/1.21

Для читателей, недостаточно хорошо знакомых с финансовыми вопросами, нелегко будет понять фразу «имеющееся значение». И с этой точки зрения вторая строка, «т. е.», представляет для них безусловный интерес. Вторая часть, «например», может быть использована читателями для подтверждения правильности своего понимания.

Если читатели извлекают пользу из подобной избыточности, то важно сделать так, чтобы вся подобная информация была ка­ким-либо образом помечена с этой точки зрения. Хорошим спо­собом является использование вводных слов и оборотов типа «например» или «т. е.».

Избыточность не сокращает число ошибок в спецификации. Она делает их более очевидными и дает возможность читателю их обнаружить. Рассмотрим, например,

too.cold = proc (temp: int) returns (b: bool)

effects b = true, если temp < 0 градусов по Фаренгейту; в противном случае b = false.

too.cold = proc (temp; int) returns (b: bool)

effects b = true, если temp < 0 градусов по Фаренгейту; в противном случае b = false. То есть b == true в точности в том случае, когда temp не больше, чем точка замерзания воды при нормальной температуре и давлении.

Спецификации

Первая спецификация не вызывает у читателя никаких подо­зрений, однако вторая явится для большинства предупредитель­ным сигналом.

Одна из базовых проблем, относящихся к спецификации, за­ключается в том, что в процессе изучения читатель вносит в этот процесс свое представление о предмете, порождающее неодно­значность. Для исключения подобной неоднозначности может потребоваться введение весьма значительной избыточности. Рас­смотрим, например,

billion = proc ( ) returns (b: int)

effects Возвращает целое число значением в один биллион. billion = proc ( ) returns (b: int) effects Возвращает целое число значением в один биллион, т. е. 10°.

Как американские, так и английские читатели найдут первую спе­цификацию абсолютно безошибочной. К сожалению, они интер­претируют ее совершенно по-разному, поскольку в Соединенных Штатах биллион есть 10", а в Великобритании биллион есть 10".

8.3. Почему именно спецификации?

Спецификации важны для достижения требуемой модульности программы. Абстракция используется для декомпозиции про­граммы на модули. Однако взятая в отдельности абстракция яв­ляется малопонятной. Без какого-либо описания мы не можем ни сказать, что она из себя представляет, ни отличить ее от одной из своих реализаций. В качестве такого описания и выступает спецификация.

Спецификация описывает соглашение между разработчиками и пользователями. Разработчик соглашается написать модуль, который относится к заданному набору спецификаторов. Поль­зователь соглашается не полагаться на знания о том, какой именно член набора используется, т. е. не предполагать ничего такого, что не было бы указано в спецификации. Такое соглашение поз­воляет разделить анализ реализации от собственно использова­ния программы. Спецификации дают возможность создавать ло­гические основы, позволяющие успешно «разделять и властво­вать».

Спецификации, очевидно, полезны и для документации про­граммы. Сам факт написания спецификации полезен также и потому, что он проливает свет на используемую абстракцию. Опыт показывает, что эта деятельность приносит столько же пользы, сколько приносит использование самого результата. Написание спецификации всегда позволяет нам узнать что-либо полезное об описываемом наборе спецификаторов. Это также всегда облегчает процесс выявления неоднозначностей, неполноты и недоопределенности. В некоторых случаях улучшение понима-

166 Глава 8

ния задачи является наиболее важным результатом данной ра-_ боты.

Цель состоит в написании спецификаций, которые являются одновременно достаточно полными и достаточно ограниченными. Таким образом, мы удаляем должное внимание поставленным тре­бованиям, исключительным ситуациям и граничным условиям. Этот процесс предполагает постановку ряда вопросов относи­тельно поведения абстракций, подобных тому, как поступать с индексами, если строка пуста. Смысл состоит в том, что поста­новка и ответ на такие вопросы заставляет нас более тщательно анализировать абстракцию и ее предполагаемое назначение.

Создание спецификации концентрирует внимание на том, ка­кой должна быть сама программа. Она служит как бы механизмом генерации вопросов, ответ на которые должен быть дан в резуль­тате консультации с пользователями, а не с разработчиками. Обусловливая постановку этих вопросов на ранних стадиях раз­работки системы, спецификация позволяет нам улучшить пони­мание требований к системе и проекту до начала их реализации.

Как это будет показано в гл. 12 и 13, спецификации можно начать составлять сразу же после того, как будут приняты опи­сываемые в них решения. Поскольку спецификации теряют зна­чимость только в том случае, если становятся устаревшими со­ответствующие абстракции, то они эволюционируют до тех пор, пока эволюционирует сама программа. Серьезная ошибка счи­тать, что процесс написания спецификаций является отдельной фазой создания программного обеспечения.

Однажды написанные, спецификации могут служить различ­ным целям. Они одинаково полезны проектировщикам, разработ­чикам и лицам, сопровождающим математическое обеспечение. В течение фазы реализации программного обеспечения наличие хорошей спецификации помогает как реализующим заданный мо­дуль, так и тем, кто использует затем этот модуль. Как уже гово­рилось, хорошая^^спецификация поддерживает баланс между ограниченностью и обобщенностью. Она сообщает разработчику, какие функции необходимо реализовать, однако не наклады­вает излишних ограничений на способ их реализации. Это дает разработчику максимальную свободу в написании модулей, согла­сованную в то же время с требованиями пользователей. Разу­меется, спецификации существенны и для пользователей, которым не на что больше положиться при реализации своих модулей. Кроме спецификаций имеются только лишь тексты программ, а на текущий момент обычно неизвестно, какая часть программы претерпела изменения. В процессе тестирования спецификации предоставляют информацию, которая может быть использована для генерации тестировочных данных и построения заглушек, имитирующих работу данного модуля. (Мы обсудим это приме-

Смцификации

нение спецификации в гл. 9.) На этапе компоновки системы на­личие хороших спецификаций позволяет сократить число и серь­езность проблем, связанных с интерфейсами, за счет уменьшения числа различных неявных предположений об этих интерфейсах. При обнаружении ошибки спецификации позволяют выявить' их местоположение. Более того, они определяют ограничения, которые необходимо соблюсти при исправлении ошибки, что помогает избежать новых ошибок в процессе исправления ста­рых.

Наконец, спецификация весьма полезна в процессе сопровож­дения программного обеспечения. Существование ясной и акку­ратной документации является необходимым условием для успеш­ного и эффективного сопровождения. Нам необходимо знать, что делает каждый модуль, а если он достаточно сложен, знать также то, как он это делает. Зачастую эти два аспекта документации сильно взаимосвязаны. Использование спецификации в качестве документации позволяет нам разделить эти две стороны, что облегчает анализ и возможные модификации. Например, модифи­кация, при которой требуется только заново реализовать един­ственную абстракцию без изменения ее спецификации, гораздо легче реализуема, чем модификация, предполагающая изменение спецификации.

8.4. Заключение

В данной главе были рассмотрены спецификации и предложены критерии, используемые при их создании. Мы определяем смысл спецификации через набор программных модулей, ей удовлетво­ряющий. Такое определение учитывает интуитивное назначение спецификации, а именно: утверждает, что общего имеют между собой все допустимые реализации абстракции. Такая специфика­ция сообщает пользователям то, из чего они могут исходить, а проектировщикам — то, что они должны реализовать.

Хорошие спецификации должны быть ограниченными, обоб­щенными и простыми. Ограниченность и обобщенность предпола­гает наличие набора модулей, удовлетворяющих спецификации: не допускаются реализации, неприемлемые для пользователей абстракции, а предпочтительные реализации (например, более эффективные) должны быть выделены. Обобщенность гораздо легче реализовать в том случае, если спецификации написаны с ис­пользованием дефинитивного подхода, который только задает свойства набора спецификаторов. Операционный подход, объяс­няющий способ реализации абстракции, дает слишком ограничен­ные спецификации.

Простота подразумевает легкость понимания спецификации пользователями. При этом в начале работы над спецификацией

168 Глава 8

желательно стремиться к наибольшей краткости, а затем ввести некоторую избыточность, часто в форме примера. Избыточность также позволяет читателям проверить свое понимание специфи­кации.. Она также делает более очевидными ошибки, поскольку при избыточных описаниях они часто проявляются в виде про­тиворечий. Для облегчения понимания спецификации вся избыточ­ная информация должна быть каким-нибудь образом выделена.'

Спецификации имеют два основных назначения. Во-первых, процесс написания спецификации проливает свет на специфици­руемую абстракцию — за счет фокусирования внимания на свой­ствах этой абстракции. Их роль может быть увеличена путем тщательного анализа свойств, которые могли остаться незамечен­ными, включая то, что должно быть указано в предложении requires относительно обработки исключительных ситуаций и по­ведения на границах. Иногда эта роль является главной ролью) выполняемой спецификацией, поскольку она выявляет проблемы в спецификациях, требующих дальнейшего анализа.

Спецификация используется также для документирования программ, на каждой стадии разработки и создания программного обеспечения. Разумеется, спецификация не покрывает всю необ­ходимую документацию. Она описывает выполняемые модулем функции, однако для сложных модулей необходимо также описа­ние принципов их работы. Если эти два вида документации четко разграничены, то модификация и сопровождение программного обеспечения значительно облегчаются.

Спецификации являются единственной расшифровкой абстрак­ции и играют существенную роль в использованной нами методо­логии, поскольку без них абстракции были бы слишком расплыв­чатыми. Мы будем акцентировать на них свое внимание и в по­следующих главах.

Дополнительная литература

Parnas, David L,, 1977, The use of precise specifications in the development of software. In Proceedings of IFIP Congress 77, pp. 861—868.

Упражнения

8.1. Приведите четкую и краткую спецификацию абстракции портфеля целых чисел с операциями по созданию пустого портфеля, размещения и удаления из него элемента, проверки на принадлежность элемента к портфелю, установле­нию числа вхождений элемента в портфель и выборки элементов и портфеля.

8.2. Воспользовавшись спецификацией, рассмотренной в предыдущей главе, проанализируйте ее ограниченность, обобщенность и простоту. 8.3. Разумно ли ставить вопрос о корректности спецификации? Объясните. 8.4. Обсудите, как спецификации могут быть использованы в процессе компоновки системы. 8.5. Рассмотрите взаимосвязь абстракции, ее спецификации и реализации.

^.Тестирование и отладка

До сих пор речь шла о спецификации и реализации программы 'в совсем мало говорилось непосредственно о ее создании. Сей­час мы перейдем к связанным с этим вопросам, касающимся про-1-верки работоспособности программы.

1 Говоря о том, что созданная программа выполняется согласно (; нашим ожиданиям, мы воспользуемся понятием «проверка досто-- верности». Проверка достоверности обычно производится путем выполнения различных тестов и анализом ряда соображений, касающихся того, что мы считаем правильной работой программы. Назовем процессом отладки такой процесс, при котором произво­дится выяснение причин, вследствие которых программа работает неправильно. Введем также понятие защитного программирова­ния, обозначая этим создание программ, специально предназначен­ных для облегчения процессов проверки достоверности и отладки.

Перед подробным рассмотрением процесса проверки на до­стоверность необходимо обсудить, что мы должны от него ожи­дать. Наилучшим результатом окажется ситуация, при которой все пользователи программы будут удовлетворены ее работой. Эта цель недостижима. Такая гарантия предполагает знание того, что понимать под удовлетворенностью пользователей работой программы. Лучший результат, на который мы можем рассчиты-'вать, это гарантия того, что программа удовлетворяет своей спе­цификации. Большая часть работы тратится на выполнение именно этого условия.

Проверка достоверности может быть реализована двумя спо­собами. Мы можем говорить о том, что программа будет работать со всеми возможными входными данными. Это предполагает тща­тельный анализ текста программы, что обычно называют верифи-цировац^ем. В гл. II мы рассмотрим ряд четких приембв' про­верки программ. Как увидим далее, формальная проверка про­грамму^ без^ привлечейия ЭВМ зачастую~чересчур__утомительна. К сожалению, на сегодняшний день существуют относительно примитивные средства проверки, и проверка программ в подав­ляющем большинстве представляет собой неформализованный процесс. Однако даже неформальная проверка может оказаться Довольно затруднительной.

170 Глава 9

пироеание и отладка

Альтернативой верификации служит тестирование. Легко можем убедиться в том, что для некоторого набора входных зна­чений программа работает правильно, простой проверкой ее ра­боты с этими значениями. Если набор возможных входных зна­чений невелик, то возможна полная проверка (для каждого из входных значений). Но для большинства программ входной на­бор слишком велик (зачастую бесконечен) и полная проверка не представляется возможной. Однако грамотно подобранный на­бор проверочных значений может в большой степени укрепить нашу уверенность в том, что программа функционирует пра­вильно. Хорошо произведенный тест позволяет выявить боль­шинство ошибок в программах.

В данной главе мы сфокусируем свое внимание на тестирова­нии как методе проверки достоверности программ. Рассмотрим, как выбирать соответствующие тесты и как организовать про­цесс тестирования. Обсудим также процесс отладки и защитное программирование.

9.1. Тестирование

,i Тестирование представляет собой процесс выполнения про-1 граммы для некоторого набора проверочных значений и сравне-^ ния полученных результатов с ожидаемыми. Цель_тести р о^ания заключается в выявлении возможных ошибок~Тестирование~не указывает^конкретное^местонахождепие ошибки, иго осуществ­ляется при отладке. При тестировании программы мы анализи­руем взаимосвязь между входными и выходными значениями. При отладке программы мы также заинтересованы в подобной взаимосвязи, однако при этом также обращаем свое внимание на промежуточные результаты вычислений.

Успех тестирования заключается в выборе подходящего на­бора проверочных данных. Как уже говорилось, исчерпывающее тестирование для большинства программ осуществить невозможно. Например, если программа принимает на входе три целых числа, каждое из которых может изменяться в диапазоне от 1 до 1000, то полная проверка потребует выполнения программы миллион раз. Если каждый прогон программы занимает по времени одну секунду, то на полную проверку потребуется немногим более 31 года.

Что мы можем сделать, учитывая невозможность проверки всех вариантов? Нашей целью является нахождение сравнитель­но небольшого набора тестов, которые позволят нам получить ту же"йнформацию, что и при полной проверке для всех возмож­ных допустимых значений. Например, предположим, что про­грамма принимает в качестве аргумента целое число, работая од­ним методом со всеми четными числами, и другим — со всеми

.четными. В этом случае достаточно будет проверки этой про-раммы с каким-либо четным числом, каким-либо нечетным, а »кже с нулем.

M.I. Тестирование методом черного ящика

Тестовые значения выбираются с учетом спецификации и са-^мой реализации программы. При тестировании методом черного 'ящика мы выбираем проверочные да^ные^^сходя^из одлои^спе->.дификации, и не учитываем внутреннюю^структуру 'программы.' 1Т1кой подход р^аспространён во многих инженёрных"дйсципли-нах и имеет ряд существенных преимуществ. Основным преиму­ществом является то, что процедура тестирования не находится в прямой зависимости от проверяемой компоненты. Например, предположим, что автор программы сделал ошибочное предполо­жение о том, что для некоторого класса входных значений про­грамма использоваться не будет. Исходя из этого, он не включил в программу анализ подобной ситуации. Если данные для проверки были подобраны на основании анализа программы, то вследствие этого неверного предположения проверяющий мог быть легко введен в заблуждение. Вторым преимуществом тестирования та­кого рода является независимость его по отношению к измене­ниям в реализации. Проверочные данные не требуется изменять даже в том случае, если в программе были произведены значи­тельные изменения. Наконец, преимуществом является также и то, что результаты тестирования могут быть проанализированы людьми, незнакомыми с внутренней структурой проверяемой про­граммы.

Пути тестирования „спецификации. Хорошим способом реа­лизации тестирования методом черного ящика является тестиро­вание, основанное на анализе различных частей спецификации. Такими частями могут быть предложения requires и effects. Рас­смотрим пример тестирования на основе анализа предложения requires. Пусть имеется следующая спецификация:

sqrt = proc (х: real, epsilon: real) returns (ans: real) ; requires х^з 0 & (.00001 < epsilon < .001) effects (х — epsilon ^ ans * ans ^ x + epsilon)

В приведенной спецификации предложение requires представляет собой конъюнкцию двух термов:

1. х>0

2. (.00001 < epsilon < .001).

Для выделения условий, при которых предложение requires будет удовлетворено, мы должны проверить парные комбинации путей, при которых должно удовлетворяться каждое из условий. Поскольку первое условие есть дизъюнкция двух примитивных

172 Глава 9

иртание и отладка

термов (х >- 0 есть сокращение от х =•= 0 1 х> 0), то оно может быть удовлетворено двумя способами. Это оставляет нам ситуа­ции, при которых требования в предложении requires удовлетво­ряются:

1. х = 0 & .00001 < epsilon < .001.

2. х > 0 & .00001 < epsilon < .001.

Любой набор тестировочных данных для программы sqrt должен обязательно удовлетворять каждому из этих условий.

Сформировать набор данных, который будет проверять все условия, заданные в предложении requires спецификации, до­вольно затруднительно. Может оказаться затруднительным даже установление того, какие условия должны быть проверены. На­пример, в приведенной выше спецификации программы sqrt мы ' предполагаем, что программа иногда возвратит точный результат, иногда результат, чуть меньший, чем точное значение квадрат­ного корня, а иногда — чуть больший. Программа же, которая всегда возвращает чуть большее или равное значение, будет вполне приемлемой. Мы окажемся не в состоянии подыскать данные, при которых возвращаемый результат чуть меньше ожи­даемого, однако мы не можем знать этого без анализа текста про­граммы. Фактически, без анализа текста программы мы не имеем представления о том, какие классы входных данных разбивают результаты на три категории.

Тем не менее мы должны внимательно проанализировать предложение effects и попытаться отыскать данные, которые ему удовлетворяют. Например, рассмотрим операцию intset$member:

member = proc (s: intset, х: int) returns (bool) effects Возвращает значение true, если х принадлежит s; в противном

случае возвращается значение false.

Предложение effects в этой спецификации представляет собой конъюнкцию: либо х принадлежит s, либо нет.

Часто тестирование, основанное на анализе предложения ef­fects, проверяет функционирование программ обработки ошибок. Отсутствие сигнала о возникновении исключительноя ситуации при запрещенных значениях входных параметров является столь же серьезной ошибкой, что и неверная работа программы при нор­мальных значениях. Следовательно, данные теста должны про­верять все возможные реакции программы. Рассмотрим, напри­мер, следующую спецификацию:

search^ proc (a: array lint], х: int) returns': int) signals (notJn) •-' effects Если х не принадлежит а, то возвращается i, такое, что a [i]=x;

в противном случае — сообщение not-in.

анной ситуации мы должны включить в проверку оба теста—. тай, когда х принадлежит а, и случай, когда х не принадле-: • а. Аналогично, если при выполнении программы sqrt возни-ают исключительные ситуации, а не значения, указанные в пред-ожении requires, то мы должны включить тесты, проверяющие акие ситуации.

Проверка граничных условий. Программа должна быть всегда Проверена с «типичными» входными значениями — например, Массивом или набором, содержащим несколько элементов, или целым числом, находящимся в границах между максимальным и 1минимальным значением, выдаваемым программой. Важно также ^проверить программу с нетипичными входными данными, которые Принято называть граничными.

t Анализ всех случаев, обусловленных предложением requires, ^Предполагает также проверку ряда граничных ситуаций — на-; пример, случай, при котором программа sqrt должна извлечь квадратный корень из нуля. Однако такой анализ не учитывает 'все граничные ситуации. Весьма важно проверить максимально возможное число таких ситуаций. Подобные проверки позволяют выявить два распространенных вида ошибок:

1. Логические ошибки, при которых обработка граничного условия не приводит к выходу на специальную подпрограмму его обработки, и

2. Отсутствие проверки условий, которые могут привести к возникновению исключительной ситуации в языке, расположен­ном на более низком уровне, или в аппаратной части (например, арифметическое переполнение).

Для составления тестов, проверяющих второй тип ошибки, хорошим приемом является использование проверочных данных, покрывающих все комбинации максимально и минимально допу­стимых значений всех тех числовых аргументов, изменение ко­торых происходит в пределах некоторых границ. Например, тесты для программы sqrt должны включать случаи со значением для epsilon, очень близким к 0.001 и 0.00001. При работе со стро­ковыми данными тесты должны включать пустую строку и строку из одного символа. Для массивов мы должны проверить пустой и односимвольный массивы.

Наложение ошибок. Другой вид граничного условия возникает в том,случае, когда один изменяемый объект связывается с двумя различными формальными параметрами, рассмотрим, например, процедуру

append, array -= proc (a I, a2: array [int]) modifies al и a2 effects Удаляет элементы из a2 и помещает их в конец al

К.

174 Глава 9

'естиромние и отладка

которая была реализована следующим образом)

append.array == proc (al, a2: array [int]) ai = array [int] while ai$size (a2) ^> 0 do ai$addh (al, a2 [ai$low(a2)]) ai$reml (a2) end end append, array

Любые проверочные значения, у которых al и a2 связаны с од­ним и тем же непустым массивом, приведут к возникновению в про­цедуре append.array серьезной ошибки.

9.1.2. Тестирование на основании текста программы

Приступая к проверке, лучше всего начать с проверки методом черного ящика. Однако при основательной проверке программы этот метод редко оказывается эффективным. Без просмотра текста самой программы невозможно сказать, какие проверки могут предоставить новую информацию. Следовательно, при использо­вании метода черного ящика неизвестно, какой объем всех воз­можных проверок будет осуществлен. Например, предположим, что некоторые выходные результаты программа выбирает из таб­лицы, а некоторые вычисляет. Если тест, основанный на методе черного ящика, включает только значения, которые программа должна отыскать в таблице, то информация о той части программы, которая ответственна за вычисления, получена не будет.

Хорошим способом реализации проверки методом черного ящика является выбо^^азличных^маршруюв^путей проверки. Главная цель в этом случае — создание теста, в котором каждый путь проверяются по крайней мере одним членом из набора. Мы говорим в этомслучае, что набор данных для теста является полно­маршрутным. В гл. II мы рассмотрим прием подсчета путей те­стирования программы. Сейчас мы будем исходить из неформаль­ных аргументов. Рассмотрим программу

max_of_three == proc (х, у, z: int) returns (int) if x>y

then if x ^> z then return (х) else return (z) end else if у > z then return (y) else return (z) end end end max_oL three

Несмотря на тот факт, что существует п" входных значений, где п есть допускаемый языком программирования диапазон для целых чисел, для проверки данной программы имеется только четыре маршрута. Следовательно, концепция проверки всех мар­шрутов позволяет нам разбить проверочные данные на четыре класса. В первом из классов х больше, чем у и z. В другом — х

ьше, чем г, но меньше, чем у, и т.д. Представители этих че-)ех классов есть

3,2,4 1,2,1 1,2,3

Легко показать, что проверка всех маршрутов недостаточна для обнаружения всех ошибок. Рассмотрим программу

шах. of. three == proc (х, у, z: int) returns (int) return (х) end max. of. three

Набор для теста содержит только тройку 2,1,1

и для данной программы является полным. Использование такого теста приведет к ложному заключению, что программа написана правильно, поскольку данный тест не в состоянии обнаружить су­ществующие ошибки. Проблема заключается в том, что стратегия проверки, основанная на проходе по всем маршрутам программы, не приводит к обнаружению отсутствующих маршрутов^ а про­пуск маршрута является одной из распространенных ошибок программирования. Эта проблема являет собой специфический при­мер ранее упомянутого факта: ни один набор данных, основан­ный на анализе текста программы, не является исчерпывающим. Всегда в расчет должна приниматься и спецификация.

Другая потенциальная проблема, связанная со стратегией тестирования, основанной на выборе полномаршрутной проверки, заключается в наличии порой слишком большого числа маршру­тов. Это делает ее непрактичной. Рассмотрим фрагмент программы на рис. 9.1. Как видно из последующего анализа, в этой программе имеется У"" маршрутов. Оператор if обусловливает выполнение одной или другой ветви программы, и оба этих маршрута приво­дят к выполнению следующего прохода цикла. Следовательно, для каждого прохода через i-ю итерацию имеется два прохода через (i + 1)-ю итерацию. Поскольку к первой итерации ведет только один маршрут, то число маршрутов по выходу из i-й ите­рации составляет 21. Следовательно, из 100-й итерации существует 2"° выхода.

j:=k

for i: int in int$fromJo (1, 100) do if pred (i *j) then j :== j + I end end

Рис. 9.1. Программа с большим числом маршрутов.

Проверка 2^ ситуаций вряд ли является реальной. В таких случаях мы останавливаемся на некотором приближении к полно­маршрутному набору проверочных значений. Наиболее распро­страненным приближением является рассмотрение в качестве

176 Глава 9

эквивалента двух или более проходов через цикл и двух или бо­лее рекурсивных обращений к процедуре. Для получения набора проверочных данных для рассмотренной выше программы оты­щем полномаршрутный набор значений для следующей программы»

j:=k

for i: int in int$from_to (1, 2) do if pred (i *j) then j :== j + I end end

В этой программе имеется только четыре маршрута. Полный на­бор данных для всех этих маршрутов может состоять, например, из следующих представителей:

1. pred (k) pred (2k + 2)

2. pred (k) ~ pred (2k + 2)

3. —pred (k) pred (2k)

4. —pred (k) — pred (2k)

Помимо этого, мы всегда включаем также случаи для каждой ветви условного выражения. Определим полномаршрутное тести­рование для циклов и итераций следующим образом:

1. Для циклов с фиксированным числом проходов, как это было в предыдущем примере, будем использовать две итерации. Мы предпочтем сделать два прохода через цикл, поскольку к од­ной из распространенных ошибок относится отсутствие повтор­ного задания начальных условий для цикла. Мы также должны убедиться в том, что ни в каком из случаев наши тесты не вызовут зацикливания.

2. Для циклов с переменным числом проходов используем в качестве значения этого числа нуль, единицу и двойку, и также включим тесты, предотвращающие зацикливание. Рассмотрим, например, следующий цикл:

while х S> 0 do

% некоторые действия end

Для такого цикла возможна ситуация, в которой не будет сделан ни один проход. Такой случай должен быть обязательно учтен в тестах, поскольку выполнение цикла в другой ситуации может быть причиной ошибки в программе.

3. Для рекурсивных процедур добавляются тесты для тех случаев, когда возврат из процедуры происходит без рекурсивных обращений, а также тесты, рассчитанные только на одно рекурсив­ное обращение.

Рассмотренное приближение к полномаршрутному тестированию, разумеется, далеко от идеала. Оно часто позволяет обнаружить ошибки, однако не дает никаких полных гарантий.

Тестирование и отладка

Соседние файлы в папке POSIBNIK