Назад Оглавление Вперед
На головную страницу М.М.Горбунов-Посадов
 
РАСШИРЯЕМЫЕ ПРОГРАММЫ
 

 Г л а в а  5
ОДНОРОДНЫЙ НАБОР
 
5.5. Однородность: где и почему
 

 

5.5. Однородность: где и почему

      5.5.1. Однородность и расширяемость. На страницах этой книги термин «однородность» впервые появился в разд. 2.5.2, где речь шла о полезности такого конфигурационного ориентира, как группа однородных модулей. Далее этот термин вплоть до настоящей главы нигде не упоминался. Однако отсутствие упоминаний отнюдь не означало непричастность однородности к разбиравшимся там конструкциям.
      Наоборот, все способы обеспечения расширяемости программы, которые рассматривались до сих пор (и которые еще предстоит рассмотреть), опираются на выявление и симметричное (т. е. строго единообразное — см. разд. 2.5.2) оформление однородных модулей. В самом деле, при оформлении варианта (гл. 1) и при проектировании многовариантных программ (гл. 4) в роли однородных частей выступали сменные модули, предназначенные для заполнения вариантного гнезда. При цепочечном подходе к построению пакетов программ (гл. 3) однородны модули, используемые в качестве звеньев формируемых цепочек. Наконец, для настоящей главы однородность настолько существенна, что этот термин даже вынесен в ее заголовок — «Однородный набор».
      Выявление и конструктивное оформление однородности — ключ к последующему развитию программы. Разработчик никогда не должен забывать правило: «Сколько однородности выявил и конструктивно оформил — столько и получай возможностей для безболезненного развития».
      Любое вычленение модуля из соображений изменяемости означает, вообще говоря, что закладывается первый кирпич в здание будущего однородного набора. Пусть в программном фонде даже никогда не будут соседствовать различные варианты этого модуля, пусть новые его модификации просто будут приходить на смену устаревшим — и в этом случае однородный набор состоится, правда, виден он будет только при ретроспективном анализе развития программы.
      Удивляет очевидное отставание в понимании «расширяемости» программы по сравнению, скажем, со сложившимся пониманием расширяемости hardware. В области аппаратуры специалисты, разумеется, отдают должное прелестям функциональной законченности таких компонентов, как системный блок, модем, сканнер, звуковая карта и т. п., однако, говоря о расширяемости, имеют в виду не законченность блоков, а прежде всего унификацию разъемов сопряжения.
      Именно благодаря таким разъемам — аппаратному аналогу наборного гнезда — пользователь персонального компьютера без труда расширяет его возможности, единообразно подключая и сетевую плату, и звуковую карту, и множество других разнообразнейших устройств. Интересно, что программа потенциально находится в существенно лучшем положении, чем аппаратура: число гнезд на материнской плате из-за очевидных инженерных соображений должно быть фиксировано, в то время как число элементов однородного набора можно наращивать практически безгранично.
      А в программировании до сих пор базисом расширяемости и многократного использования считают объектную ориентацию, т. е. функциональную самостоятельность сочленяемых блоков, но не возможность их унифицированного сопряжения, не наличие в программе расширяемых однородных наборов. Однако классы и объекты немногое могут предложить для оформления однородности.
      Заглянем, к примеру, в одну из наиболее фундаментальных монографий [Прее, 1995], посвященных объектной ориентации. Материал подается в традиционном ключе: тщательно проработаны вертикальные связи и вместе с тем кое-как оформлены горизонтальные. Так, статические однородные элементы, безупречно построенные с точки зрения иерархии классов, по замыслу автора, как правило, связываются в список. Список — горизонтальная связь, она неинтересна с позиций объектной ориентации, не поддерживается непосредственно аппаратом классов. И программа в этом месте приобретает неопрятный вид: формирование статического списка происходит на стадии выполнения (!), а при расширении однородного набора надо будет отредактировать исходный текст, добавив операторы включения в список нового элемента. Но объектная ангажированность автора не позволяет ему почувствовать здесь некоторую фальшь.
      Хочется, чтобы плодотворная связь между однородностью и расширяемостью привлекла внимание к однородным конструкциям со стороны создателей языков программирования. Ни одна однородность, встречающаяся в программном материале, не должна вызывать сомнений относительно того, не является ли она случайным совпадением. Каждый уважающий себя язык программирования должен воспринимать неудачу в подобающем оформлении какой-либо однородности не менее остро, чем, скажем, неудачу, приведшую к появлению в программе оператора GO TO.
      Большую пользу могло бы принести встраивание в язык средств поддержки расширяемости. Любая синтаксическая единица языка должна иметь возможность превратиться в вариантное гнездо, а любая конструкция, предусматривающая появление однородных элементов, — в наборное.

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

      5.5.3. Последовательное и параллельное выполнение. Одна из простейших «малых» конструкций (а точнее, управляющих структур), имеющих дело с однородными частями, — последовательное выполнение. Подавляющее большинство традиционных языков программирования позволяет записать друг за другом несколько выполняемых операторов, и при этом последовательность их записи будет определять последовательность их выполнения. Выполняемые операторы являются тут однородными частями программы, хотя даже синтаксически эта однородность не всегда достаточно четко проявлена. Вспомним, например, проблему «завершитель или разделитель» (см. разд. 5.2.2): только завершитель позволяет единообразно оформить однородные части, но, тем не менее, в языках нередко применяют разделитель, в результате чего первая (или последняя) часть (в нашем случае — оператор) несколько выпадает из однородного ряда.
      Если спроецировать последовательное выполнение на уровень модулей, то мы получим цепочечный подход к построению пакетов. Здесь в роли последовательно выполняемых частей выступают уже модули — звенья цепочек. Интересно, что цепочечный подход можно рассматривать и как частный случай каркасного подхода с использованием наборного гнезда: составляющими набор однородными модулями будут все те же звенья, а любая формируемая расчетная программа представляет собой результат подстановки в одно «головное» наборное гнездо заданного подмножества звеньев в заданной последовательности.
      Заметно реже встречаются «малые» конструкции, предписывающие параллельное выполнение входящих в них однородных частей. В то время как при последовательном выполнении расположение частей работало на наглядность, задавая нужный порядок, при параллельном выполнении, напротив, вынужденная последовательная запись однородных частей противоречит (зрительно) предписываемой им равноправной параллельности. Чистое (с точки зрения наглядности) решение тут можно получить, размещая параллельные ветви как самостоятельные объекты программного фонда и затем ассоциативно собирая их для параллельного выполнения. Однако ассоциативная сборка, столь обычная для конфигурационных построений уровня модулей, к сожалению, совершенно не вписывается в рамки традиционных языков.
      Ассоциативно заполняемые наборные гнезда в определенном смысле лучше отражают современные представления о соотношении между параллельным и последовательным. Здесь по построению первичным является отсутствие упорядоченности однородных частей, а для задания какой-либо их последовательности требуются дополнительные усилия (см. разд. 5.4.2). Заметный шаг в нужную сторону был в свое время сделан в Алголе-68, где параллельно выполняемые операторы разделялись запятой, которая имела более высокий приоритет, чем точка с запятой, традиционно обозначавшая последовательное выполнение. К сожалению, в современных языках эти завоевания утрачены. Там, как правило, действует ровно обратное соотношение: наиболее ходовые и удобные конструкции этих языков «склоняют» разработчика программы к заданию последовательности даже там, где ее не было в первоначальной постановке задачи.
      Обслуживающие параллельность конструкции нередко вовсе отсутствуют в языке и надстраиваются над ним, только когда создается специализированное обеспечение для эффективной загрузки параллельно работающего оборудования. А в результате разработчик воспринимает средства объявления параллельности как неизбежное зло, с которым приходится мириться, если хочешь ускорить выполнение программы.
      На самом же деле возможность объявить, что порядок выполнения ветвей программы не имеет значения, чрезвычайно полезна и вне всякой связи с эффективностью. Кому не приходилось, анализируя текст программы, размышлять о том, эквивалентны ли встретившиеся в разных местах пары вызовов процедур {a(); b();} и {b(); a();}? Нередко эквивалентность почти очевидна, и все же для надежности приходится предпринимать трудоемкое исследование. А ведь трудности эти порождены искусственно: когда упомянутые вызовы создавались, разработчик, вероятно, испытывал неловкость, разделяя их символом последовательного выполнения.
      Если посмотреть на соотношение между последовательным и параллельным с точки зрения качества, наглядности программы, то нельзя не отметить, что однородность параллельности «более высокой пробы», она много интереснее и для читателя, и для транслятора. Читатель избавлен от необходимости подозревать несуществующий тайный смысл в расположении равноправных ветвей, транслятор волен организовать выполнение ветвей в любой удобной ему (и аппаратуре) последовательности. Запись параллельных ветвей посредством последовательных конструкций в чем-то сродни записи цикла посредством условного оператора.
      Причины небрежения параллельностью со стороны создателей языков легко объяснимы. Языки, хоть и называются часто «алгоритмическими» — средством записи алгоритмов, все же в значительно большей степени являются «языками программирования» — средством общения человека с вычислительным оборудованием. Но оборудование пока в массе своей параллельность не поддерживает. Иначе говоря, запятая Алгола-68 отмерла потому, что редко находила отражение в объектном коде и объявление ветвей программы параллельными оставалось в известной степени голословным.
      С ростом масштабов применения параллельного оборудования шансы параллельных конструкций на органичное включение в языки программирования существенно возрастают. Их применение позволит не только проявить важные свойства алгоритма, но и четче вычленить потенциальные точки роста программы: в дальнейшем не придется ломать голову над тем, где разместить еще одну добавочную параллельную ветвь.

      5.5.4. Оператор выбора. Следующая «малая» однородная конструкция — оператор выбора. Новые возможности, которые открываются при подключении к построению оператора выбора аппарата однородного набора, были проиллюстрированы на примере из разд. 5.2.3. К оператору выбора применимы и приведенные выше соображения о соотношении между параллельным и последовательным: по современным представлениям более предпочтительным является оператор с равноправными, параллельно проверяемыми условиями, и именно такой оператор проще всего построить посредством наборного гнезда с ассоциативной сборкой.
      Оператор выбора позволяет глубже понять соотношение между вариантными и наборными гнездами. Если посредством наборного гнезда с ассоциативной сборкой построить оператор выбора периода компиляции, то получится конструкция, по существу эквивалентная вариантному гнезду. В самом деле, подключение новых ветвей (которые тут выступают в качестве аналога сменных модулей) будет происходить безболезненно, а в силу того, что из этих ветвей конструируется оператор выбора периода компиляции, в выполняемую программу попадет ровно одна ветвь. (Строго говоря, из оператора выбора произвольного вида в программу может не попасть ни одной ветви. Но этого легко избежать, используя выбор с ветвью «иначе», которая в данном случае будет аналогом сменного модуля, подставляемого в вариантное гнездо по умолчанию.)

      5.5.5. Описание структуры. К «малым» однородным конструкциям относится и описание структуры: в нем однородны описания одноуровневых полей. Описание структуры заслуживает особого упоминания, поскольку именно этот вид однородности был впервые использован разработчиками алгоритмических языков в качестве отправной точки для безболезненного развития программы. Так, в Си++, а затем и в языке Оберон [Вирт, 1991] появилась возможность безболезненного расширения состава полей структуры.
      Однако способы оформления такого расширения в Си++ и в Обероне весьма несхожи. В Си++ расширение структуры (или класса) обслуживается несколько тяжеловесным аппаратом, достаточно строго следующим канонам понятия «наследование» из объектно-ориентированного программирования. Аппарат Оберона существенно легче, его уже можно воспринимать не как проекцию понятия «наследование» на среду Модулы-2, а как непосредственное применение в языке довольно очевидного наблюдения о том, что однородные конструкции, как правило, допускают простой механизм безболезненного расширения. Хочется надеяться, что со временем разработчики алгоритмических языков обратят свое внимание и на другие однородности (в частности, на оператор выбора), которые с не меньшим основанием могут претендовать на роль базисных конструкций безболезненного развития программы.

      5.5.6. Более крупные конструкции. Помимо перечисленных, существует еще множество однородных конструкций уровня операторов. Однако они мало что могут добавить для построений уровня модулей, и поэтому мы их затрагивать не будем. Отметим только, что «большие» однородные конструкции все же несколько богаче малых. В частности, в традиционных языках на уровне операторов практически не поддерживаются многосвязные однородные построения, которые мы не раз с успехом применяли на уровне модулей.
      Более крупные однородные конструкции в алгоритмических языках встречаются значительно реже, хотя потребность в них, безусловно, существует. Причина здесь, по-видимому, заключается в том, что подобные конструкции безупречно работают только при наличии специализированной поддержки. Но разработчики языков, как уже не раз отмечалось, старательно избегают решений, выходящих за пределы традиционных схем реализации, и поэтому вокруг крупных конструкций постоянно возникают технологические неувязки.
      Характерный пример крупной однородной языковой конструкции — определение полиморфной операции, т. е. семейство однородных описаний процедур, задающих различные действия операции с одним и тем же обозначением над различными типами данных. Соображения целостности требуют «вынести за скобки», сосредоточить в единственном экземпляре все общие атрибуты семейства (скажем, приоритет операции). Соображения наглядности, напротив, располагают к размножению этих атрибутов в текстах всех членов семейства. Привлечение специализированных средств поддержки расширяемости легко разрешает это противоречие: атрибуты, а точнее, каркас однородной процедуры существует в единственном экземпляре, но специализированные средства размножают его при просмотре или при редактировании текстов описаний.

      5.5.7. Визуальное программирование. Крупные однородные конструкции, не нашедшие отражения в языках программирования, встречаются на практике достаточно часто. Проследим, например, какую роль играет однородность в одном из самых популярных современных течений — в визуальном программировании.
      Как возникло визуальное программирование? Апологет объектной ориентации предложит, вероятно, следующую версию. Сначала пришло понимание того, что экранные примитивы (кнопки, надписи, меню, линейки прокрутки и т. д.) должны быть реализованы как объекты соответствующих классов. Затем обратили внимание на общность классов различных экранных примитивов, в результате чего появился суперкласс «экранный примитив». Дальнейшее, с позиций объектной ориентации, было делом техники.
      С точки зрения однородной ориентации хроника событий выглядят несколько иначе. При проектировании среды разработки обратили внимание на однородность ряда ее компонентов — экранных примитивов. Визуальное программирование состоялось лишь тогда, когда выявленная однородность была энергично овеществлена в реализации среды. Именно так сформировались ключевые конструкции визуального программирования, представляющие собой однородные наборы.
      Прежде всего, было построено текстовое меню, составленное из названий однородных элементов-примитивов, или же более наглядное представление меню примитивов — экранная панель однородных иконок. Затем пришло понимание того, что текст реализации выбранных разработчиком экранных примитивов должен быть составлен из однородных процедур, обслуживающих отдельные проявления этих примитивов. Наконец, все атрибуты каждого из примитивов были сведены в таблицу однородных элементов, меняя которые разработчик настраивает примитив на свои конкретные нужды.
      К сожалению, однородность перечисленных конструкций непосредственно видна только в случае меню и таблицы атрибутов, где однородные элементы представлены в виде ровных рядов клеток единой таблицы. Наиболее интересный для нас однородный набор — процедуры-реализации отдельных проявлений экранных примитивов — воспринимается обычно лишь умозрительно, поскольку, как уже не раз отмечалось, в современных алгоритмических языках нет средств поддержки крупной однородности.
      Зато налицо расширяемость — неразлучная сестра однородности. Эволюция любой из существующих сред визуального программирования во многом опирается на пополнение набора обслуживаемых экранных примитивов и на расширение числа их атрибутов. Развитые среды визуального программирования не только множат с каждой новой версией элементы этих штатных однородных наборов, но и предоставляют своим пользователям возможность самостоятельно реализовать и встроить в среду любые дополнительные примитивы и атрибуты.

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

*     *     *

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

Далее

Рейтинг@Mail.ru