6.4. Модуль расширения
6.4.1. Требования к модулю расширения. Итак, мы познакомились с несколькими базовыми механизмами, обслуживающими безболезненное расширение программы: вариантное гнездо и сменные модули, наборное гнездо и регулярный или рассредоточенный однородный набор. Вернемся теперь к обсуждавшимся в гл. 2 проблемам модуляризации и попытаемся определить, какие части программы заслуживают того, чтобы называться «модулями расширения».
Для расширения программы могут быть использованы самые различные формы материалов. Однако термин «модуль расширения» подразумевает особую легкость подключения и отделения, этим термином имеет смысл обозначить лишь те формы, которые обеспечивают безболезненность подключения данного материала к программе, равно как и безболезненность его удаления.
Таким образом, к модулям расширения следует отнести сменные модули вариантного гнезда и элементы регулярного однородного набора. Модули могут быть составными. В их текстах допускаются объявления элементов рассредоточенных наборов, а также вложенные вариантные и наборные гнезда.
Можно посмотреть на модуль расширения и под несколько иным углом зрения. Пусть в принимающем программном фонде имеется несколько однородных наборов. Тогда модуль расширения имеет право дополнить каждый из этих наборов любым количеством новых однородных элементов. Безболезненность означает, что весь эффект включения модуля в программный фонд сводится к дополнению имеющихся однородных наборов, а эффект исключения модуля к исключению из тех же наборов принадлежащих ему элементов.
При проектировании расширяемой программы происходит столкновение двух тенденций. С одной стороны, вычленяя направления будущего развития, разработчик стремится к функциональной самостоятельности добавляемых модулей, поскольку тем самым сокращается объем интерфейсных соглашений, упрощаются спецификации, улучшается структура программы и т. д. С другой стороны, хочется, чтобы добавляемые модули врастали в программу достаточно органично, чтобы их функциональная самостоятельность не мешала бы восприятию программы как единого целого и не наносила бы заметного ущерба ее эффективности. Участие модуля расширения в нескольких однородных наборах принимающей программы позволяет найти компромисс, примирить эти две тенденции.
Наиболее тесное сращивание нового модуля с принимающей программой достигается, если реализуется их взаимопроникновение, если отдельные составляющие модуля непосредственно «вживляются» в текст программы. Взаимопроникновение модуля и программы дело, вообще говоря, не простое. В крупной программе обычно вычленяется целый ряд функциональных слоев, и если добавляемый модуль достаточно нетривиален и желает органично вписаться в свое новое окружение, то он, вероятно, не поглотится единственным из них, а должен будет так или иначе оказать влияние на несколько слоев. Именно благодаря участию модуля в нескольких слоях завязываются горизонтальные межмодульные связи, которые придают программе законченность, объединяя ее компоненты в дееспособный сплоченный коллектив единомышленников. Поэтому так важно при проектировании программы придать функциональным слоям форму расширяемых однородных наборов.
Подобные схемы построения расширяемых систем характерны не только (и даже не столько) для программирования они окружают нас повсюду, и там мы принимаем их как нечто само собой разумеющееся. Приведем несколько аналогов модуля расширения из различных областей человеческой деятельности.
6.4.2. Пример из области, далекой от программирования. Проще всего выглядит пример аналог использования сменного модуля для вариантного гнезда. Пусть вам требуется просверлить некоторое отверстие, и в вашей мастерской имеется необходимая для этого дрель (каркас), но отсутствует сверло нужного диаметра. И тут вы узнаете, что у вашего приятеля есть такое сверло (модуль расширения сменный модуль). Вы просите его у приятеля, закрепляете в патроне (вариантном гнезде) дрели (разумеется, интерфейсы патрона и сверла должны быть согласованы), и задача решена. Отметим, что в данном случае сверло выступает в роли многократно используемого (и вами, и приятелем) компонента.
Теперь посмотрим, что нового привнесет усложнение рассматриваемой среды. Пусть вы покупаете некоторый инструмент (крупный модуль расширения), пополняя возможности своей мастерской (программы). В этом случае покупка приобретает несколько измерений, слоев: сам инструмент надо поместить на полку в кладовке; в ящике письменного стола собираются в одной папке все спецификации, паспорта, гарантии; еще один слой запись в гроссбухе домашнего хозяйства; еще один напоминания о периодической профилактике, которые, вероятно, соединятся в записной книжке с напоминаниями о внесении квартплаты, о поздравлении друга с днем рождения и т. п. Таким образом, в момент вселения (инсталляции) инструмента происходит рассредоточение его составляющих по нескольким односвязным хранилищам (функциональным слоям однородным наборам): кладовка, папка документов, гроссбух, записная книжка. Чем четче очерчена однородность элементов в каждом из хранилищ, тем больше порядка в доме, тем органичнее и проще проходит и вселение инструмента, и последующая его эксплуатация.
6.4.3. Пример из области «железа» IBM PC. Здесь аналогом механизма вариантного гнезда и сменного модуля являются отношения между системным блоком и клавиатурой. Достаточно соединить клавиатурный кабель с предназначенным для него разъемом на тыльной стороне системного блока и сборка завершена. Благодаря четкой функциональной специализации клавиатуры и унификации разъема (точнее, интерфейса) любой системный блок, вообще говоря, успешно соединяется с любой клавиатурой.
Отношения между системным блоком и клавиатурой образец аккуратного инженерного решения, облегчившего жизнь и производителям «железа», и пользователям. Однако подключение клавиатуры малоинтересная, рутинная операция. Системный блок не может жить без клавиатуры, клавиатура без системного блока, от их соединения никто не ждет какой-либо новой нетривиальной функциональности, выходящей за пределы того, что еще при царе Горохе было заложено в архитектуру IBM PC.
Иное дело подключение, например, устройства чтения компакт-дисков. При проектировании архитектуры вряд ли ожидалось появление именно такого устройства, а тем не менее для него подготовлено все необходимое. Само устройство располагается в одном из однородных гнезд, называемых по инерции гнездами «пятидюймовых дисководов», но испокон века исправно служащих для размещения самых невероятных вновь изобретаемых конструкций. Питание устройства обеспечивается посредством подключения к одному из однородных универсальных силовых разъемов, обмен информацией к одному из однородных интерфейсных разъемов. Взаимопроникновение системного блока и нового устройства дополняется тем, что устройству может быть выделено одно или несколько однородных системных прерываний. Наконец, драйвер устройства пополняет открытый операционной системой однородный ряд драйверов.
Иначе говоря, устройство чтения компакт-дисков при подключении делегирует своих представителей в целый ряд слоев системного блока: слой «пятидюймовых дисководов», слой интерфейсного кабеля («шлейфа»), слой прерываний и т. д. Отношения в каждом из слоев несколько проще, чем полный свод отношений в упомянутой выше паре «системный блок клавиатура», но в совокупности слои позволяют организовать не менее плотное и эффективное взаимодействие.
Когда говорят о том, что каждый пользователь персонального компьютера имеет возможность собрать, точнее, сконструировать для себя уникальную по своим функциональным возможностям аппаратную конфигурацию, то, разумеется, имеют в виду не подключение той или иной клавиатуры. Конструирование базируется на размещении требующихся разнообразных устройств в однородных гнездах пятидюймовых дисководов, в однородных разъемах шины PCI или других подобных подключениях, основанных на технике делегирования нескольких представителей подключаемого устройства в различные слои системного блока. Именно эта техника позволяет достаточно свободно развивать аппаратуру компьютера, и нечто подобное, похоже, требуется для реализации расширяемости программы.
6.4.4. Пример из области операционных систем. Только что рассмотренная схема подключения устройства чтения компакт-дисков очень похожа на организацию инсталляции нового крупного приложения в операционной системе. Для обеспечения эффективного взаимодействия с принимающей средой новое приложение делегирует операционной системе не только свой выполняемый код, но и драйверы, запросы на определенные ресурсы и т. д., пополняя однородные слои подобных элементов, сформированные приложениями-предшественниками. Многообразие участвующих в инсталляции однородных слоев определяет глубину интеграции приложения и операционной системы.
Приведенные примеры оборудование мастерской, оснащение персонального компьютера подводят нас к выводу о том, что расширяемость системы должна опираться на четкое вычленение расширяемых функциональных слоев однородных наборов. Однако в наших примерах такое вычленение происходило стихийно, из очевидных соображений здравого смысла. Авторы архитектуры расширяемой системы не формулировали свои результаты в терминах однородных наборов.
Для систем, далеких от программирования, это небольшая потеря. Но в случае операционной системы «стихийная», неосознаваемая однородность влечет за собой уже заметные издержки. (О трудностях инсталляции/деинсталляции приложений в конкретной операционной системе MS Windows разговор пойдет в следующей главе, в разд. 7.3.) Однородных наборов «не видят» а следовательно не ставится задача создания и регулярного использования средств их поддержки. И далее, без средств поддержки недостижима безболезненность подключения, т. е. инсталлируемые приложения превращаются не в модули расширения, а в аморфный программный материал, трудно налаживающий/прерывающий отношения с принимающей средой.
Рассмотрим теперь небольшой пример рациональной организации модуля расширения.
6.4.5. Компилятор с расширяемого языка. Немного разовьем пример из разд. 5.3.1. Пусть имеется компилятор с расширяемого языка, и пусть требуется добавить к нему новый модуль, реализующий еще одну языковую конструкцию. В момент добавления модуль распадается на компоненты, получающие прописку в нескольких слоях: новые зарезервированные слова, возможно, пополнят таблицы лексического анализатора, добавится новый блок в синтаксическом анализаторе и, наконец, блок в генераторе кода (рис. 6.2).
Рис. 6.2. Подключение к компилятору нового компонента
В разд. 5.3.1 речь шла только о синтаксическом анализаторе и генераторе кода, теперь основное внимание уделим таблицам лексического анализатора. Но прежде напомним некоторые сопутствующие технологические соображения.
Разумеется, разнесение нового модуля по слоям не должно означать, что он перестает существовать как единое целое. Разработчик вправе в любой момент посмотреть в полном объеме, что же такое он внедрил в свою программу при подключении модуля. Кроме того, модуль сам по себе является еще одним, не менее интересным функциональным слоем программы (ортогональным к слоям, на которые он распадается), и с точки зрения наглядности возможность его систематического просмотра несомненно полезна. Наконец, если в какой-то момент от модуля придется отказаться, то сохранение его единства позволит вместо сложного и чрезвычайно ненадежного отката разрозненных изменений применить технологически безупречную операцию удаления модуля как целого.
В то же время ничуть не менее весомые аргументы говорят в пользу расчленения модуля. При анализе исходного текста разработчик компилятора должен иметь возможность просмотреть в компактной форме текущее состояние таблицы зарезервированных слов реализуемого языка, не перескакивая при этом через не интересующие его в данный момент блоки синтаксического анализа и генерации кода. Реализация упомянутой таблицы при текстуальном соединении составляющих ее слов окажется более эффективной. Оба этих соображения говорят о том, что из добавляемого модуля нужно извлечь содержащиеся в нем зарезервированные слова и поместить их, грубо говоря, в исходный текст данной таблицы. (Отметим, что даже если пренебречь интересами программных слоев, все равно находятся модули расширения, без расчленения которых обойтись нельзя: например, модуль может включать в себя графический или звуковой фрагмент, а они обычно не встраиваются в текст программы, а оформляются как самостоятельные файлы, хранящиеся где-то в стороне.)
Нетрудно видеть, что имеющиеся в распространенных алгоритмических языках конструкции, и в частности классы, мало чем могут помочь в подобающем оформлении распадающихся на слои модулей. Возможности рассылки составляющих модуля по нескольким слоям в этих языках либо вообще отсутствуют, либо весьма ограничены. Такому положению трудно найти рациональное объяснение, поскольку потребность в рассылке возникает достаточно часто, а ее реализация, как мы сейчас убедимся, представляется относительно несложным делом.
Итак, примем для определенности, что компилятор с расширяемого языка написан на языке Си, и попытаемся рационально организовать подключение к нему модуля расширения, реализующего новую конструкцию языка. Три перечисленных выше составляющих реализации новой конструкции расширяемого языка, рассылаемые по различным слоям, оформляются как три односвязных компонента (ЛЕКС, СИНТ, ГЕН) многосвязного модуля расширения, пополняющего однородный набор КОНСТР. О том, как поступить с компонентами СИНТ и ГЕН, подробно рассказано в разд. 5.3.1. Что же касается компонента ЛЕКС, то весь эффект его инсталляции сводится к пополнению набора зарезервированных слов, куда надо добавить от нуля до нескольких новых слов. Для реализации рассылки новых зарезервированных слов нам потребуется механизм рассредоточенного набора.
Пусть новая конструкция расширяемого языка содержит два зарезервированных слова: NewWord1 и NewWord2. Тогда компонент ЛЕКС объявляет два элемента рассредоточенного набора KEYWORD:
#INSTALL_IN KEYWORD
WORD : NewWord1
#INSTALL_IN KEYWORD
WORD : NewWord2
#END_OF_INSTALL
|
Элементы набора KEYWORD будут востребованы по крайней мере в двух точках программы. Во-первых, в интерфейсном модуле (h-файле), разделяемом лексическим и синтаксическим анализатором, где всем зарезервированным словам присваиваются номера. Во-вторых, в таблице зарезервированных слов лексического анализатора.
Пусть константе перечисления, задающей номер зарезервированного слова, присваивается имя, получаемое конкатенацией префикса «w_» и нумеруемого слова. Тогда спецификатор перечисления, записываемый в h-файле и определяющий эти константы, будет иметь вид:
enum {
#HORIZON KEYWORD
w_ ## #KEYWORD.WORD
#DELIMITER ,
#END_OF_HORIZON
};
|
Текст перечисления порождается при расширении наборного гнезда KEYWORD. Напомним, что оператор ## в Си означает конкатенацию двух лексем (периода компиляции). Таким образом, наш спецификатор перечисления после работы препроцессора должен превратиться в
enum { ... , w_NewWord1, w_NewWord2, ...};
Теперь посмотрим, как с помощью другого наборного гнезда формируется таблица зарезервированных слов:
char *Words[] = {
#HORIZON KEYWORD
##KEYWORD.WORD ,
#END_OF_HORIZON
};
|
Записанный в третьей строчке оператор # работает так же, как и в макроопределении Си, задавая заключение подставляемого на место переменной цикла текста в двойные кавычки. После обработки препроцессором таблица примет вид
char *Words[] = { ... , "NewWord1", "NewWord2", ... , };
где зарезервированные слова появляются строго в той же последовательности, что и константы в только что рассмотренном перечислении. С точки зрения синтаксиса, второе наборное гнездо отличается от первого отсутствием части #DELIMITER: запятая перебралась в тело гнезда, здесь она использована в роли завершителя, а не разделителя, что допускается языком Си.
Итак, проблема рассылки зарезервированных слов благополучно решена. С одной стороны, слова сохраняются в тексте модуля расширения, тем самым модуль становится легко обозримым и, кроме того, он может быть в любой момент вполне технологично удален из программы. С другой стороны, слова автоматически попадают во все нуждающиеся в них слои, и благодаря этому слои также приобретают наглядное и эффективное текстовое представление.
Безукоризненно четко проведена граница между модулем расширения, реализующим новую конструкцию языка, и принимающими его частями компилятора. Модуль только объявляет свои зарезервированные слова и не содержит ни малейшего упоминания о структурах принимающей программы, где все объявленные таким образом зарезервированные слова языка будут сведены воедино. Столь аккуратное расчленение программного материала нелегко было бы построить в традиционной операционной среде, в частности, опираясь на существующий сейчас аппарат классов.
6.4.6. Однородность и расширяемость. Существенным свойством элементов построенного выше набора зарезервированных слов была их однородность. Можно сформировать или набор идентификаторов, или набор фрагментов алгоритма, или набор каких-либо иных языковых конструкций, однако в пределах одного набора никаких вольностей не допускается, все элементы должны подчиняться строгим ограничениям. Именно благодаря однородности элементов удается свободно выстраивать их друг за другом в наборном гнезде.
Однородность набора, разумеется, заслуживает конструктивного оформления. Только в Си, с его беспечным отношением к статическому контролю типа, можно позволить себе ограничиться примитивными конструкциями, подобными рассмотренным выше. Более строгие языки потребуют в первую очередь недвусмысленных указаний относительно типа элементов для каждого из наборов. Но требование это легко будет удовлетворить: дооснащение предложенной схемы работы с однородным набором средствами спецификации типа элементов выполняется относительно просто и лишь незначительно усложнит синтаксис.
Главное достоинство однородного набора его расширяемость, открытость для пополнения новыми элементами. Если реализованы соответствующие средства поддержки, то однородный набор способен развиваться чрезвычайно легко, безболезненно: при подключении к набору новых элементов не требуется ничего редактировать. А это означает, вообще говоря, что все написанные и отлаженные ранее тексты алгоритмов при добавлении очередного элемента набора сохранят свою работоспособность. Если же теперь все воздействие вновь появившегося модуля на принимающую программу удастся свести к рассылке его составляющих в один или несколько однородных наборов, то и подключение модуля в целом будет проходить безболезненно, без какого бы то ни было редактирования текста принимающей программы.
Но дело не только в безболезненности подключения. Ничуть не менее важно, что при оформлении однородного набора находит конструктивное воплощение потенциальная точка роста программы, конкретизируется одно из возможных направлений ее развития. Более того, все дополнения, призванные развивать программу в данном направлении, приобретают строгие конкретные очертания. В частности, становится очевидным, как должен выглядеть добавляемый модуль, или по крайней мере его составляющая, делегируемая в данный однородный набор, т. е. относящаяся к данному направлению развития.
Оформление однородного набора оказывается полезным и в случае, если никаких подключений новых модулей не ожидается. С его помощью получают наглядное компактное представление все ортогональные слои программы, которые иначе неизбежно распались бы на далеко отстоящие друг от друга текстовые фрагменты.
Однородные конструкции влияют на программу настолько благотворно, что, как правило, идет на пользу делу даже некоторое насилие над природой модуля, позволяющее поставить его в строй, включить в однородный ряд. Так, в визуальном программировании основной ряд однородных экранных примитивов нередко с успехом пополняется объектами (типа таймера), не получающими визуального отображения в выполняемой программе. Сходные идеи выдвигал еще Петр I в своем указе от 9 декабря 1708 г.: «Подчиненный перед лицом начальствующим должен иметь вид лихой и придурковатый, дабы разумением своим не смущать начальства». А своими неповторимыми гранями модуль может блистать в других, специально отведенных для этого горизонтальных слоях.
* * *
Здесь заканчивается систематическое изложение вопросов построения расширяемых программ. Следующая, седьмая глава заключительная. В ней собрано несколько характерных примеров применения предложенных конструкций.
|