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

 Г л а в а  2
МОДУЛЯРИЗАЦИЯ
 
2.3. Технические мотивы модуляризации
 

 

2.3. Технические мотивы модуляризации

      Попытаемся ответить на вопрос: каковы побудительные мотивы модуляризации? Что подталкивает разработчика к выполнению нередко весьма трудоемкого процесса расчленения своего программного материала на модули?
      Безусловно важнейшими, фундаментальными мотивами такого расчленения являются систематизация и упрощение разработки, многократная используемость и изменяемость сложных программ. Им будет посвящен следующий раздел. Здесь же мы остановимся только на некоторых «технических» мотивах, связанных с той или иной стороной ведения программного хозяйства.

      2.3.1. Сокращение записи. Один из лежащих на поверхности мотивов модуляризации — сокращение записи. Повторяющиеся или близкие по содержанию части программы как бы выносятся за скобки. Вернее, «за скобками» формируется модуль, который становится единственным представителем всех повторявшихся (с точностью до параметризации) частей текста первоначальной программы. Текст программы сокращается, поскольку на месте вынесенных за скобки частей записываются лишь обращения к сформированному модулю. Оформляются такие модули в виде подпрограмм, макроопределений и т. п.
      На самом деле за текстуальной близостью вычленяемых в самостоятельный модуль сходных частей программы, как правило, легко просматривается функциональное родство. Если функциональное родство действительно имеет место, то преимуществом модуляризированной программы становится уже не столько сокращение ее размера, сколько отсутствие дублирования текста, негативные последствия которого разбирались в гл. 1. Строго говоря, там рассматривалось дублирование полностью совпадающих текстов, здесь же речь идет о более общем случае — о совпадении «с точностью до параметризации». Однако с точки зрения последствий это мало что меняет, так как любое дублирование угрожает серьезными технологическими издержками.
      В то же время сокращение записи — настолько очевидный мотив, что опасаться следует скорее его абсолютизации, а не недооценки. Оформление модулей, диктуемое другими мотивами (некоторые из них рассматриваются ниже), нередко приводит к увеличению размера программы. В частности, размер программы обычно увеличивается, если оформление модуля не связано с повторяющимися текстами, т. е. если к создаваемому модулю в программе будет только одно обращение. Очень часто такие издержки вполне оправданы.
      Да и всегда ли уместно тут говорить об издержках? Исчисление размера исходного кода в байтах или в строках в данном контексте выглядит несколько механистично. При модуляризации полезнее измерять размер модуля с учетом его сложности, например, по известной формуле [Холстед, 1981]

