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

 Г л а в а  5
ОДНОРОДНЫЙ НАБОР
 
5.3. Двумерные структуры
 

 

5.3. Двумерные структуры

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


 
Рис. 5.1.  Отображение двумерной структуры на реальный язык.
 
— элементы двумерной структуры,
— односвязные конструкции языка

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

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

      5.3.2. Пример: основная надпись. Согласно отечественным стандартам (ГОСТ 2.104–68), в правом нижнем углу конструкторских документов располагается основная надпись (один из допустимых форматов основной надписи изображен на рис. 5.2).


 
Рис. 5.2.  Примерный вид основной надписи

Стандарт предписывает содержание и правила заполнения для всех граф основной надписи, в которых конструктор указывает (в текстовой форме) различные реквизиты документа и проектируемого изделия.
      В состав системы автоматизированной подготовки конструкторской документации обычно включается программа, поддерживающая заполнение граф основной надписи. На примере этой программы мы и проиллюстрируем некоторые двумерные конструкции.
      Что требуется от программы заполнения основной надписи? На экране дисплея сначала высвечивается контур надписи (подобно рис. 5.2). Пользователь должен получить возможность свободно перемещать курсор по всему полю основной надписи и заполнять или редактировать отдельные графы. При завершении заполнения или редактирования графы появившийся там текст подвергается контролю на соответствие с требованиями, предъявляемыми стандартом к данной графе.
      Как реализовать такую программу? Тут сразу видны контуры набора однородных модулей (вертикальных слоев), соответствующих отдельным заполняемым графам основной надписи. Назовем этот однородный набор ГРАФА. Каждый модуль однородного набора ГРАФА должен включать в себя, в частности, следующие односвязные компоненты:

      ПРОВЕРКА — фрагмент алгоритма, контролирующий корректность содержимого графы,
      ПОДСКАЗКА — текст подсказки, поясняющий назначение и правила заполнения графы,
      НАЧАЛО_X, НАЧАЛО_Y, КОНЕЦ_X, КОНЕЦ_Y — координаты графы на экране дисплея.

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

struct unit {
      char *fld_help;
      void (*fld_test)();
};

и запишем ее в отдельный модуль head.h, который будет вставляться во все упомянутые автономно транслируемые модули. Здесь и далее префикс fld_ (field — поле) добавлен к идентификаторам некоторых полей структур для того, чтобы облегчить изучение примера читателям, плохо ориентирующимся в Си.
      Теперь можно перейти к самим автономно транслируемым модулям. Начнем с модулей, обслуживающих отдельные графы. Для них надо будет сформировать наборное гнездо. Однако, в отличие от разбиравшихся ранее конфигураций (где гнездо содержалось внутри текста программы), тут каждое повторение цикла наборного гнезда должно порождать автономно транслируемый модуль.
      Для разграничения автономно транслируемых частей текста введем конструкцию #TRANSLATE, записываемую перед каждой автономно транслируемой частью. (По аналогии с завершителем (см. разд. 5.2.1) эту конструкцию следовало бы назвать «начинателем». Отсутствие устоявшегося термина для «начинателя» связано с тем, что авторы языков программирования обращаются к услугам такого рода разграничителей довольно редко. Объясняется это, по-видимому, стремлением следовать традиции естественного языка, где завершители (точка и др.) и разделители (запятая и др.) имеют форму, удобную для изображения в программе, в то время как начинатели (прописная буква, абзацный отступ, буквица, заголовок и т. п.) не вписываются в сложившийся стиль представления текста программы.)

#HORIZON  ГРАФА
      #TRANSLATE
      #include "head.h"
      static void test() {
            #ГРАФА.ПРОВЕРКА
      }
      struct unit #ГРАФА.NAME = {
            "#ГРАФА.ПОДСКАЗКА",
            test,
      };
