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

 Г л а в а  7
НЕВЫДУМАННЫЕ ИСТОРИИ
 
7.1. Пункт меню
 

 

Г л а в а  7

НЕВЫДУМАННЫЕ ИСТОРИИ

      В этой главе собраны три истории, приключившиеся с автором за последние три-четыре года. Они, каждая по-своему, преломляют тему расширяемости программы. В названиях историй отражены объекты, об организации добавления которых пойдет в них речь:
      «Пункт меню» добавляется в работающую под MS Windows программу, созданную в среде Borland C++;
      «Атрибут платежа» добавляется в программу расчета платежей по займу, созданную в среде Delphi;
      «Инсталляция приложения», равно как и деинсталляция, происходит в операционной системе MS Windows.


7.1. Пункт меню

      Из упомянутых трех сюжетов первый относится к 1995 году. Программируя в среде весьма популярного в то время в России компилятора — Borland C++ [Сван, 1995], автор решил проанализировать, как там выполняется одна из наиболее типичных операций по расширению. В программе, написанной на Си++ для MS Windows, в этот момент уже было реализовано меню, состоявшее из шести пунктов, и требовалось добавить седьмой пункт, который должен был получить название «О программе».

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


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

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

MENUITEM  текст , сообщение

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

MENUITEM  "О программе", CM_ABOUT

      Преимущества оформления ресурсов как самостоятельного слоя достаточно очевидны. Главное из них, по-видимому, заключено в том, что значительно облегчается процедура изменения элементов внешнего оформления: теперь для этого не нужно вторгаться (все тем же злополучным редактором) в требующую особо деликатного обращения алгоритмическую часть, достаточно поменять описание ресурса. Кроме того, язык ресурсов — эффективное средство быстрого макетирования: он позволяет, в частности, не написав ни строчки алгоритма, увидеть на экране основные изобразительные решения проектируемой программы. Вводить и модифицировать проектируемый ресурс можно как в форме текста на языке ресурсов, так и в стиле визуального программирования, непосредственно получая на экране его графическое воплощение и интерактивно манипулируя его расположением, размерами, способом оформления и другими внешними атрибутами.
      Итак, односвязной реализации пункта меню добиться не удается: для облегчения последующих изменений элементов внешнего оформления один из компонентов этой реализации должен быть записан не на Си++, а на языке ресурсов, и размещается этот компонент (№ 1) в отдельном, самостоятельном модуле. Но из-за чего же разрывается на отстоящие друг от друга куски оставшаяся часть реализации? Оказывается, и здесь причины достаточно убедительны.
      Сообщению, которое передается программе при обращении к новому пункту меню, в предложении MENUITEM был присвоен идентификатор CM_ABOUT. Этому идентификатору надо сопоставить некоторое целое число, поскольку в MS Windows все сообщения занумерованы:

#define  CM_ABOUT  213

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

EV_COMMAND  (CM_ABOUT, CmAbout)

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

      7.1.2. Решение. Хочется надеяться, что читатель, честно одолевший предшествующие главы, давно уже узнал знакомые очертания. Конечно же, это — однородный набор! Теперь последовательность действий очевидна.
      Реализация пункта нашего меню оформляется как многосвязный модуль — член регулярного однородного набора OurMenu. Односвязными компонентами этого модуля становятся: Text — текст пункта меню, MessageName — имя сообщения, MessageNumber — номер сообщения, FunctionName — имя обрабатывающей функции и т. д.
      Далее в каждой из пяти разбросанных по тексту программы точек, относящихся к реализации пунктов меню, записываются наборные гнезда. Например, на месте элементов таблицы сообщений (слой № 3) будет записано

