Назад Оглавление Вперед
На головную страницу М.М.Горбунов-Посадов
 
РАСШИРЯЕМЫЕ ПРОГРАММЫ
 

 Г л а в а  5
ОДНОРОДНЫЙ НАБОР
 
5.2. Наборное гнездо
 

 

5.2. Наборное гнездо

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

      5.2.1. Сфера применения. В предыдущем разделе было показано, что наборные гнезда являются основным инструментом для одной из стратегий поэтапной разработки — программирования «вширь». Кроме того, ранее (в конце гл. 4) утверждалось, что они же нередко требуются в задачах вычислительного эксперимента. В обоих случаях их применение существенно облегчает последующее пополнение программного фонда новыми модулями. И это не случайно. Как мы вскоре убедимся, наборное гнездо является, пожалуй, наиболее мощным из числа средств, служащих для построения развивающихся, расширяемых программ.
      При проектировании расширяемой программы имеет смысл взять на вооружение уже известный нам по предыдущему разделу конфигурационный ориентир — набор однородных модулей. Сначала выявляют вероятные направления развития, а затем каждому из них пытаются сопоставить упомянутый набор.
      Если такое построение увенчалось успехом, то большинство очередных модификаций программы удается свести к пополнению некоторого набора (наборов) одним или несколькими новыми однородными модулями. Тем самым развитию программы придается регулярный характер, вновь создаваемые модули попадают в специально подготовленную для них среду. Если же, кроме того, аппарат наборных гнезд для размещения однородных модулей обеспечен системной поддержкой, то подключение новых модулей происходит безболезненно для окружения, что существенно повышает надежность процесса выполнения изменений.
      Существует и еще одна причина обращения к наборным гнездам. Они позволяют более рельефно оттенить однородные участки программы, что делает ее нагляднее. Особенно интересным здесь оказывается оформление многосвязных однородных модулей, поскольку средства системной поддержки наборных гнезд способны предоставить возможность независимого просмотра и обработки любого среза их односвязных компонентов.
      Например, во многих программах, авторы которых в большинстве своем никогда не задумывались о роли однородных конструкций, встречаются тем не менее весьма протяженные операторы выбора. Такие операторы имеют много ветвей, и каждая из ветвей представляет собой увесистый кусок алгоритма. Изучать и модифицировать протяженный оператор выбора довольно неудобно, в частности, из-за того, что он не помещается целиком на экране.
      Иногда пытаются выйти из положения, оставив в операторе выбора только условия, а все действия оформляя в виде самостоятельных подпрограмм. Возможно, тем самым и удается втиснуть оператор в пространство экрана, но одновременно неоправданно усложняется структура программы.
      Применение в подобной ситуации аппарата наборных гнезд позволяет не только облегчить процесс развития программы, но и сделать ее нагляднее. Каждая ветвь выбора оформляется в виде многосвязного модуля, состоящего из двух компонентов: условия и действий. В результате, с одной стороны, теперь подключение новой ветви не будет требовать редактирования текстов существующих программ.
      С другой стороны, это решение выводит программу на новый уровень наглядности. Средства системной поддержки позволят компактно показать на экране всю совокупность условий. Далее, подведя курсор к любому из условий, можно будет переключиться на просмотр соответствующих действий.

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

#VARIANT  имя_гнезда

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

•    •    •
#HORIZON  имя_набора
      тело_гнезда
