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

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

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

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

Borland C++

Рассмотрим характерный пример. Пусть имеется отлаженная программа, написанная на языке программирования Си++ для среды MS Windows и использующая аппарат меню. Кроме того, пусть в распоряжении разработчика имеется типичная современная инструментальная среда - транслятор Borland C++ [1]. Требуется дополнить одно из меню, состоящее, скажем, из шести пунктов, новым, седьмым пунктом. Пусть для определенности добавляемый пункт носит название "О программе".

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

Picture 1

Рис. 1. Добавление пункта меню в среде Borland C++

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

Однако при более внимательном анализе выясняется, что расчленение реализации пункта меню на пять разбросанных по тексту программы компонентов произведено совершенно осознанно. Более того, такое расчленение диктуется вполне разумными организационными решениями, принятыми в среде Borland C++.

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

MENUITEM текст, сообщение где текст - название пункта, которое выводится на экран для пользователя создаваемой программы, а указанное сообщение посылается выполняемой программе, когда пользователь обращается к данному пункту. (Реальный синтаксис MENUITEM немного сложнее, но сейчас это не имеет значения.) Добавляя в меню новый пункт "О программе" и привязывая к нему сообщение с номером CM_ABOUT, разработчик обязан отредактировать описание ресурсов, дополнив его еще одной, седьмой конструкцией MENUITEM:

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

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

Сообщению, которое передается программе при обращении к новому пункту меню, в предложении MENUITEM был присвоен идентификатор CM_ABOUT. Этому идентификатору надо сопоставить некоторое целое число, поскольку в Windows все сообщения занумерованы:

#define CM_ABOUT 213 

Предложение #define размещается там, где уже записаны шесть подобных предложений для предшествующих пунктов, - в заголовочном файле, который используют все относящиеся к меню модули. Таких модулей по крайней мере два: описание ресурсов и алгоритмическая часть на Си++ - так что без общего заголовочного файла никак не обойтись. Тем самым от реализации пункта откалывается еще один отдельно стоящий фрагмент (# 2).

Далее, в Borland C++ требуется заполнить определенного вида таблицу, где должны быть собраны все сообщения, обрабатываемые содержащим меню окном. Здесь нашему сообщению CM_ABOUT сопоставляется имя обрабатывающей его функции CmAbout:

EV_COMMAND (CM_ABOUT, CmAbout) 

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

