Михаил Горбунов-Посадов
ИПМ им. М. В. Келдыша РАН, Москва
gorbunov@spp.keldysh.ru

Везде ли пригоден класс?
Примеры из разных областей
Многослойность и односвязность
Реализация рассылки
Однородный набор
Заключение
Литература

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

У программиста, впервые столкнувшегося с термином "многократное использование" ("reuse"), возникают самые разнообразные ассоциации. Иногда это - доступная всем подпрограмма вычисления синуса из стандартной библиотеки. Или вспоминается коллега, бросающий на ходу: "Я, кажется, года три назад решал нечто похожее на твою задачу. Пошарь у меня на диске, может быть тебе удастся там что-то и для себя приспособить". Подобное спонтанное многократное использование весьма маловероятно, чрезвычайно неэффективно и потому не заслуживает внимания. С синусом, напротив, все обстоит вполне благополучно: механизм многократно используемых подпрограмм известен уже несколько десятилетий, прекрасно себя зарекомендовал и не собирается сдавать позиции.

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

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

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

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

Если сборочное программирование придерживается объектно-ориентированного стиля, то упомянутым требованиям к многократно используемым конструкциям срединной области в какой-то мере отвечают классы. Класс как бы синтезирует в себе важные свойства подпрограммы и каркаса, обеспечивающие их многократное использование. С одной стороны, к методам класса обращаются для решения вспомогательных подзадач, и этим класс напоминает подпрограмму, лепесток формируемого дерева. С другой стороны, допускается доопределение виртуальных методов класса или переопределение обычных методов, и здесь класс играет роль ствола поддерева, каркаса, в гнездах которого размещаются вновь определяемые методы. Каркас (framework) обычно не ограничивается рамками одного класса - в современных разработках, как правило, применяются каркасы, построенные в форме библиотеки классов [1].

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

Везде ли пригоден класс?

Если внедряемый класс достаточно содержателен, сложен, он заставляет "плясать под свою дудку" всю остальную программу, поскольку она вынуждена подлаживаться под его нетривиальный интерфейс. Из-за нетривиальности интерфейсов иллюзорной представляется возможность компоновки достаточно крупной программы из независимо разработанных (ничего не знающих об интерфейсах друг друга) многократно используемых классов: эти классы, вероятнее всего, не смогут ужиться между собой. Строго говоря, несогласованность интерфейсов можно преодолеть с помощью переходников ("смазывающих" подпрограмм, брокеров). Однако изготовление переходников - дело весьма трудоемкое и деликатное, да и структура программы тут превращается в труднопроходимые дикие заросли, и поэтому переходники нельзя всерьез воспринимать как регулярный инструмент поддержки многократного использования.

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

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

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

Определенные результаты в направлении приспособления класса и объекта для нужд многократного использования получила компания Microsoft в своих разработках OLE, COM и ActiveX [2-3]. Но, к сожалению, речь в них идет именно о приспособлении класса и объекта, иначе говоря, априорно ограничивается область поиска перспективных многократно используемых программных конструкций. Разработчики Microsoft увлечены и другими идеями, неоправданно сужающими постановку проблемы многократного использования. Им хотелось бы объединять в одной программе многократно используемые компоненты, написанные на разных языках программирования, сделать компоненты независимыми от вычислительной платформы, а также обеспечить возможность распределенного выполнения компонентов. Для достижения искомых многоязычности, многоплатформенности и распределенности пришлось настолько жестко ограничить возможности взаимодействия компонента с принимающей программой, что их взаимопроникновение стало восприниматься как несбыточная мечта. Однако ничего несбыточного тут нет: как будет видно из последующих примеров, взаимопроникновение компонентов допускает достаточно компактную реализацию, которая открыла бы значительные резервы для вычисления и эффектного оформления многократно используемых частей.

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

Примеры из разных областей

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

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

Другой пример - из области аппаратуры IBM PC. Здесь системный блок и клавиатура по сути связаны отношением "каркас-подпрограмма". Достаточно соединить клавиатурный кабель с предназначенным для него разъемом на тыльной стороне системного блока - и сборка завершена. Благодаря четкой функциональной специализации клавиатуры и унификации разъема (точнее, интерфейса) любой системный блок, вообще говоря, успешно соединяется с любой клавиатурой. Впрочем, при более пристальном рассмотрении клавиатура воспринимается скорее как объект класса "клавиатуры", где методам соответствуют многочисленные программно-аппаратные соглашения, обеспечивающие реальное, довольно-таки сложное взаимодействие с системным блоком.

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

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

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

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

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

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

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

Рисунок 1.
Подключение к компилятору нового компонента

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

Многослойность и односвязность

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

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

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

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

Реализация рассылки

Итак, вернемся к компилятору с расширяемого языка, точнее, к добавлению реализации новой языковой конструкции. Рассмотрим только одну небольшую подзадачу: как организовать рассылку по своим местам вновь появившихся зарезервированных слов? Будем предполагать, что компилятор написан на языке Си [4], и попытаемся дополнить этот язык средствами, поддерживающими расслоение компонента. Воспользуемся тем, что в Си уже имеется понятие препроцессирования, и включим в число директив препроцессора еще три: #install_in, #set и #end_of_set.