V = N log 2 n

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

      2.3.2. Сокращение времени трансляции. Сокращение суммарного времени трансляции достигается за счет выделения модулей в форме конструкций алгоритмического языка, допускающих автономную трансляцию. Это позволяет при внесении изменений перетранслировать только затронутый ими модуль, а не всю программу. В результате трансляции такого модуля обычно порождается вторичный объект программного фонда — объектный модуль (см. разд. 2.2.1).
      Интересно, что некоторые языки программирования (Алгол-60, Паскаль) в своих ранних версиях пренебрегали автономной трансляцией, но затем под давлением требований разработчиков больших программ вынуждены были так или иначе легализовать эту возможность. В более поздних языках (Ада и др.) автономная трансляция является одним из базовых понятий, но в то же время статус автономии все больше размывается. «Независимая» трансляция модуля, где абсолютно ничего не было известно о его будущих соседях по выполняемой программе, уступает место «раздельной» трансляции, происходящей в контексте знаний о некоторых свойствах партнеров модуля.
      Известен и более фундаментальный подход к проблеме сокращения интервала времени между внесением изменений в исходный текст и началом выполнения исправленной программы. Данный интервал удается свести к минимуму, применив инкрементальную сборку программы. Инкрементальная сборка протекает следующим образом.
      Языково-ориентированный редактор выявляет подвергшиеся редактированию конструкции языка и только их подает на вход инкрементального транслятора. Инкрементальный транслятор способен перетранслировать отдельные фрагменты исходного текста, корректируя в соответствующих местах полученный ранее объектный код. Далее (там, где при традиционной схеме должна была последовать ресурсоемкая процедура полной перекомпоновки) инкрементальные средства всего лишь вносят небольшие изменения в имеющуюся выполняемую программу (аналог загрузочного модуля). При инкрементальной сборке задержка выполнения исправленной программы становится, вообще говоря, практически незаметной.
      Инкрементальный транслятор умеет транслировать в контексте ранее оттранслированного окружения большинство сколько-либо крупных языковых конструкций. Поэтому проблема специального выделения автономно транслируемых модулей теряет здесь свою актуальность. Другими словами, данный мотив модуляризации в среде инкрементальной сборки не действует.
      Инкрементальный подход к трансляции и компоновке массового распространения пока не получил. Современный транслятор обычно способен принять только вполне определенную языковую единицу — модуль трансляции. Все настолько привыкли к этому, что нередко другие формы модуля, не допускающие, вообще говоря, автономной трансляции, относят к разряду малоинтересных нелепостей.
      Однако, как будет видно из последующего изложения, при определенных обстоятельствах такие формы оказываются весьма полезными. Так, иногда требуется модуль размером всего в несколько символов (например, точность представления значения некоторой переменной — см. разд. 2.3.5), который не представляет ни малейшего интереса с точки зрения экономии времени трансляции и потому не допускается большинством трансляторов.
      Или же многосвязный модуль, текст которого состоит из нескольких фрагментов, принадлежащих, вообще говоря, различным модулям трансляции. Многосвязный модуль может понадобиться, например, когда нужно проследить или скорректировать влияние на программу некоторого нетривиального аспекта исходной постановки задачи.
      Непосредственно оттранслировать такой модуль нельзя, нельзя даже автономно проверить его синтаксическую корректность, поскольку транслятором подобный текст будет воспринят как чужеродный объект. Но это — всего лишь мелкие технические неудобства, и из-за них определенно не стоит отказываться от использования нетрадиционных, но продуктивных форм модуля.
      Здесь некоторое недоумение вызывает позиция Н.Вирта, который в недавно опубликованном интервью [Вирт, 1998] заявил, что «расширяемое программирование подразумевает возможность добавления модуля без какого-либо изменения существующих модулей — они не должны даже перекомпилироваться». С первой половиной утверждения (отсутствие изменений существующих модулей) можно уверенно согласиться, но для чего понадобилось безусловно исключить трансляцию из процесса подключения нового модуля — не совсем понятно. Похоже, неявная отправная точка последнего тезиса состояла в том, что Н.Вирт видит подключаемый модуль исключительно в образе модуля трансляции.

      2.3.3. Удобство редактирования и наглядность. Для удобства редактирования текста программы имеет смысл разбивать большую программу на более мелкие части, которые выступают в качестве обозримых и потому более удобных объектов редактирования. Характерная форма модуля здесь — файл, содержащий часть текста программы и являющийся объектом для текстового редактора.
      К сходным формам модулей приводят и соображения наглядности, или легкости восприятия, удобочитаемости программы. Выделяются модули, логику которых можно «охватить одним взглядом».
      В то время как при модуляризации программы мотив наглядности редко оказывается на первом месте, при расчленении текстовой документации соображения удобочитаемости, напротив, обычно становятся решающими. Успех любой книги во многом зависит от качества ее структуризации. Очевидно, что если при написании даже коротенькой брошюры не применять традиционные средства рубрикации (главы, разделы, абзацы и т. д.), то восприятие такого «текстового монолита» потребует от читателя чрезмерных усилий.
      Задача, решаемая при структурировании книги, в некотором смысле более сложна, чем задача модуляризации программы. Автор книги обычно стремится к тому, чтобы читатель мог изучить материал «строго линейно», т. е. последовательно прочитывая страницу за страницей, не заглядывая вперед и не возвращаясь к ранее прочитанному. Разработчик программы, напротив, линеаризации своего текста внимания практически не уделяет.
      Линейное построение материала вполне оправдано в беллетристике или, скажем, в учебнике истории, где автор традиционно придерживается хронологической последовательности изложения. Для большинства же научных монографий линеаризация выполняется с известной долей насилия над содержанием книги, насыщенным обычно многочисленными перекрестными связями. Не меньшим насилием выглядела бы и попытка линеаризации текста крупной программы.
      Альтернативой линейному построению является завоевывающая все большую популярность техника гипертекста. В гипертексте материал расчленяется на отдельные относительно независимые страницы. Каждая из страниц представляет собой некоторый информационный квант, содержащий, вообще говоря, множество ссылок (гиперссылок) на другие страницы, уточняющих или поясняющих встречающиеся в тексте кванта понятия. Таким образом, имеющиеся в исходном материале разнообразные связи между понятиями находят свое отражение в виде гиперссылок.
      От забот о линеаризации материала разработчик гипертекста избавлен. Тем не менее и тут рациональная организация текста требует известных творческих усилий, однако они обычно с лихвой окупаются. Благодаря размещению гипертекстового материала в памяти ЭВМ читатель получает возможность стремительно перемещаться по любой из гиперссылок, мгновенно докапываясь до всех интересующих его подробностей.
      Вычленение модулей в программе существенно больше напоминает вычленение страниц в гипертексте, чем структуризацию линейного текста книги. Среди множества внешних связей программного модуля хронологические (по порядку выполнения) отношения «предшествующий» и «следующий» — аналоги линейного построения материала — не играют, как правило, сколько-нибудь заметной роли, а часто и вовсе не определены. В то же время читателю программы обычно доступны все необходимые ресурсы ЭВМ, и при надлежащей поддержке со стороны операционной среды он мог бы существенно повысить эффективность процесса изучения программы, перемещаясь по мере необходимости по любой из многочисленных связей модуля.
      Если повнимательнее приглядеться к позиции гипертекста в ряду современных средств изучения текста программы, станет заметной известная несбалансированность. С одной стороны, на уровне модулей, допускающих автономную трансляцию, гипертекстовая многокомпонентная структура программы настолько общепринята, что неожиданно свежей может показаться мысль об обеспечении возможности систематического последовательного линейного просмотра всего текста (разумеется, наряду с обычным хаотическим блужданием). С другой стороны, если спуститься ниже, на уровень строк исходного текста, то здесь, напротив, нелинейность — явление достаточно редкое: почти не встречаются конструкции, хоть немного более интересные, чем, скажем, предложение #include в Си.
      Программа многому могла бы поучиться у организации традиционного текста. Например, широкое признание получил тезис о том, что и проектным спецификациям, и сопроводительной документации имеет смысл придать форму нескольких преломлений под различными углами зрения, иначе говоря, нескольких различных проекций описываемого продукта, где число и состав проекций определяются спецификой предметной области. В то же время нечасто приходится слышать о распространении этой многообещающей идеи на исходные тексты программы.
      Техника гипертекста применяется как для реализации связей между соседними равноправными понятиями, так и для организации иерархических структур, напоминающих рубрикацию книги. Но если в книге объект, занимающий достаточно высокое положение в иерархии (например, глава), традиционно имеет весьма внушительные размеры, то в гипертексте он чаще всего представлен в компактной форме — в виде одной страницы. От этой страницы, разумеется, тянутся связи к подчиненным объектам. (В книге подобный элемент — список разделов или аннотация главы — не обязателен и встречается относительно редко.)
      Точно так же, если в программной среде существуют какие-либо иерархические отношения, то каждому вышележащему объекту целесообразно сопоставить самостоятельный первичный модуль, в котором, в частности, так или иначе очерчивается круг подчиненных объектов. Некоторые примеры таких «иерархов» (описание конкретной конфигурации программы, библиотека) рассматривались в разд. 2.2.
      Применительно к иерархическим модулям можно говорить о двух размерах модуля: о сумме размеров всех подчиненных модулей и о размере текста самого модуля-иерарха. В данном разделе мы будем использовать второе толкование размера.

      2.3.4. Размер модуля. Ограничения сверху. Удобство редактирования и наглядность диктуют ограничения сверху на размер модуля. Во многих работах, обобщающих опыт создания больших программ, предлагаются чисто количественные ограничения, которые обычно колеблются в диапазоне 30–300 строк исходного текста.
      Возможны и несколько иные подходы к заданию ограничений на размер модуля. Например, когда на заре программирования широко использовались блок-схемы, популярно было следующее определение: «Блок-схема — графическое изображение алгоритма на одном листе бумаги». Ударение тут делалось на слове «одном», так как какое-либо перелистывание страниц при изучении блок-схемы безнадежно запутывает дело.
      Применяя подобный подход к определению размеров модуля, можно потребовать, чтобы его текст целиком умещался, скажем, на экране дисплея. На первый взгляд, ограничение слишком жесткое — на экране помещается обычно 25–30 строк. Но, располагая языком достаточно высокого уровня и дополнив текстовый редактор эффективными средствами переключения (в стиле гипертекста) на вызывающий, на соседние и на вызываемые модули, можно получить вполне жизнеспособную операционную среду.
      Размеры 30–300 строк характерны для описания класса, т. е. для набора (или подмножества набора) данных и методов, реализующих объект некоторого типа. Если же спуститься ниже и говорить о модуле как об элементарном кирпичике, лежащем в основании алгоритма, то типичный размер такого модуля окажется существенно меньше.
      Замечено [Цейтин, 1990], что при программировании на языке Форт, когда разработчик не стеснен многочисленными ограничениями, навязываемыми операционной средой, создаваемые им модули имеют обычно размер 7±2 предложения. К тем же цифрам, но уже применительно к более распространенным языкам, приводят следующие наблюдения.
      Один из наиболее популярных рецептов комментирования программ предписывает формирование комментариев-заголовков, специфицирующих назначение следующего за таким комментарием фрагмента программы. Анализ программ, написанных по данному рецепту, показывает, что размер первичных (т. е. не содержащих внутри себя комментариев-заголовков) прокомментированных фрагментов колеблется, как правило, в тех же пределах: 7±2 предложения.
      Согласно введенному ранее определению, прокомментированный фрагмент, безусловно, следует считать модулем. Такой модуль позволяет в известной мере спрятать («инкапсулировать») алгоритмические подробности решения задачи, сформулированной в заголовке. Среди мотивов, ведущих к вычленению прокомментированных фрагментов, выделим два: наглядность и изменяемость программы.
      Наглядность существенно улучшается за счет того, что появляется прекрасная возможность для беглого просмотра программы. При беглом просмотре достаточно читать комментарии-заголовки, практически не обращая внимания на весь остальной текст.
      Прокомментированные фрагменты способствуют изменяемости. Если потребуется включить в программу новый вариант решения задачи, поставленной в комментарии-заголовке, то не надо искать границы вносимых изменений. Они, очевидно, совпадут с границами прокомментированного фрагмента.
      В некоторых проектах языковых сред прокомментированным фрагментам уделяется особое внимание. Это было сделано, например, в работе [Тейтелбаум, 1981], где им придан статус полнокровных конструкций алгоритмического языка (ПЛ/1). Тем самым появилась возможность в языково-ориентированном редакторе выполнять над такими фрагментами ряд операций, облегчающих просмотр и модификацию текста.

      2.3.5. Размер модуля. Ограничения снизу. Итак, создавать слишком большие модули неразумно. Но стоит ли создавать слишком маленькие? Практика показывает, что в некоторых ситуациях модули малого размера совершенно необходимы.
      Рассмотрим, например, часто встречающийся случай оформления варианта для выполнения расчета с той или иной точностью. (Вариантный фрагмент программы является модулем, побудительный мотив к его оформлению — изменяемость программы, о которой речь пойдет далее, в разд. 2.4.4.) Пусть точность вычислений зависит только от спецификации одной переменной: быстрая, но дающая грубое приближение версия программы получается, если переменная представлена с одинарной точностью, а медленная, но более точная версия — если с двойной. Тогда размер модуля (вариантного фрагмента) будет очень мал: модулем станет часть спецификации этой переменной, задающая ту или иную форму представления.
      Сколько-нибудь весомых соображений в пользу ограничения размера модуля снизу обычно никто не выдвигает. Однако на практике малые размеры модуля зачастую влекут за собой большие проблемы. Например, технические ограничения операционной среды могут не позволить оформить в виде самостоятельного модуля часть строки программы.
      Кроме того, оформление нового модуля, как правило, подразумевает создание нового файла. Файл традиционно рассматривается операционной средой как достаточно крупный объект, под который «широким жестом» отводится не менее одного блока пространства на диске. Если модуль мал, то его текст занимает лишь небольшую часть блока, что приводит к неэффективному расходованию внешней памяти.
      Встречаются операционные среды, где нет ни макросредств, ни средств подстановки (in-line) тела подпрограммы на место ее вызова. В подобной среде модуль, независимо от его размера, реализуется как вызываемая подпрограмма. При малых размерах модуля может оказаться, что относительный вес накладных расходов (периода выполнения) на вызов подпрограммы будет непозволительно велик.
      Наконец, модуль малого размера часто удобнее просматривать не в качестве самостоятельного объекта, а в контексте вызывающей программы в форме непосредственной подстановки текста модуля на место его вызова. Очень немногие текстовые редакторы обеспечивают возможность такого просмотра.
      Каков бы ни был конкретный источник затруднений, можно только посочувствовать разработчику, когда те или иные ограничения операционной среды вынуждают его отказываться от оформления малого модуля. Если же операционная среда хоть в какой-то мере претендует на обслуживание расширяемых программ, то она просто обязана предоставить эффективные средства для работы с модулями небольшого размера.

      2.3.6. Распределение работ. При распределении работ по написанию программы между несколькими исполнителями в качестве модулей выступают части, реализуемые отдельными исполнителями. Обычно стремятся к минимизации связей между такими частями, что позволяет сократить количество и интенсивность взрывоопасных совместных обсуждений, проводимых в ходе разработки.
      Наиболее популярными являются иерархические отношения в коллективе разработчиков. Так, широко известна методика группы главного программиста, где основную творческую работу выполняет один высококлассный специалист, а его помощники берут на себя все скучные, рутинные дела: трансляцию, архивизацию, инструментарий, часть тестирования и др.
      Для крупных проектов (свыше 100 тыс. строк исходного текста) требуется даже несколько ступеней иерархии: назначаются проектировщик верхнего уровня, детализирующие его указания проектировщики следующих уровней, сотрудники, занятые непосредственным кодированием, тестированием, ведением программного фонда и т. д. В то же время для проектов среднего размера нередко принимаются схемы организации, при которых фактически никакой иерархии нет, все непосредственные исполнители находятся на одном уровне подчиненности и имеют равные права.
      Таким образом, распределение работ действует как мотив модуляризации практически при любой форме организации коллектива исполнителей, за исключением, может быть, группы главного программиста. Помимо минимизации межмодульных связей здесь следует позаботиться и об учете индивидуальных возможностей каждого сотрудника, что нередко позволяет получить крупный выигрыш в производительности труда.

      2.3.7. Распределенное выполнение. В связи с повсеместным распространением компьютерных сетей все чаще возникает потребность расчленить программу на модули, допускающие распределенное выполнение. Если поначалу в большинстве случаев вычленялись всего лишь два таких модуля — клиентский и серверный, то теперь преобладает более энергичный подход: вся программа мыслится как совокупность модулей (которые тут часто называют компонентами), каждый из которых способен выполняться вдалеке от других, на отдельной вычислительной установке.
      Соединенная в сеть вычислительная среда, где происходит распределенное выполнение программы, как правило, гетерогенна: она формируется из несхожих между собой вычислительных установок. Поэтому обычно требуют, чтобы компонент обладал еще и способностью выполняться на различных платформах. Многоплатформенность ведет к более жесткой стандартизации межкомпонентных интерфейсных соглашений по сравнению с соглашениями, обслуживающими распределенное выполнение в гомогенной среде.
      Формирующиеся тем самым стандарты интерфейса до такой степени абстрагируются от второстепенных технических деталей, что уже не составляет труда добавить к ним еще одно полезное требование: независимость от языка программирования. Примером удачного решения всех упомянутых проблем служат соглашения CORBA [КОРБА, 1999].
       Итак, по современным представлениям компоненту, вычленяемому по мотиву распределенного выполнения, желательно привить также способность функционировать на различных вычислительных платформах и возможность быть написанным на любом из распространенных языков программирования. Таких компонентов создано уже достаточно много, они не только широко применяются в сетевой среде, но и оказались удобным конструктивом для многократного использования в нескольких программах, о чем пойдет речь далее, в разд. 2.4.3.

      2.3.8. Сегментирование. Потребность размещения длинной программы в оперативной памяти ограниченного объема может быть удовлетворена посредством выделения модулей (сегментов), сменяющих друг друга в оперативной памяти во время выполнения программы. Главное, к чему следует стремиться при сегментировании, — минимизация числа смен модулей, т. е. подкачек сегментов из внешней памяти в оперативную.
      Оперативная память дешевеет, ее объемы постоянно растут, и потребность в сегментировании возникает все реже и реже. Кроме того, сегментирование часто вытесняется такими методами размещения длинных программ, как виртуальная память. Эти методы несколько менее эффективны, но зато не требуют от разработчика специальных усилий по модуляризации.
      Все же прибегать к сегментированию время от времени приходится, и поэтому стоит упомянуть о его негативных сторонах. Сегментирование является серьезным тормозом последующего развития программы. Весьма неуютно чувствует себя разработчик, постоянно ожидая, что вносимые им изменения в очередной раз выведут размер объектного кода за рамки ограничений оперативной памяти и потребуется фундаментальная реорганизация разбиения на сегменты. По этой причине, в частности, для сегментированных программ не удается организовать безболезненное (см. разд. 1.2) выполнение изменений.

Далее

Рейтинг@Mail.ru