Но и это еще не все. Функция CmAbout должна быть объявлена (вслед за шестью аналогичными объявлениями) как метод класса, обслуживающего содержащее меню окно (# 4). И наконец, разумеется, потребуется собственно описание функции CmAbout, которое несет основную алгоритмическую нагрузку (# 5).

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

#Set-конструкция

Схематично решение проблемы выглядит следующим образом. Каждый пункт меню заносится в базу данных проекта в форме записи c полями: Text - текст пункта меню, MessageName - имя сообщения, Message Number - номер сообщения, FunctionName - имя обрабатывающей функции и т.д. Таким образом формируется совокупность однородных записей, которые помечаются как принадлежащие набору OurMenu - наше меню.

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

#Set OurMenu 
EV_COMMAND (#MessageName, #FunctionName), 
#End 

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

Синтаксис и семантика конструкции #Set достаточно очевидны. Первая строка - заголовок - указывает однородный набор записей (OurMenu), которые должны извлекаться из базы данных проекта. За ней следует тело #Set, представляющее собой текст, среди которого располагаются ссылки на поля записи, отмеченные символом '#'. Завершает конструкцию предложение #End.

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

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

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

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

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

Но наиболее интересные последствия применения #Set-конструкции связаны с организацией подключения к программе новых частей. Теперь новый пункт добавляется к меню безболезненно, без какого бы то ни было редактирования существующего отлаженного исходного текста программы: достаточно поместить в базу данных проекта новую запись и объявить ее принадлежащей однородному набору OurMenu. Так же безболезненно, без участия редактора происходит и удаление пункта. В результате мы избавляемся от источника весьма частых ошибок, бросавшего тяжелую тень неблагополучия на повседневный процесс развития программы.

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

Итак, привлечение #Set-конструкции обеспечило безболезненность подключения к меню нового пункта. Однако у искушенного читателя, вероятно, давно уже зреет вопрос: а стоило ли из-за такой малости громоздить новый, ни на что не похожий элемент инструментальной среды? Иначе говоря, насколько часто ситуации, близкие к описанной выше, встречаются в реальных программах? Попытаемся показать, что подобные обстоятельства достаточно типичны, для чего обратимся к другому популярному продукту фирмы Borland - к транслятору с языка Паскаль Delphi [2, 3].

Delphi

Сейчас Delphi - одна из наиболее известных инструментальных сред, исповедующих идеологию визуального программирования. Типичный сеанс работы с Delphi начинается с того, что программист выбирает из меню и расставляет на первоначально пустой панели разнообразные необходимые для будущей программы примитивы MS Windows: кнопки, поля ввода, основное меню, линейки прокрутки и т.д. Затем, указав на некоторый примитив, он может поменять его атрибуты или записать фрагмент алгоритма, реализующий реакцию указанного примитива на какое-либо событие. Для записи фрагмента алгоритма перед ним открывается окно с каркасом соответствующей процедуры, где курсор услужливо подмигивает в свободном пока промежутке между begin и end.

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

Нарисованная идиллическая картинка программирования в Delphi при более внимательном рассмотрении несколько тускнеет. Увлеченность визуальной стороной привела к тому, что если раньше программист мог работать только в последовательности "запрограммировал - увидел", то теперь - только "нарисовал (увидел) - запрограммировал".

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

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

Отсутствие какой-либо границы между текстом, генерируемым Delphi, и текстом, вводимым программистом, до такой степени мешает работать, что одно из руководств [3] даже рекомендует пользователю Delphi набирать служебные слова языка программирования Паскаль прописными буквами (BEGIN), чтобы отличать их от окружающих служебных слов, принадлежащих каркасу и записанных строчными буквами (begin). Что же помешало авторам Delphi, бесстрашно совершившим мощный прыжок в малоисследованную сферу визуального программирования, дополнить Паскаль относительно несложной конструкцией каркаса? Не исключено, что причина кроется в до обидного слабой теоретической базе и в отсутствии опыта широкого применения такого рода конструкций. Ведь если быть последовательным, то легализация каркаса закономерно приведет к внедрению в язык чего-то подобного #Set-конструкции из предыдущего раздела, а примененная там ассоциативная выборка из базы данных проекта пока еще воспринимается как эксцентричная выходка, непозволительная для разработчика повседневного программистского инструментария.

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

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

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

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

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

TPayment = 
CLASS(TObject) {one element in the amortization table} 
END; 
  • PaymentNum : Integer; 
    PayPrincipal : Real; 
    PayInterest : Real; 
    PrincipalSoFar : Real; 
    InterestSoFar : Real; 
    ExtraPrincipal : Real; 
    Balance : Real; 
    PROCEDURE GetStringForm 
    (VAR 
    • StrPayNum, 
      StrPayPrin, 
      StrPayInt, 
      StrPrinSoFar, 
      StrIntSoFar, 
      StrExtraPrin, 
      StrBalance : String15); 

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

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

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

Ареал безболезненного программирования

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

1) В реальной программе, как правило, удается вычленить один или несколько однородных наборов, которые охватывают основную часть ее объема.

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

Строгое обоснование справедливости этих тезисов - дело, видимо, безнадежное, несмотря на припасенные в них бесхребетные оговорки типа "как правило" и "основная часть". Все, что можно здесь сделать, это попытаться вызвать доверие к ним, обратившись к косвенным подтверждениям.

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

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

Представим себе теперь, что тезис (1) верен. Для практического программирования из этого вытекало бы по крайней мере два важных следствия.

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

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

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

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

Не следует думать, что регулярный механизм развития всегда требует какого-либо аналога #Set-конструкции, где в формируемый текст программы включаются все хранящиеся в базе данных проекта записи однородного набора. Часто удается ограничиться более простой в реализации конструкцией #Variant, где из имеющегося набора однородных записей выбирается и попадает в формируемый текст лишь одна. Конструкция #Variant широко применяется при программировании многовариантных задач (рис. 2). В этих задачах появление нового значения изменяемого фактора означает, что старая запись, отражающая прежнее значение фактора, должна быть заменена новой. Однако и старая запись не отбрасывается, поскольку впоследствии вновь может потребоваться прежнее значение фактора; таким образом в базе данных проекта накапливается однородный набор сменных записей, отражающих различные когда-либо использовавшиеся значения изменяемого фактора.

Picture 2

Рис. 2. Сборка многовариантной программы

Поскольку в генерируемый текст попадает ровно одна запись, отпадает нужда в цикле периода компиляции, а поля записи непосредственно подставляются в текст программы. Поэтому конструкция #Variant весьма проста:

#Variant набор.поле 

где набор - имя однородного набора сменных записей (обычно в качестве имени используется мнемоническое обозначение изменяемого фактора), поле - имя вставляемого поля записи.

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

#Variant Method 

где Method - имя однородного набора методов расчета.

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

Несмотря на существенные внешние различия, конструкции #Set и #Variant схожи в главном: и та и другая обеспечивает безболезненное развитие программы. Поэтому, если верен тезис (2), то основной поток работ по развитию программы можно направить в русло безболезненных изменений. Разумеется, для нужд реального программирования описанные конструкции потребуется усовершенствовать и дополнить [4], но игра стоит свеч: исключение из процесса развития такой опасной процедуры как редактирование отлаженного ранее текста выводит этот процесс на новый уровень надежности.

Так или иначе, если в тезисах (1) или (2) имеется хотя бы доля правды, средства системной поддержки однородных наборов заслуживают включения в повседневный программистский инструментарий. Именно отсутствие поддержки сдерживает массовое применение предлагаемого аппарата. Реализация средств поддержки однородных наборов не потребует сегодня значительных усилий, поскольку основные предпосылки этого шага вполне созрели.

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

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

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

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

Литература

1. Сван Т. Программирование для Windows в Borland C++. - М.: Бином, 1995 г. - 480 с.
2. Сван Т. Основы программирования в Delphi для Windows95. - Киев: Диалектика, 1996 г. - 480 с.
3. Дантеман Д., Мишел Д., Тейлор Д. Программирование в среде Delphi. - Киев: Диасофт, 1995 г. - 608 с.
4. Горбунов-Посадов М.М. Конфигурации программ. Рецепты безболезненных изменений. - 2-е изд., испр. и доп. - М.: Малип, 1994 г. - 272 с.

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