Революционные радикально преобразуют программу, однако, потребность в них возникает нечасто. Основной объем усилий приходится, как правило, на эволюционные изменения.
Эволюция программы идет относительно небольшими шагами (транзакциями), на каждом из которых существующий код почти не меняется, но к нему добавляется компонент, реализующий новую функциональную возможность (use case). Как согласовать между собой структуры программы и транзакции, чтобы новый компонент органично вписался в сложившийся коллектив программных модулей и чтобы эволюционное изменение проходило безболезненно, не угрожая работоспособности ранее написанных частей программы?
Горизонтальные слои
Более двадцати лет назад А.Л.Фуксман [1] обратил внимание на то, что добавляемый компонент обычно распадается на модули, которые относятся к нескольким уже сформировавшимся образованиям, так называемым горизонтальным слоям принимающей программы. В каждом из горизонтальных слоев локализуется какой-либо аспект функционирования программы, и если добавляемый компонент достаточно нетривиален, он должен оказать влияние сразу на несколько слоев.
Рис.1. «Квадратно-гнездовая» схема программы |
Представления о горизонтальных слоях становятся сегодня популярными и на Западе. Нередко [2,3] общая структура программы изображается в виде «квадратно-гнездовой» схемы (рис.1), где горизонтальными овалами представлены горизонтальные слои, пунктирными прямоугольниками - компоненты, а на их пересечении обычно записывается название модуля (модулей). На рис.1 названия модулей не приведены, а лишь отмечены звездочками те составляющие слоев, которые действительно присутствуют в программе. Дело в том, что компонент, разумеется, вовсе не обязан отметиться во всех горизонтальных слоях, и для сложной программы матрица модулей, подобная рис.1, оказывается довольно-таки разреженной.
Приведем два примера. Пусть вы пишите статью, и добавляемый к ней компонент - новый раздел. Тогда, наряду с размещением текста нового раздела среди уже имеющихся (горизонтальный слой «Основной текст»), требуется, вообще говоря, пополнить содержимое еще нескольких слоев. Возможно добавится несколько новых позиций в библиографическом списке (горизонтальный слой «Литература»), появятся новые строчки в глоссарии и т.д.
Второй пример. Вы разрабатываете текстовый редактор. И добавляете к нему компонент, реализующий новую возможность - скажем, контекстный поиск. Конечно же, основной объем нового исходного текста попадет в горизонтальный слой «Реализация основных операций». Однако в то же время потребуется пополнить такие горизонтальные слои как «Меню операций» и, возможно, «Таблица горячих клавиш».
При поверхностном взгляде на прямоугольные очертания рис.1 может показаться, что горизонтальные и вертикальные направления симметричны и взаимозаменяемы. Но это неверно - направления существенно отличаются по своему содержанию.
Горизонтальные слои - жизненно необходимая, неотторгаемая часть программы. Программа, лишенная каких-либо из своих горизонтальных слоев, скорее всего, окажется непригодной ни к отладке, ни к эксплуатации. В самом деле, изолированный список литературы, без собственно текста, нельзя назвать даже полуфабрикатом статьи. Изолированная таблица горячих клавиш не может быть запущена на выполнение, она почти ничего не способна рассказать о содержащей ее программе. И как эксплуатировать или хотя бы отлаживать текстовый редактор, если в нем отсутствует меню операций?
Напротив, компоненты («вертикальные слои», по терминологии Фуксмана) лишь расширяют возможности программы, она, пусть в несколько усеченном виде, может функционировать и без них. Так, статью с несколькими отсутствующими пока разделами вы вполне можете показать коллеге - он, вероятно, легко поймет, о чем там идет речь, и сделает полезные замечания. Текстовый редактор, не имеющий пока возможности контекстного поиска, можно не только отлаживать, но даже эксплуатировать.
Тем самым вертикальные слои представляют собой материал, реализацию которого при создании крупной программы удобно отложить на потом. Эта особенность вертикального слоя впервые была подмечена Фуксманом [1] и послужила отправной точкой предложенной им стратегии поэтапной разработки. Согласно Фуксману, на первом этапе создается «основа» - предельно упрощенная версия программы, остающаяся после удаления из нее всех вертикальных слоев. Затем, на последующих этапах (транзакциях) реализуются и добавляются к расширяемой таким образом программе все новые и новые вертикальные слои. С технологической точки зрения заметное преимущество данной стратегии состоит в том, что при отладке любой промежуточной (без некоторых вертикальных слоев) версии программы тут не требуются «заглушки» - имитаторы недостающих частей, без которых не могут обойтись популярные стратегии «сверху вниз» или «снизу вверх».
Несколько иным путем приходят к той же самой структуре расширяющего программу компонента в работах [2,3]. Здесь компонент рассматривается с позиций многократного использования (reuse). Отмечается, что сложившийся стереотип представления о многократно используемом компоненте как об относительно самостоятельной подпрограмме или классе неоправданно обедняет механизмы взаимодействия компонента и принимающей программы. Существенно более плодотворным является представление о программе как о совокупности горизонтальных слоев, в каждом из которых добавляемый компонент вправе оставить свой след.
Так или иначе, мы имеем веские основания для того чтобы любую транзакцию, обслуживающую эволюционное развитие программы, оформлять как добавление нового компонента (вертикального слоя), который распадается на модули, предназначенные, вообще говоря, для нескольких горизонтальных слоев. Это решение равно пригодно как для частей программы, разрабатываемых специально для нее, так и для заимствуемых многократно используемых компонентов. В последующем изложении нас редко будет интересовать происхождение материалов эволюционной транзакции, а термины «горизонтальный слой» и «компонент» будут употребляться в основном как синонимы.
Двухмерная структура программы
Реализация техники горизонтальных и вертикальных слоев сталкивается с определенным противоречием. Структура программы здесь по существу двухмерна, а все массовые алгоритмические языки предполагают одномерное представление исходного текста. В результате разработчик оказывается перед выбором: если спроектировать горизонтальные слои в виде непрерывных участков исходного текста, то модули, составляющие вертикальный слой, окажутся далеко отнесенными друг от друга. Если же непрерывными сделать вертикальные слои, то это повлечет разобщенность модулей горизонтального слоя. Остроту данного противоречия хорошо иллюстрируют технологические решения, принятые в упомянутых работах.
Фуксман [1] отдавал предпочтение непрерывности горизонтальных слоев. В результате вертикальный слой оказывался рассредоточенным по тексту программы и потребовалась специальная весьма тяжеловесная конструкция - «сосредоточенное описание рассредоточенного вертикального слоя». Ведь просто безвозвратно растворить модули вертикального слоя в тексте программы было бы, по меньшей мере, нетехнологично. Во-первых, вертикальный слой - важный структурный элемент, увидеть контуры которого чрезвычайно полезно при последующем изучении исходного текста. Во-вторых, нередко требуется отменить подключение одного из вертикальных слоев, и без сосредоточенного описания сделать это было бы очень нелегко.
В работах [2,3], напротив, непрерывными оказались компоненты, а рассредоточенными - горизонтальные слои. Компоненты оформлены как классы (объекты), которые содержат в себе составляющие горизонтальных слоев. Здесь требуются заметные усилия для организации работы горизонтального слоя как единого целого. Не все благополучно и с наглядностью исходного текста: непросто бывает увидеть текущий состав горизонтального слоя программы. Наконец, рассредоточение горизонтального слоя по нескольким классам нередко приводит к заметным потерям эффективности кода: таблицы приходится превращать в списки и т.д.
Выход из противоречия, порождаемого двухмерностью слоев, достаточно очевиден: требуются инструментальные средства, поддерживающие эту двухмерность. Если понятия «горизонтальный и вертикальный слои» отразить в инструментальной среде, то в обиход разработчика войдут простые и надежные базовые операции включения/отключения вертикального слоя, а при изучении текста программы появится возможность просмотра и горизонтальных и вертикальных конструкций. На вход компилятора, разумеется, будет тем не менее подаваться сгенерированный инструментальной средой одномерный текст.
Объявление модуля
Чтобы инструментальная среда сумела поддержать двухмерность, эволюционная транзакция должна обрести определенную структуру. Требуется, во-первых, добавляемый компонент (вертикальный слой) каким-то образом расчленить на модули, относящиеся к различным горизонтальным слоям, и во-вторых, предложить эффективную схему подключения этих модулей к принимающей программе.
Начнем с расчленения добавляемого компонента. Нам нужно, чтобы компонент так или иначе задавал совокупность объявлений составляющих его модулей, предназначенных для указанных горизонтальных слоев. Можно предложить следующую конструкцию объявления модуля:
#INSTALL_IN имя_горизонтального_слоя {#имя_поля : значение_поля } [#APPLY применение ] #END_OF_INSTALL
В первой строке указывается имя_горизонтального_слоя, для которого предназначен объявляемый модуль.
Фигурные скобки, окаймляющие вторую строку, означают, что записанный в них элемент должен быть воспроизведен в одном или в нескольких экземплярах. Тем самым вводится возможность объявления составного модуля: каждый экземпляр элемента объявляет имя и значение одного из полей.
Для чего могут понадобиться поля модуля? Вновь обратимся к нашим примерам. В модуле раздела статьи, предназначенном для горизонтального слоя «Основной текст», имеет смысл вычленить поля «Заголовок» и «Текст» - тогда появится возможность автоматически единообразно оформлять все заголовки и формировать оглавление. В библиографической ссылке можно вычленить поле «Автор» - тогда в списке литературы можно будет, например, выделить фамилии авторов курсивом. Структуру (совокупность констант), задающую горячую клавишу, лучше представить не в виде сплошного текста описания на языке программирования, а как совокупность значений отдельных ее полей - это и короче, и гибче.
Далее в объявлении модуля следует элемент #APPLY. Окаймляющие его квадратные скобки говорят о том, что этот элемент может быть опущен. Элемент #APPLY применяется только во вложенных объявлениях. Он задает применение - то, что должно остаться в исходном тексте объемлющего модуля после обработки конструкции #INSTALL_IN препроцессором. Применение представляет собой произвольный текст на исходном языке, среди которого могут размещаться значения полей объявляемого модуля, задаваемые вставками вида #имя_горизонтального_слоя.имя_поля.
Вложенные объявления могут потребоваться, когда составляющие компонент модули тесно связаны между собой. Проиллюстрируем взаимозависимость модулей на рассмотренном примере раздела статьи, содержащего библиографические ссылки. Где должно размещаться объявление модуля библиографической ссылки? Лучше всего погрузить его непосредственно в текст раздела, поскольку в этом случае при удалении содержащего ссылку текстового фрагмента автоматически скорректируется и состав библиографического списка. Однако текст раздела представляет собой самостоятельный модуль, точнее, поле «Текст» модуля, предназначенного для горизонтального слоя «Основной текст». Таким образом, здесь требуется объявить модуль библиографической ссылки внутри объявления другого модуля.
Пусть, например, в текст раздела требуется включить ссылку на книгу Дейкстры «Дисциплина программирования», причем при окончательной публикации ссылка должна там принять вид «[Дей78]». Тогда на месте ссылки в модуле основного текста записывается конструкция
#INSTALL_IN Литература #Имя : Дей78 #Автор : Дейкстра Э. #Текст : Дисциплина ... #APPLY [#Литература.Имя] #END_OF_INSTALL
Тем самым при размещении раздела в базе данных проекта горизонтальный слой «Литература» пополнится модулем с именем «Дей78» и еще двумя полями, «Автор» и «Текст». Элемент #APPLY обеспечит появление в публикуемом тексте раздела требующейся ссылки вида «[Дей78]».
Однородность модулей
Предложенная конструкция позволяет объявить в составе добавляемого к программе компонента несколько модулей, предназначенных для различных горизонтальных слоев. Далее возникает вопрос: каким образом определить точки текста программы, где следует разместить новые модули? Задать эти точки оказывается достаточно легко, если принять во внимание одну важнейшую особенность горизонтального слоя: составляющие его модули однородны.
Под однородностью здесь понимается не только общность назначения (семантики) всех модулей слоя, но и полное синтаксическое их единообразие. Модули состоят из полей, и однородность означает, что состав полей будет одним и тем же для всех модулей горизонтального слоя, а значения одноименных полей в свою очередь однородны.
Проиллюстрируем однородность модулей, вернувшись к рассмотренным примерам горизонтальных слоев. Первый пример: к готовящейся статье добавляется компонент - новый раздел. Горизонтальный слой «Основной текст» образуется из единообразно оформленных модулей - разделов, горизонтальный слой «Литература» - из единообразно оформленных библиографических ссылок и т.д. В каждом модуле слоя «Основной текст» присутствует поле «Заголовок», и все эти поля единообразно оформлены.
Второй пример: разрабатываемый текстовый редактор пополняется новой операцией. Горизонтальный слой «Реализация основных операций» состоит, по-видимому, из однородных процедур, «Меню операций» - массив однородных структур (констант), «Таблица горячих клавиш» - тоже массив однородных структур, но, разумеется, другого типа.
Рис. 2. Однородные модули горизонтальных слоев программы Темным цветом закрашены модули, добавленные в результате выполнения очередной транзакции |
Любая транзакция, расширяющая программу, имеет вполне определенную структуру: она представима в форме серии расширений имеющихся горизонтальных слоев. Отдельные горизонтальные слои транзакция может вовсе не затрагивать, а в некоторые слои добавлять один модуль, в другие - несколько модулей. Например, очередной раздел статьи может содержать произвольное число библиографических ссылок, в частности, вообще не содержать ссылок. Для одной операции текстового редактора может быть не предусмотрено горячих клавиш, для другой - отводится одна клавиша, для третьей - две клавиши (скажем, Ctrl+F и F11). Каждый горизонтальный слой фиксирует формат своих модулей, и пополнять его можно только однородными элементами этого формата. На рис. 2, как и на рис. 1, показана структура программы с горизонтальными слоями, но здесь иллюстрируется однородность модулей слоя и многообразие форматов составляющих модулей различных слоев.
Представление горизонтального слоя
Итак, горизонтальный слой вобрал в себя все однородные модули, направленные туда из включенных в программу компонентов. Далее этот однородный набор модулей должен каким-то образом превратиться в пригодный для трансляции текст на языке программирования. Какой же вид имеет горизонтальный слой в программе?
Оказывается, горизонтальный слой может принимать форму любой однородной языковой конструкции. Конструкций таких в существующих языках имеется достаточно много. Однородны, например, константы в массиве констант, ветви в операторе выбора и др. Даже самая массовая языковая конструкция - группа параллельно или последовательно выполняемых действий - составлена из однородных частей-операторов.
В исходном тексте горизонтальный слой оформляется в виде цикла периода компиляции [4]. Цикл повторяется столько раз, сколько модулей данного слоя заявлено во включенных в программу компонентах, а переменная цикла в это время последовательно пробегает все модули слоя. Цикл записывается непосредственно в тексте программы и имеет вид
#LAYER имя_горизонтального_слоя тело_цикла #END_OF_LAYER
Предложения #LAYER и #END_OF_LAYER очерчивают границы цикла, а имя_горизонтального_слоя задает горизонтальный слой и одновременно служит в качестве переменной цикла, пробегающей по всем модулям слоя. В цикле будет многократно воспроизводиться тело_цикла, представляющее собой произвольный текст на исходном языке. В этом тексте располагаются значения полей модуля слоя, задаваемые вставками вида #имя_горизонтального_слоя.имя_поля.
Как уже говорилось, число повторений цикла определяется числом подставляемых однородных модулей. Каждое повторение приводит к добавлению в формируемый текст программы еще одного экземпляра текста тела цикла, где на место вставок помещены тексты соответствующих полей очередного модуля.
Конструкций #LAYER, относящихся к одному и тому же горизонтальному слою, в исходном тексте может присутствовать несколько. Так, горизонтальный слой «Основной текст» статьи будет использован по крайней мере дважды: для составления собственно основного текста с заголовками разделов и для составления оглавления, куда из него войдут только заголовки разделов.
Вернемся к примеру оформления библиографических ссылок. Для определенности будем полагать, что статья готовится для публикации в Internet и потому пишется на языке HTML. (Любители Си, Паскаля и других популярных языков найдут десятки «более программистских» примеров в книге [4].) Все объявленные в тексте статьи ссылки можно собрать в единый список литературы посредством конструкции:
#LAYER Литература
[#Литература.Имя] #Литература.Автор #Литература.Текст
#END_OF_LAYERВсе ссылки выстроятся одна за другой, и каждой из них будет посвящено пять строк, определяющих формат списка. Если вспомнить ссылку на книгу Дейкстры из разобранного ранее примера, то на экране дисплея в результате обработки библиографического списка браузером она примет вид
[Дей78] Дейкстра Э. Дисциплина ...
Разумеется, для реальных применений предложенная конструкция, представляющая горизонтальный слой, должна быть тщательно отшлифована и несколько усовершенствована. Например, имеет смысл добавить средства, позволяющие упорядочивать формируемый библиографический список по именам ссылок или в последовательности появления в тексте статьи. Потребуются усовершенствования и для предложенной конструкции объявления модуля. Однако ничего принципиально нового эти усовершенствования не привнесут: и идеология, и реализация описанной схемы эволюции программы оказываются достаточно компактными и эффективными.
Безболезненность эволюции
Мы убедились, что реализация техники горизонтальных слоев не только возможна, но и относительно проста. Подытожим теперь, что же это дает программисту.
Прежде всего, двухмерные конструкции позволяют наглядно отобразить наиболее существенное, глубинное содержание текста программы. Вертикальные слои (компоненты) характеризуют многообразие внешних проявлений программы, а горизонтальные - цементируют ее, обеспечивают единство алгоритмических решений и пользовательского интерфейса.
Новые возможности открываются при разработке многократно используемых компонентов. Взаимодействие крупного компонента с принимающей программой теперь не выглядит как громоздкий хаотический набор разнородных интерфейсных соглашений, а наглядно и технологично структурируется на унифицированные интерфейсы модулей, размещаемых в отдельных горизонтальных слоях.
Есть и еще одно, менее очевидное, но весьма существенное преимущество техники горизонтальных слоев. Подключение нового вертикального слоя (компонента) происходит здесь безболезненно, не требуя какого бы то ни было редактирования написанных ранее исходных текстов. В самом деле, любая транзакция, обслуживающая эволюцию программы, распадается на ряд подключений новых однородных модулей к существующим горизонтальным слоям. А каждое такое подключение происходит безболезненно: модуль просто помещается в базу данных программного проекта и снабжается атрибутом принадлежности к определенному горизонтальному слою. И не нужно редактировать исходные тексты принимающей программы, да и вообще ничего больше делать не надо: при компоновке программы из базы данных проекта будут ассоциативно извлечены и помещены во вновь формируемый горизонтальный слой все модули, снабженные данным атрибутом.
Немаловажно и то, что легко в стиле «Plug and play», происходит не только подключение компонента, но и его исключение («Unplug and play») из программы. Ведь в базе данных программного проекта компонент всегда доступен не только как совокупность составляющих модулей, но и как единое целое. Поэтому исключение компонента сводится здесь к уничтожению его как объекта базы данных и не требует никакой коррекции окружающих участков исходного кода. При других структурах эволюционных транзакций исключение внедренного ранее компонента сопряжено обычно с серьезными трудностями - достаточно вспомнить известную проблему деинсталляции приложения в Windows.
Наконец, массовое признание двухмерного программирования означало бы торжество чрезвычайно продуктивной идеи: любая точка роста (hot spot) программы может быть представлена в виде горизонтального слоя - расширяемого набора однородных модулей.
Перспективы
Сейчас, казалось бы, самое время перейти к рассказу о компаниях-изготовителях описанных инструментальных сред или хотя бы указать сроки появления на рынке средств поддержки двумерных программных конструкций. К сожалению, придется разочаровать заинтересовавшегося читателя: несмотря на растущее число посвященных данной проблематике публикаций, в массовом программировании эти инструменты появятся, похоже, еще нескоро.
Хотя реализация двухмерности относительно проста, однако она затрагивает многие деликатные аспекты (горизонтальные слои) инструментальной среды. Достаточно упомянуть символьную отладку, где в любой точке выполнения программы пользователь должен теперь иметь возможность увидеть все три ипостаси ее исходного текста: вертикальную, горизонтальную и окончательную, т.е. попавшую на вход традиционного компилятора. Из-за этого не удастся, по-видимому, реализовать поддержку двухмерности в форме надстройки (plug-in), не вторгаясь в код инструментальной среды.
Образовался порочный круг. Без участия изготовителя безболезненно надстроить поддержку двухмерности не удается, поскольку код современной инструментальной среды недостаточно открыт для расширений. Но открытости коду недостает прежде всего потому, что в свое время у его создателей не было в распоряжении все тех же двухмерных инструментов. В результате код среды получился по сути не двухмерным - там не нашли надлежащего оформления, остались непроявленными расширяемые горизонтальные слои. Этот порочный круг будет разорван, вероятно, лишь тогда, когда изготовители инструментальных сред сочтут двухмерные программные конструкции достойными внимания и сами включит их поддержку в свои продукты.
Работа поддержана грантом Российского фонда фундаментальных исследований 99-01-00984
Об авторе
Михаил Горбунов-Посадов — зав. сектором Института прикладной математики имени М.В.Келдыша РАН. С ним можно связаться по электронной почте по адресу: gorbunov@keldysh.ru
Литература
[1] Фуксман А.Л. Технологические аспекты создания программных систем. - М.: «Статистика», 1979. - 184с.
[2] VanHlist M., Notkin D. Using C++ templates to implement role-based designs // JSSST international symposium in object technologies for advanced software. - Springer-Verlag, 1996. - p. 22-37
[3] Smaragdakis Y., Batory D. Implementing reusable object-oriented components. // 5th International Conference on Software Reuse, Victoria, Canada, June 1998. - http://www.cs.utexas.edu/users/schwartz/pub.htm
[4] Горбунов-Посадов М.М. Расширяемые программы. - М.: «Полиптих», 1999. - 336 с.