#HORIZON  OurMenu
      EV_COMMAND (  #OurMenu.MessageName,
#OurMenu.FunctionName ),
#END_OF_HORIZON

Эти четыре строки после обработки препроцессором превращаются в семь строк EV_COMMAND, так что затем на входе компилятора оказывается точно такой же текст, как и ранее.
      Приведенное решение можно улучшить, если привлечь аппарат рассредоточенного набора. Так, в только что сформированной таблице сообщения от пунктов меню соседствуют с сообщениями от других источников. Здесь точнее было бы образовать рассредоточенный набор OurWindowMessages всех сообщений окна и объявлять в реализации пункта меню новый элемент этого набора. Тогда приведенное выше наборное гнездо OurMenu будет играть роль транзитного узла на пути к набору OurWindowMessages:

#HORIZON  OurMenu
      #INSTALL_IN  OurWindowMessages
            Message 
            Function 
: #OurMenu.MessageName
: #OurMenu.FunctionName
      #END_OF_INSTALL
#END_OF_HORIZON

а генерация строк EV_COMMAND будет происходить уже в наборном гнезде OurWindowMessages.
      Рассредоточенный набор уместен и на слое № 2. С его помощью легко свести воедино все сообщения программы и автоматизировать сквозную нумерацию этих сообщений.

      7.1.3. Что дал однородный набор. Разобранный эпизод — неплохой повод лишний раз напомнить преимущества, которые приносит применение техники однородного набора, и прежде всего, регулярного однородного набора. Сразу обращает на себя внимание сокращение длины исходного текста программы. Сокращение это может оказаться довольно значительным, но с точки зрения облегчения последующего развития оно не представляет особого интереса.
      Существенно более важный результат — повышение наглядности. В традиционной инструментальной среде разработчик в подобной ситуации вынужден выбирать: либо попытаться все проявления пункта меню воплотить в одном наглядном сплошном (вертикальном) куске текста, но тогда лишиться перечисленных в предыдущем разделе преимуществ горизонтальных слоев программы, либо смириться с многосвязностью реализации пункта меню, обрекая себя на последующие мучительные поиски всех точек внесения изменений при необходимости добавить или удалить некоторый пункт. Техника однородного набора соединяет в себе лучшие стороны этих двух решений: реализация пункта локализуется в одном модуле, который легко добавляется и удаляется, но и все преимущества горизонтальных слоев остаются в силе, поскольку окончательный исходный текст, получающийся после раскрытия наборных гнезд, ничем не отличается от старого многослойного.
      Беда традиционного текста программы в его одномерности, которая вступает в противоречие с двумерной структурой, образуемой пунктами меню (вертикали) и разбросанными по программе отдельными компонентами их реализации (горизонтали). Во времена первых вычислительных машин текст программы ассоциировался с текстом книги, и потому его одномерность представлялась чем-то естественным (или даже более того, единственно возможным). Но теперь, в эпоху повсеместного распространения гипертекста, СУБД и других подобных многомерных средств одномерность текста, отображающего двумерную структуру, определенно выглядит анахронизмом.
      Современная операционная среда просто обязана предоставить разработчику возможность в полной мере проявить содержащуюся в программе многомерность. Оформив совокупность реализаций пунктов меню в виде однородного набора, разработчик вправе посмотреть на любой горизонтальный (совокупность одноименных полей) или вертикальный (полная реализация пункта) срез образовавшейся структуры, и возможность эта никак не должна ущемлять эффективность реализации. Кроме того, поддержка предлагаемого аппарата должна включать просмотр всех гнезд указанного набора как в форме исходного текста, так и в форме его расширения (результата работы препроцессора).
      Но наиболее интересные последствия применения техники однородного набора связаны с организацией подключения к программе новых модулей. Теперь новый пункт добавляется к меню безболезненно, без какого бы то ни было редактирования существующего отлаженного исходного текста программы: достаточно поместить в программный фонд новый модуль и объявить его принадлежащим однородному набору OurMenu. Так же безболезненно, без участия редактора происходит и удаление пункта. В результате мы избавляемся от источника весьма частых ошибок, бросавшего тяжелую тень неблагополучия на повседневный процесс развития программы.
      Вспомним, что в традиционной среде первым шагом работы по подключению нового пункта был поиск многочисленных точек программы, в которые требовалось внести изменения. Поиск этот неизбежно сопровождался осмыслением назначения каждой из точек-претендентов, поскольку даже обнаружение где-либо группы из шести однородных членов, относящихся к шести имеющимся пунктам, вообще говоря, не означало, что в данной точке должен появиться и представитель седьмого пункта: нельзя было механически отсечь предположение о том, что обнаруженная группа могла образоваться в результате случайного совпадения. Применение техники однородного набора, где канонизируются связи реализации пункта с остальной программой, снимает с разработчика эти заботы. Подключая пункт, он может вовсе не заглянуть в продуманные и размещенные ранее в программе наборные гнезда, а лишь сформировать известный набор компонентов модуля.

      Итак, привлечение техники однородного набора обеспечило множество важных преимуществ. Но насколько часто ситуации, близкие к описанной выше, встречаются в реальных программах? Попытаемся показать, что подобные обстоятельства достаточно типичны, для чего обратимся ко второму эпизоду, где фигурирует другой популярный продукт фирмы Borland — транслятор с языка Паскаль Delphi [Сван, 1996; Дантеман, 1995].

Далее

Рейтинг@Mail.ru