Пусть для определенности новая конструкция расширяемого языка содержит одно зарезервированное слово - newword. Тогда в тексте ее реализации предлагается записать строку

#install_in keyword, newword

где #install_in - новая директива препроцессора Си, обеспечивающая включение второго операнда (newword) в набор зарезервированных слов, имя которого (keyword) задано первым операндом.

Элементы формируемого посредством директив #install_in набора keyword будут востребованы по крайней мере в двух точках программы. Во-первых, во включаемом файле, разделяемом лексическим и синтаксическим анализатором, где всем зарезервированным словам присваиваются номера. Во-вторых, в таблице зарезервированных слов лексического анализатора.

Пусть константе перечисления, задающей номер зарезервированного слова, присваивается имя, получаемое конкатенацией префикса "w_" и нумеруемого слова. Тогда спецификатор перечисления, записываемый в h-файле и определяющий эти константы, будет иметь вид:

enum { 
#set keyword 
 w_ ## keyword 
#end_of_set , 
}; 

Между строками #set и #end_of_set заключено наборное гнездо, представляющее собой цикл периода компиляции. Цикл повторяется столько раз, сколько содержится элементов в наборе, имя которого указано в директиве #set. Имя набора служит кроме того в качестве переменной цикла, которая последовательно пробегает все элементы набора. Каждое повторение цикла приводит к добавлению в формируемый текст строк, заключенных между строками #set и #end_of_set, где на место переменной цикла подставляется очередной элемент набора. В директиве #end_of_set указывается разделитель, записываемый между повторениями цикла. Напомним, что оператор ## в Си означает конкатенацию двух лексем (периода компиляции). Таким образом, наш спецификатор перечисления после работы препроцессора должен превратиться в

enum { ... , w_newword, ... }; 

Теперь посмотрим, как с помощью наборного гнезда формируется таблица зарезервированных слов:

char *words[] = { 
#set keyword 
 #keyword , 
#end_of_set 
}; 

Записанный в третьей строчке оператор # работает так же, как и в макроопределении Си, задавая заключение подставляемого на место переменной цикла текста в двойные кавычки. После обработки препроцессором таблица примет вид

char *words[] = { ... , "newword" , ... , }; 

где зарезервированные слова появляются строго в той же последовательности, что и константы в только что рассмотренном перечислении. Заметим, что в директиве #end_of_set отсутствует операнд: запятая перебралась в тело цикла, здесь она использована в роли завершителя, а не разделителя, что допускается синтаксисом Си.

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

Реализация директив #install_in, #set и #end_of_set несколько отличается от традиционной схемы работы препроцессора. Перед тем как начать расширение наборного гнезда #set ... #end_of_set, нужно выявить элементы указанного в нем набора. А для этого придется предварительно проанализировать все включаемые в строящуюся программу исходные тексты и выполнить расположенные там директивы #install_in. Такой же анализ надо провести и по интерактивному запросу разработчика, пожелавшего ознакомиться с полным списком элементов определенного набора. Кроме того, поскольку расширение наборного гнезда может оказаться достаточно нетривиальным, полезно было бы дооснастить препроцессор средствами интерактивного просмотра результатов его работы.

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

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

Однородный набор

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

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

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

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

Оформление однородного набора оказывается полезным и в случае, если никаких подключений многократно используемых компонентов не ожидается. С его помощью получают наглядное компактное представление все ортогональные слои программы, которые иначе неизбежно распались бы на далеко отстоящие друг от друга текстовые фрагменты [5-7]. Кроме того, к программе могут подключаться не только многократно используемые компоненты, но и одноразовые дополнения, и влияние однородного набора на организацию таких подключений также будет весьма благотворным.

Заключение

Итак, что же дает применение однородного набора? С одной стороны, его двери всегда гостеприимно распахнуты для приема новых многократно используемых компонентов. С другой стороны, однородный набор позволяет конструктивно и четко провести расчленение программы на модули, отвечающие за отдельные направления ее развития, и благодаря этой четкости и конструктивности у элементов набора существенно возрастают шансы на превращение в "независимые единицы программистского знания" [8], в многократно используемый программный материал.

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

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

Работа выполнена при поддержке РФФИ (грант 96-01-01138)

Литература

  1. Fayad M.E., Schmidt D.C. Object-oriented application frameworks // Comm. ACM. - 1997. - V.40, N.10. - P.32-38.
  2. Чеппел Д. Технологии ActiveX и OLE. - М.: Русская редакция, 1997. - 320 с.
  3. Роджерсон Д. Основы COM. - М.: Русская редакция, 1997. - 376 с.
  4. Керниган Б., Ритчи Д. Язык программирования Си. - М.: Финансы и статистика, 1992. - 272 с.
  5. Горбунов-Посадов М.М. Конфигурации программ. - М.: Малип, 1994. - 272 с.
  6. Горбунов-Посадов М.М. Безболезненное развитие программы. Открытые системы. - 1996. - # 4. - С.65-70.
  7. Горбунов-Посадов М.М. Система открыта, но что-то мешает // Открытые системы. - 1996. - # 6. - С.36-39.
  8. Цейтин Г.С. На пути к сборочному программированию. Программирование. - 1990. - # 1. - С.78-92.