#END_OF_HORIZON

      Здесь сначала подключается модуль head.h (обработку предложения #include лучше поручить конфигурационному препроцессору, но можно оставить и препроцессору Си), затем описывается локальная в модуле подпрограмма проверки содержимого графы (test) и, наконец, объявляется и инициализируется интерфейсная входная переменная — структура. Имя интерфейсной переменной совпадает с именем модуля однородного набора.
      В автономно транслируемом модуле, обеспечивающем работу с экраном, нас будет интересовать только сравнительно небольшая его часть, связанная с распознаванием того, в которой из граф основной надписи находится курсор (координаты курсора известны), и, кроме того, организующая обращение к подпрограмме проверки и выдачу подсказки. Для этого предлагается образовать массив (coord) структур, где поля структуры содержат координаты отдельной графы и ссылку на интерфейсную структуру этой графы. Таким образом, автономно транслируемый модуль приобретает вид:

#TRANSLATE
#include "head.h"
•    •    •
#HORIZON  ГРАФА
      extern struct unit #ГРАФА.NAME ;
#END_OF_HORIZON
•    •    •
struct {
      int x_begin;
      int y_begin;
      int x_end;
      int y_end;
      struct unit *fld_unit;
} coord[] = {
      #HORIZON  ГРАФА
            {
                  #ГРАФА.НАЧАЛО_X,
                  #ГРАФА.НАЧАЛО_Y,
                  #ГРАФА.КОНЕЦ_X,
                  #ГРАФА.КОНЕЦ_Y,
                  &#ГРАФА.NAME,
            },
      #END_OF_HORIZON
};
•    •    •
(*coord[i].fld_unit–>fld_test)();
•    •    •
. . . coord[i].fld_unit–>fld_help . . .
•    •    •

      Здесь, как и раньше, сначала подключается модуль head.h, определяющий интерфейсную структуру unit. Затем располагается наборное гнездо, с помощью которого объявляются внешними все интерфейсные структуры из модулей, обслуживающих отдельные графы. Далее следует обещанный массив структур coord, элементы которого инициализируются с помощью второго наборного гнезда. Завершают текст примеры работы с элементами интерфейсной структуры, извлекаемыми из массива coord: сначала вызов подпрограммы проверки корректности содержимого графы, а затем обращение к строке подсказки.
      Остается добавить, что большинство реализаций Си довольно либерально относится к использованию символа «,» (запятой) в качестве разграничителя списка инициализации: запятая может быть как завершителем, так и разделителем (см. разд. 5.2.2). В приведенном тексте запятые использовались как завершители. Однако любители разделителей могут легко избавиться от замыкающей запятой, воспользовавшись конструкцией #DELIMITER, в результате чего второе наборное гнездо в нашем примере примет следующий вид:

•    •    •
#HORIZON  ГРАФА
      {
            •    •    •
            #ГРАФА.КОНЕЦ_Y,
            &#ГРАФА.NAME
      }
#DELIMITER ,
#END_OF_HORIZON
•    •    •

      Чего же мы добились, применив в рассмотренной программе аппарат однородных наборов? Прежде всего, с помощью этого аппарата удалось полностью проявить изначальную двумерную программную структуру, которая в противном случае неизбежно потерялась бы во множестве разбросанных по программному фонду фрагментов.
      Появилась возможность вводить, модифицировать и просматривать отдельные односвязные модули набора в окружении их соседей как по горизонтальным, так и по вертикальным слоям. Можно, например, увидеть компактно собранными воедино тексты всех подсказок или, просматривая текст некоторой подпрограммы контроля, запросить координаты соответствующей графы на экране.
      Удалось в наглядной форме наборного гнезда зафиксировать единый каркас для всех модулей трансляции, отвечающих за отдельные графы. Если бы не эта конструкция, внешнее сходство таких модулей, проистекающее из их изначальной однородности, можно было бы впоследствии не заметить или же принять за случайное совпадение. Несколько менее существенно то, что каркас модулей «вынесен за скобки» и тем самым устранен источник возможного дублирования.
      Первое наборное гнездо из модуля обслуживания экрана позволило в компактной и надежной форме задать связи этого модуля с модулями обслуживания отдельных граф. Задание всех этих связей вручную потребовало бы довольно кропотливой работы и привело бы к появлению несообразно длинного монотонного участка исходного текста программы.
      Перечисление конкретных достоинств рассмотренного примера применения однородного набора можно продолжать еще долго. Но, опасаясь утомить читателя излишними подробностями, мы здесь решительно прервем это перечисление и перейдем к подведению итога. Главное достоинство приведенного решения можно сформулировать так: удалось навести порядок в достаточно ответственном и объемном разделе программного хозяйства. В самом деле, весь вовлеченный в двумерную структуру программный материал у нас аккуратно разложен по однородным полочкам, что не может не вызвать чувство глубокого удовлетворения у любого программиста, неравнодушно относящегося к своей профессиональной деятельности.
      Разумеется, по современным представлениям язык Си далек от идеала. В языке с объектной ориентацией можно было бы завести класс «графа», сосредоточив в нем все составляющие вертикального слоя. Программа приобретет несколько более аккуратный вид. Но, тем не менее, как ни отображай двумерную структуру на линейный текст — потери неизбежны. В нашем случае совокупность граф распадается на ряд объектов класса «графа», и разработчик утрачивает возможность работы с горизонтальными слоями. В частности, при анализе программы уже нельзя будет увидеть в компактной форме на экране совокупность подсказок или сводную таблицу координат граф. Даже получение ответа на вопрос «Сколько сейчас в программном фонде содержится модулей-граф?» во многих современных объектно-ориентированных средах вызывает заметные трудности.

Далее

Рейтинг@Mail.ru