[
#DELIMITER  разделитель ]
#END_OF_HORIZON
•    •    •

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

#имя_набора . имя_компонента

Если модуль односвязный, то, разумеется, имя компонента не указывается и конструкция вставки приобретает еще более простой вид:

#имя_набора

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

#имя_набора . NAME

Сходным образом можно включать в текст номер текущего повторения цикла (NUMBER), общее число однородных модулей (SIZE) и т. д.
      Рассмотрение последней, необязательной части наборного гнезда — «#DELIMITER разделитель» — придется начать несколько издалека. В существующих языках программирования для разграничения записываемых друг за другом одноуровневых конструкций применяются обычно либо разделители, размещаемые между конструкциями, либо завершители, размещаемые вслед за каждой конструкцией. Разница между ними заключается в том, что завершитель пишется и вслед за последней конструкцией, а разделитель — не пишется.
      Использование завершителей несколько технологичнее, поскольку тут (в отличие от разделителей) единообразно выполняется вставка/удаление единственной, первой, последней и любой другой одноуровневой конструкции: всегда вставляется/удаляется пара «конструкция + разграничитель». При использовании разделителей эти действия зависят от положения конструкции: у первой вставляемой/удаляемой конструкции вставляемый/удаляемый разграничитель оказывается справа, у последней — слева, а у единственной конструкции он вообще отсутствует. Поэтому в современных языках предпочтение все чаще отдается завершителям.
      В случае наборного гнезда отдельные повторения текста тела гнезда обычно требуется так или иначе разграничивать, используя, в зависимости от особенностей языка программирования, либо завершители, либо разделители. Завершители и здесь оказываются удобнее. Завершитель просто записывается непосредственно в конце тела гнезда и тем самым повторяется ровно столько раз, сколько нужно.
      Для задания разделителя приходится прилагать дополнительные усилия. Разделитель размещается вслед за телом гнезда и отделяется от него служебным словом #DELIMITER. В собираемой программе выделенный таким образом текст разделителя дописывается в конец тела при всех повторениях цикла, кроме последнего.
      Если конструкцию наборного гнезда органично внедрить в язык программирования, то часть DELIMITER может и не понадобиться: препроцессор сам сумеет подобрать разделитель или завершитель в зависимости от синтаксической позиции гнезда. Однако пока рассчитывать на такое сращивание наборных гнезд со средой не приходится, и в последующих примерах разделители будут указываться явно.

      5.2.3. Пример: переключатель в Си. Рассмотрим небольшой пример. Пусть в некоторой программе на языке Си записан оператор переключателя (switch), и пусть требуется регулярно дополнять этот переключатель новыми ветвями. Как сделать безболезненным для окружения процесс добавления новых ветвей? Или, конкретнее, как оформить в виде наборного гнезда совокупность ветвей переключателя?
      Сначала несколько пояснений для читателей, не знакомых с Си. Оператор переключателя является одной из популярных разновидностей оператора выбора. Переключатель имеет вид (рубленым полужирным шрифтом, как обычно, выделены нетерминальные символы):

switch  (выражение) {
      case  константа :  действия
      case  константа :  действия
      •    •    •
      default :  действия
}

Его выполнение заключается в сравнении значения выражения с константами во всех ветвях case и в передаче управления на действия, помеченные константой, равной значению выражения. Если ни одна из констант в ветвях case не равна значению выражения, то выполняются действия, связанные с меткой default [Керниган, 1992].
      Формируя наборное гнездо, каждой ветви переключателя сопоставим многосвязный модуль, состоящий из двух компонентов: константы (CONSTANT) и действий (ACTIONS). Набору таких модулей присвоим имя BRANCH. Тогда переключатель, оснащенный наборным гнездом, можно записать в виде:

•    •    •
switch(x) {
      #HORIZON  BRANCH
            case #BRANCH.CONSTANT:
                  #BRANCH.ACTIONS
                  break;
      #END_OF_HORIZON
      default:
            printf("\n Ошибка \n");
            exit(1);
}
•    •    •

      Здесь в роли разграничителей односвязных компонентов выступают служебное слово case, символ «:» (двоеточие) и оператор break, обеспечивающий выход из переключателя после выполнения действий. (В Си, как правило, используются завершители, и поэтому конструкция #DELIMITER обычно не требуется.) Можно было бы обойтись без разграничителей, включив их в тексты однородных модулей. Существует, однако, ряд весомых соображений в пользу размещения разграничителей в тексте гнезда.
      Во-первых, гнездо с разграничителями выглядит существенно нагляднее. В частности, четче очерчиваются синтаксические и функциональные границы односвязных компонентов модулей, можно даже сказать, что в этом случае тело гнезда представляет собой каркас для компонентов модулей однородного набора.
      Во-вторых, вынося разграничители в гнездо, мы избегаем их размножения в однородных модулях, т. е. избавляемся от дублирования.
      В-третьих, тексты модулей, разгруженные от разграничителей, уменьшаются в размерах. Благодаря этому можно более компактно показать, скажем, совокупность констант нашего переключателя (BRANCH .CONSTANT) на экране дисплея.
      Наконец, в-четвертых, если однородные модули разграничиваются разделителем, а не завершителем, то единственное приемлемое решение — вынесение разделителя в конструкцию #DELIMITER. Ведь в противном случае разделитель должен быть включен в тексты всех модулей, кроме первого или последнего. Тем самым неявно фиксируется последовательность подстановки модулей, что безусловно нетехнологично.
      Оформив наборное гнездо, мы приобрели возможность не только безболезненно пополнять набор однородных модулей, но и просматривать на экране в компактной форме любой интересующий нас «срез» их односвязных компонентов. В то же время оформление и сопровождение появившихся таким образом однородных модулей, вероятно, потребуют от разработчика некоторых дополнительных усилий. О том, чтобы усилия эти оказались не слишком велики, должны позаботиться средства системной поддержки.
      В частности, имеет смысл предоставить средства для ввода и редактирования текста однородного модуля в рабочем контексте, т. е. в окружении наборного гнезда. Кроме того, полезно было бы обеспечить просмотр программы заданной конкретной конфигурации в окончательном виде, с подставленными в гнездо текстами однородных модулей.

Далее

Рейтинг@Mail.ru