3.6. Каркасный подход
Проницательный читатель, вероятно, давно догадался о том, что отношение автора к цепочечному подходу более чем прохладное. И для этого есть достаточно веские основания. Над цепочечным подходом постоянно витает какой-то романтический ореол, все время кажется, что с его помощью вот-вот удастся совершить впечатляющий рывок в теории и практике программирования.
Почти каждый профессиональный программист рано или поздно проходит через период увлечения цепочечным подходом. Но для большинства это увлечение заканчивается лишь обманутыми надеждами наиболее заметные результаты, полученные до сих пор в рамках данного направления, применимы только к относительно узкому классу задач и мало что дают программисту-практику.
Более перспективным представляется совершенно иной подход к построению пакета программ. К нему ведет следующее наблюдение.
В любой предметной области, требующей сколько-нибудь значительного объема работ по программированию, с течением времени складываются устойчивые представления о рациональной модульной организации. Рациональная организация, как правило, исключает регулярные перестановки модулей с одного места на другое (в то время как цепочечный подход основывался именно на таких перестановках). Более того, обычно выявляется некоторое консервативное многократно используемое подмножество модулей, состав и положение которых не меняются при переходах от одной версии программы к другой. За остальными (сменными) модулями также закрепляются фиксированные места, однако в формируемые версии включаются не все, а только некоторые из сменных модулей.
Такие конфигурационные представления и отражает каркасный подход к построению пакетов. При каркасном подходе любая формируемая из модулей пакета версия программы включает два компонента. Первый, постоянный компонент каркас, не меняющийся от версии к версии и несущий в себе гнезда для размещения сменных модулей. Второй, переменный компонент содержимое гнезд каркаса. Все многообразие формируемых версий достигается только за счет варьирования содержимого гнезд в разнообразных комбинациях.
Несмотря на столь аскетичные средства конструирования программ, в большинстве случаев каркасный подход оказывается много продуктивнее цепочечного.
3.6.1. Сопоставление с цепочечным подходом. Возможности выбора отдельных включаемых в формируемую программу модулей при каркасном подходе в некотором смысле беднее, чем при цепочечном. Дело в том, что варьируемые модули, как правило, пишутся в расчете на использование в конкретном гнезде каркаса (рис. 3.3).
Рис. 3.3. Каркасный подход.
каркас, гнезда и сменные модули
Поэтому при заполнении гнезда в качестве кандидатов рассматривают не все доступные модули пакета, а лишь относящиеся к данному гнезду. Напротив, при формировании цепочки любой модуль пакета может, вообще говоря, претендовать на роль очередного звена.
Иначе говоря, пользователь цепочечного пакета, формирующий конкретную конфигурацию программы, выполняет более сложную работу по выбору необходимых модулей, чем пользователь каркасного пакета. Кроме того, гнезда каркаса обычно отражают отдельные характеристики решаемой задачи, а сменные модули допустимые значения данных характеристик. Благодаря этому с точки зрения пользователя задание конкретной конфигурации выглядит еще проще: оно превращается в указание значений характеристик задачи.
Оформление задания для каркасного пакета можно проиллюстрировать на примере описания конкретной конфигурации программы с двумя вариантными гнездами, рассмотренном в разд. 1.8.4. Там описание имело вид:
МЕТОД | < ЭЙЛЕР |
ДОСТУП | < ХЕШИРОВАНИЕ |
где МЕТОД и ДОСТУП имена гнезд (или характеристик задачи), а ЭЙЛЕР и ХЕШИРОВАНИЕ имена сменных модулей (значений характеристик).
Набор подобных указаний полностью определяет необходимую конкретную конфигурацию. Для ее сборки понадобятся сравнительно скромные системные средства, несоизмеримые по сложности с нетривиальным алгоритмическим аппаратом, обслуживающим автоматическое планирование вычислений или вызов процедур по образцу.
В то же время способы описания требуемой конкретной конфигурации в каркасном подходе и в цепочечном подходе с применением автоматического планирования вычислений в некотором смысле близки. И там, и тут пользователь пакета формулирует то, что он хочет получить, не конкретизируя, как, т. е. с помощью каких алгоритмов и программ, пакет должен достичь искомого результата. Подобные формы описания называются непроцедурными.
Для пакета с автоматическим составлением цепочки непроцедурность описания конфигурации достаточно очевидна. Так, в примере, разобранном в разд. 3.3.2, пользователь пакета указывал только известные и искомые величины (время, сила, масса; путь) и ни слова не говорил о программе, которая будет работать с этими величинами.
Для каркасного подхода, на первый взгляд, все обстоит ровно наоборот. В описании конкретной конфигурации явно указываются составляющие программу модули (в нашем примере ЭЙЛЕР, ХЕШИРОВАНИЕ). Однако такая запись описания выглядит как указание модулей только в глазах разработчика пакета.
Пользователю же каркасного пакета представляется, что в описании конфигурации он никак не соприкасается ни с алгоритмом, ни с программой, а задает одни лишь характеристики решаемой задачи. Действительно, если в упомянутом примере еще можно усмотреть явное указание методов решения, то в описании вида
МАТЕРИАЛ | < СТАЛЬ |
ТОЧНОСТЬ | < ДВОЙНАЯ |
алгоритмическая подноготная, связанная с гнездами каркаса и сменными модулями, для непосвященного уже практически незаметна.
Такое двуединое восприятие описания конкретной конфигурации характерно только для каркасного подхода. При цепочечном подходе в описании либо явно строится алгоритм (как, например, конвейер в разд. 3.2.1), либо другая крайность фигурируют только понятия, имеющие к формируемому алгоритму весьма отдаленное отношение (известные и искомые величины при автоматическом планировании разд. 3.3.2).
Переходя к сопоставлению формируемых выполняемых программ, заметим, что каркасный подход (в отличие от цепочечного см. разд. 3.2.3) вполне согласуется с требованиями объектно-ориентированного программирования, или, точнее, никогда не вступает в конфликты с этими требованиями. «Бесконфликтность» каркасного подхода основана на том, что он не навязывает никаких межмодульных связей периода выполнения, ему практически безразличны используемые управляющие структуры. Поэтому, в частности, гнезда каркаса легко вписываются в структуру модулей, реализующих отдельные классы и объекты, или же охватывают какие-либо надмодульные конструкции, нигде не препятствуя сложившимся схемам взаимодействия методов или сопрограмм.
Тут мы подошли к основному отличию двух подходов. Ограниченность применения цепочечного подхода проистекает в конечном итоге отнюдь не из слабости возможных форм описания конфигурации и не из сложности алгоритмического аппарата. Средства задания цепочек в существующих пакетах достаточно удобны, а, скажем, используемый для автоматического планирования ассоциативный механизм широко применяется и при каркасном подходе и оказывается здесь весьма продуктивным.
Главная беда цепочечного подхода, как уже не раз подчеркивалось, излишне жесткий, ущербный конфигурационный ориентир. Напротив, каркасный подход предоставляет практически полную свободу в проектировании структуры выполняемой программы и благодаря этому оказывается более предпочтительным для подавляющего большинства предметных областей.
3.6.2. Типы гнезд. Структура программы, изображенная на рис. 3.3, иллюстрирует только самое общее представление о каркасном подходе. Пакеты, опирающиеся на этот подход, нередко весьма несхожи между собой. Разнообразие пакетов во многом определяется отличиями используемых в них типов гнезд.
Прежде всего, обычно допускается вложенность гнезд. Вложенность означает, что сменные модули, помещаемые в гнезда каркаса, могут, в свою очередь, содержать гнезда и т. д. Иначе говоря, наряду с каркасом программы в целом, могут существовать и каркасы включаемых в нее сменных модулей. Допущение вложенности существенно увеличивает мощность каркасного пакета, но несколько осложняет реализацию.
Иногда бывает удобно объявить некоторое подмножество гнезд каркаса одним обобщенным гнездом. Такое гнездо называется многосвязным (в отличие от одиночных односвязных гнезд). Происхождение многосвязных гнезд можно пояснить на следующем несложном примере.
Пусть известно, что некоторую задачу надо будет многократно решать, применяя то один, то другой численный метод. И пусть всем таким численным методам сопоставлены процедуры, у которых совпадают не только заголовки, но и определенные части тел: начальные, промежуточные и завершающие действия. Тогда для размещения сменных модулей, реализующих различные методы, потребуются два гнезда: первое между начальными и промежуточными действиями, второе между промежуточными и завершающими (рис. 3.4).
Рис. 3.4. Многосвязное вариантное гнездо.
каркас, , гнезда и сменные модули, многосвязные модули
Поставленная задача, разумеется, может быть решена и с помощью двух односвязных гнезд. Однако в таком случае останется непроявленной генетическая близость этих гнезд, что приведет к определенным трудностям при последующих эксплуатации и развитии пакета. Появление же многосвязного гнезда вносит полную ясность: дополнение пакета новым методом требует реализации двух сменных модулей, переключение формируемой версии программы на другой метод расчета синхронной смены содержимого двух гнезд.
Многосвязному гнезду очевидным образом сопоставляются многосвязные модули комплекты сменных модулей, синхронно заполняющих многосвязное гнездо (см. рис. 3.4). Использование термина «модуль» для обозначения такого комплекта несколько непривычно для традиционного программирования. Оно опирается на расширенное толкование модуля, предложенное в гл. 2.
Имеет смысл присвоить многосвязному модулю самостоятельное имя. Именно это имя должно фигурировать в описании конкретной конфигурации, например в виде назначения
имя_многосвязного_гнезда < имя_многосвязного_модуля
Компоненты комплекта, т. е. односвязные компоненты многосвязного модуля, приобретают в этом случае составные имена вида
имя_многосвязного_модуля . имя_компонента
где имя_компонента имя односвязного гнезда (входящего в состав многосвязного), для которого предназначен данный компонент многосвязного модуля.
Наряду с рассмотренными выше вложенностью и связностью важнейшей характеристикой гнезда является способ его заполнения. По способу заполнения различают вариантные и наборные гнезда.
До сих пор в нашем изложении фигурировали только вариантные гнезда, впервые появившиеся еще в первой главе при конструировании специализированных средств для оформления варианта (разд. 1.8). Особенностью вариантного гнезда является то, что на его место при формировании конкретной конфигурации программы всегда подставляется ровно один сменный модуль.
В отличие от вариантных, наборные гнезда каркаса предназначаются для подстановки на их место сразу нескольких модулей пакета. (Как и ранее, предполагается, что все подставляемые модули написаны специально для данного гнезда.) Применение наборных гнезд проиллюстрируем сначала на небольшом примере.
Пусть алгоритм решения некоторой оптимизационной задачи предполагает многократные переключения (в ходе выполнения) с одного метода поиска экстремума на другой. И пусть в распоряжении пользователя пакета находится широкий выбор модулей, реализующих такие методы. Однако в каждом конкретном расчете целесообразно использовать лишь некоторое подмножество имеющихся модулей, и только эти модули должны войти в формируемую конкретную конфигурацию программы. Тогда в надлежащем месте каркаса программы должно быть расположено наборное гнездо, в которое в соответствии с указаниями пользователя будут подставлены модули, вошедшие в выбранное подмножество.
Обычно тексты модулей, размещаемые в наборном гнезде, не просто конкатенируются (т. е. записываются непосредственно один за другим), а помещаются предварительно в некоторые обрамляющие конструкции или же снабжаются разделителями. Такой способ формирования окончательного текста во многом напоминает используемые в макрогенераторах циклы периода компиляции. Но в качестве элементов перечисления тут выступают не явно выписываемые элементы текста, а модули, набор которых указывается в описании конкретной конфигурации программы.
Описание конкретной конфигурации здесь может быть оформлено в том же стиле, что и для вариантных гнезд. Например, для рассмотренной задачи можно задать назначение
ОПТИМИЗАЦИЯ < ( ГРАДИЕНТ , КООРД , ОВРАГ)
где ОПТИМИЗАЦИЯ имя наборного гнезда, а ГРАДИЕНТ, КООРД, ОВРАГ имена вставляемых модулей.
Наборные гнезда нередко бывают многосвязными. Для иллюстрации этого вновь воспользуемся только что приведенным примером. Пусть каждый оптимизационный метод, помимо собственно алгоритма, снабжается также условиями применимости и инструкцией. Тогда условия применимости и тексты инструкций потребуют, вероятно, двух различных, далеко отстоящих друг от друга гнезд каркаса.
Условия применимости, по-видимому, следует собрать в том месте выполняемой программы, где происходит переключение с одного оптимизационного метода на другой. Тексты инструкций можно объединить с другими поясняющими текстами, составив тем самым руководство к программе, которое пользователь будет изучать перед ее запуском или во время ее выполнения.
Преимущества объединения трех компонентов реализации метода (собственно алгоритма, условий применимости и текста инструкции) в единый модуль достаточно очевидны. Такой модуль дает ясное и полное представление о всех аспектах реализации метода, именно такими модулями должен оперировать разработчик при включении метода в пакет, равно как и при исключении метода.
Насколько необходимы здесь многосвязные модули и наборные гнезда? Некоторое приближение к искомому модулю можно было бы получить и в традиционной объектно-ориентированной инструментальной среде. Можно образовать класс «метод поиска экстремума», где, например, условия применимости и собственно алгоритм будут представлены как виртуальные элементы. Тогда наш модуль реализуется в форме объекта, конкретизирующего эту виртуальность. Но работать с таким объектом будет сложновато: современный аппарат классов, к сожалению, не всегда эффективно справляется с задачей последующего слияния в единой конструкции разбросанных по объектам условий применимости или текстов инструкций.
Кроме того, классы обычно не интересуются связями между однородными объектами, а специализированные средства, поддерживающие многосвязные гнезда, позволяют просматривать «горизонтальные срезы» сменных частей. Например, можно увидеть одновременно (в форме линейного текста или таблицы) всю совокупность условий применимости имеющихся оптимизационных методов, даже если и в исходном тексте, и в выполняемой программе эти условия далеко отстоят друг от друга.
Сведения о гнездах, приведенные в данном разделе, далеко не полны. Материалы, раскрывающие и уточняющие особенности тех или иных типов гнезд, будут постоянно встречаться в последующих главах. Однако содержащейся здесь информации достаточно для того, чтобы сделать некоторые заключения о безболезненности каркасного подхода.
3.6.3. Безболезненность при каркасном подходе. Основной механизм, посредством которого происходит развитие пакета программ при каркасном подходе, пополнение множества сменных модулей, размещаемых в гнездах каркаса. Каркасный подход обеспечивает безболезненность такого развития. Строго говоря, безболезненно может развиваться и каркас, но за счет иных механизмов, не имеющих непосредственного отношения к каркасному подходу. Заслугой каркасного подхода в обеспечении расширяемости пакета является только безболезненное пополнение содержимого гнезд, и ниже пойдет речь именно об этом направлении развития.
Тут вновь может сложиться впечатление, что каркасный подход уступает цепочечному, теперь уже по объему сферы безболезненного развития. Ведь при цепочечном подходе вновь появившийся безболезненно подключаемый модуль может быть внедрен, вообще говоря, в любое место цепочки, т. е. формируемая конкретная конфигурация программы может быть произвольно изменена. При каркасном подходе сфера безболезненного развития пакета несколько уже.
Однако на практике обычно сравнительно легко удается еще на ранних стадиях проектирования выделить области локализации предполагаемых направлений развития программы (т. е. точки роста пакета) и оформить их в виде гнезд каркаса. Тогда основной объем работ по развитию пакета придется на написание новых сменных модулей для существующих гнезд, хотя, разумеется, непредвиденные изменения могут затронуть и каркас.
Зато для большинства выполняемых изменений создание нового модуля приобретает регулярный характер, оно идет значительно проще и быстрее, чем, скажем, при цепочечном подходе. Для модуля подготовлена прекрасная почва: определены все его внешние связи, специализированные средства предоставляют благоприятную среду (контекст) для ввода его текста. Кроме того, некоторые неясности можно разрешить, обратившись к аналогам ранее созданным сменным модулям, предназначенным для данного гнезда.
Вернемся теперь к собственно безболезненности. То, что для вариантного гнезда подключение еще одного сменного модуля происходит безболезненно, было показано еще в первой главе (разд. 1.8). В самом деле, подключение нового модуля не влечет за собой изменений текстов существующих, новый модуль не может быть неявно включен в отлаженные ранее конкретные конфигурации программы и, таким образом, не может нарушить их работоспособность.
С наборными гнездами все обстоит несколько сложнее. Описывая конкретную конфигурацию формируемой программы, для задания требуемого содержимого наборного гнезда можно использовать два механизма: перечислительный и ассоциативный. Безболезненность существенно зависит от того, которому из двух механизмов отдано предпочтение. Эта проблема для произвольной задачи сборки подробно обсуждалась в разд. 3.4. Воспроизведем сформулированные там выводы применительно к заполнению наборного гнезда.
При перечислительной схеме в описании конфигурации явно перечисляются имена сменных модулей, размещаемых в гнезде. (Пример перечислительного описания можно найти в разд. 3.6.2, где перечислялись имена подключаемых методов оптимизации.) Если новые модули, пополняющие программный фонд, не должны участвовать в отлаженных ранее версиях программы, то с точки зрения безболезненности заполнение наборного гнезда по перечислительной схеме ничем не отличается от заполнения вариантного гнезда. Повторив приведенные выше рассуждения, касавшиеся вариантного гнезда, можно сделать вывод и о безболезненности подобного применения перечислительной схемы.
Однако, если вновь создаваемые сменные модули нужно подключать к имеющимся версиям, то перечислительная схема теряет даже свойство безболезненности для окружения. Ведь при этой схеме приходится вручную редактировать существующий первичный объект описание конкретной версии программы, содержащее перечень входящих в нее модулей.
При ассоциативной схеме в наборном гнезде размещаются либо все имеющиеся в пакете модули, предназначенные для этого гнезда, либо только те из них, которые обладают некоторыми свойствами (атрибутами), указанными в описании конфигурации. Тут новый сменный модуль всегда может сразу же включиться в существовавшую ранее версию программы, поскольку назначение его производится не непосредственно (путем указания имени), а неявно (путем указания свойства).
Достоинства и недостатки неявного (ассоциативного) подключения модуля уже разбирались в разд. 3.4. С одной стороны, подключение происходит безболезненно для окружения. Ни соседние модули, ни тексты описаний конкретных конфигураций не меняются. Тем не менее, новый модуль не только органично вливается в пакетное окружение, но и, возможно, сразу же на равных правах со всеми включается в работу.
С другой стороны, требованию безболезненности для работоспособности ассоциативная схема не удовлетворяет. Причина в том, что если в подключаемом модуле имеются ошибки, то его появление (из-за неявного указания) в ранее отлаженной версии программы может привести к потере работоспособности.
3.6.4. Сменный модуль. Внешний вид текста сменного модуля поначалу вызывает некоторое недоумение. Привыкший к модулям трансляции глаз программиста ищет обычную их атрибутику (обрамляющие скобки, спецификации параметров, переменных и др.) и не находит ее. Сменный модуль производит впечатление фрагмента программы, «вырванного из контекста». Но такова уж его природа: действительно, вне контекста своего гнезда сменный модуль не может, да и не должен существовать. Поэтому и анализировать, и редактировать, и распечатывать, и транслировать (проверяя синтаксическую корректность) сменный модуль чаще всего удобнее не изолированно, а в окружении текста, объемлющего его гнездо.
В то же время спецификации сменного модуля вполне уместны. Они распадаются на два вида: спецификации, общие для всего семейства сменных модулей, относящихся к данному гнезду, и спецификации конкретного сменного модуля.
Общие спецификации, или спецификации гнезда, могут определять функциональное назначение гнезда, ограничивать совокупность переменных, доступных сменному модулю, а также явно задавать синтаксическую позицию модуля. Все эти сведения в какой-то мере можно почерпнуть из окружения гнезда, но явное их задание облегчает последующий анализ и создание новых сменных модулей.
Спецификации гнезда, разумеется, не дублируются в программном фонде для каждого сменного модуля, а хранятся в единственном экземпляре. Тем самым конструктивно оформляется однородность семейства сменных модулей, которая при размножении спецификаций могла быть ошибочно воспринята как ни о чем не говорящее случайное совпадение.
Если гнездо односвязное, спецификации располагаются обычно непосредственно в тексте гнезда. Спецификации многосвязного гнезда оформляются как самостоятельный объект программного фонда.
Спецификации конкретного сменного модуля определяют его функциональные особенности (т. е. прежде всего то, чем он отличается от других сменных модулей этого гнезда) и, возможно, вводят локальные переменные (для односвязного модуля). Эти спецификации записываются непосредственно в тексте модуля.
Подробная проработка аппарата спецификаций для сменных модулей выходит за рамки тематики данной книги. В дальнейших рассмотрениях эта сторона применения каркасного подхода затрагиваться не будет.
Отмеченная выше несамостоятельность сменного модуля приводит к известным затруднениям при организации его трансляции. Проверку синтаксической корректности, не говоря уже о получении объектного кода, не удается произвести автономно. Как правило, для этих целей требуется подставить сменный модуль в его гнездо. Более того, если в тексте содержащего гнездо модуля трансляции имеется еще несколько гнезд, то для проведения полноценной трансляции всех их надо будет чем-либо заполнить, например, подставив везде сменные модули по умолчанию.
Все заботы по подстановке текста сменного модуля в гнездо должны взять на себя специализированные средства системной поддержки. Если эти средства достаточно продуманы, разработчик, в частности, не почувствует неудобств из-за текстуальной разобщенности спецификаций гнезда и подчиняющегося им сменного модуля, но в полной мере ощутит все преимущества отсутствия дублирования этих спецификаций.
* * *
В данной главе были рассмотрены два известных подхода к построению пакетов программ: цепочечный и каркасный. К цепочечному подходу больше мы возвращаться не будем. Каркасный подход, напротив, заслуживает более глубокого изучения, и ему постоянно будет уделяться внимание в последующих главах.
Дальнейшее изложение в известной мере опирается на введенные в разд. 3.6.2 типы заполнения гнезд каркаса. Гл. 4 посвящена вариантным гнездам, а гл. 5 и 6 наборным.
|