Г л а в а 5
ОДНОРОДНЫЙ НАБОР
Рассматривая каркасный подход к построению расширяемой программы, мы выделили два типа гнезд каркаса: вариантные и наборные (см. разд. 3.6). В вариантное гнездо при формировании конкретной конфигурации программы всегда подставляется ровно один сменный модуль, в наборное гнездо может подставляться сразу несколько модулей. На регулярном использовании вариантных гнезд основываются проектирование и реализация многовариантных программ, которым была посвящена предыдущая глава. В данной главе мы разберем применение наборных гнезд.
Потребности и в вариантных, и в наборных гнездах могут возникать в задачах из практически любой предметной области. Однако в вариантных гнездах наиболее остро нуждается относительно обособленная отрасль программирования задачи вычислительного эксперимента. У наборных гнезд также существует такой особо заинтересованный заказчик, но уже общепрограммистского масштаба. Заказчик этот не получившая пока широкого признания, но весьма многообещающая стратегия поэтапной разработки программ. Следуя терминологии, предложенной С.С.Лавровым, будем называть эту стратегию программированием «вширь».
5.1. Программирование «вширь»
5.1.1. Поэтапная разработка программ. То, что большие программы следует разрабатывать поэтапно, давно ни у кого не вызывает сомнений. Расчленение процесса создания программы на ряд относительно самостоятельных этапов наделяет этот процесс многими полезными качествами.
Как было показано в гл. 2, разбиение программы на модули облегчает ее восприятие, поскольку каждый из модулей, как правило, существенно проще, чем программа в целом. Подобный эффект достигается и при разбиении процесса разработки на этапы: процесс становится обозримым, поскольку на каждом из этапов в рассмотрение вовлекается лишь некоторая часть из всего множества конструкций программы.
При поэтапной разработке можно на ранних стадиях «обкатать» наиболее тонкие алгоритмические решения и застраховаться тем самым от крупных переделок программы на заключительной стадии.
Количество и состав этапов, выполненных к определенному сроку, позволяют объективно оценить степень готовности разрабатываемой программы, что дает возможность своевременно скорректировать усилия как разработчиков, так и заказчика.
Иногда на ранних этапах удается создать небольшой макет будущего программного продукта, реализующий, пусть крайне неэффективно, основные функциональные возможности. В процессе пробной эксплуатации такого макета заказчик уточняет спецификации программы, а затем на последующих этапах неэффективные части макета заменяются эффективными.
Можно легко продолжить список достоинств поэтапной разработки, но для наших целей вполне достаточно и приведенных соображений. Теперь, убедившись в полезности расчленения процесса разработки на этапы, перейдем к рассмотрению известных стратегий такого расчленения.
В теории и практике программирования наиболее популярны стратегии «сверху вниз» и «снизу вверх». К этим стратегиям ведет следующее вполне логичное построение. Прежде всего постулируется, что проектирование программы включает в себя взаимодополняющие процессы анализа и синтеза. Анализ трансформируется в движение «сверху вниз» расчленение решаемой проблемы на относительно независимые аспекты и, соответственно, расчленение разрабатываемой программы на модули (см. разд. 2.4.2). Синтез позволяет соединять между собой «снизу вверх» вновь создаваемые части нижнего уровня, а также накопленный ранее программный материал.
Далее мы вспоминаем о пользе поэтапной разработки. Ее сравнительно легко удается скрестить как с тем, так и с другим направлением. Наиболее хорошо известен результат скрещивания поэтапной разработки с направлением «сверху вниз», который получил название пошаговое уточнение (step-wise refinement).
При пошаговом уточнении очередной этап разработки оставляет после себя некий полуфабрикат создаваемой программы, где верхняя, заголовочная структура уже в основном сформирована, а «на нижних этажах» встречаются еще нереализованные части. На месте каждой из нереализованных частей располагается пара: заглушка и спецификация. Заглушка служит для отладки, имитируя в весьма ограниченном объеме поведение отсутствующего пока куска программы. Спецификация задает на псевдокоде (т. е. на некотором формализованном языке) назначение нереализованной части. Иногда псевдокод допускает создание интерпретатора, и тогда надобность в заглушках отпадает, поскольку их роль берет на себя интерпретация спецификаций. Однако тут требуется утомительно строгое и подробное специфицирование, и, кроме того, интерпретация нередко ведет к недопустимому замедлению выполнения отлаживаемой программы.
На следующем этапе разработчик выбирает одну или несколько нереализованных частей и уточняет их, т. е. подставляет на их место тексты алгоритмов (реализующих спецификации), которые, в свою очередь, могут содержать заглушки и спецификации. Так постепенно, этап за этапом, происходит реализация (уточнение) недостающих частей. На выходе последнего этапа уточнения появляется полноценный текст программы.
Похоже выглядит и поэтапная стратегия «снизу вверх». Для отладки опять потребуются заглушки. Здесь они имитируют поведение не подчиненных частей, а среды, в которой будет функционировать отлаживаемый нижележащий фрагмент в окончательной версии создаваемой программы.
Обе стратегии можно применять на различных стадиях жизненного цикла программы. Можно сначала с их помощью выполнить проектирование, полностью определив состав и интерфейсы входящих в программу модулей, и лишь затем приступить к реализации, которая, в свою очередь, пойдет по направлению «сверху вниз» и/или «снизу вверх». Можно впасть и в другую крайность, торопясь программировать и отлаживать еще сырые модули незаконченного проекта.
Такие полярные варианты взаимодействия процессов проектирования и реализации оказываются применимыми только для программ небольшого или среднего размера. Создание же крупной программы обычно связано с поиском разумного компромисса между этими вариантами.
В самом деле, полное проектирование всей модульной структуры крупной программы, не контролируемое, хотя бы частично, отладкой, приводит к построению слишком зыбкого, чисто умозрительного фундамента последующих работ, который нередко дает трещины на первых же шагах реализации. Не менее опасен и вариант, где проектирование и реализация идут нога в ногу. Тут неизбежны неприятные сюрпризы на завершающей стадии разработки, когда очередной спроектированный модуль предъявляет непредвиденные требования к уже существующей среде, влекущие за собой серьезную реорганизацию написанного и отлаженного ранее программного материала.
Однако и компромиссные варианты порождают определенные технологические сложности. Разработчик здесь должен постоянно иметь дело как бы с двумя слоями создаваемой программы. Первый из слоев записан практически в окончательном виде и находится в стадии отладки. Второй слой состоит из спроектированных модулей, относящихся к столь глубинным частям программы, что еще не время думать о сколько-нибудь содержательном включении их в пригодный к частичной отладке полуфабрикат.
Стратегии «сверху вниз» и «снизу вверх» освящены внушительным количеством теоретических работ и получили широкое распространение на практике. Однако при внимательном изучении становится видно, насколько они далеки от идеала: наряду с отмеченными трудностями взаимодействия проектирования и реализации у этих стратегий имеются и другие слабые стороны.
Определенное противоречие несет в себе применение заглушек. С одной стороны, простенькие дешевые заглушки недостаточно полно имитируют среду, в которой предполагается впоследствии использовать реализуемые на данном этапе части программы, и в результате дальнейшие этапы разработки отягощаются доотладкой созданных ранее частей. С другой стороны, поскольку заглушки обречены на отмирание на последующих этапах, представляется нерациональным расходовать на них значительные усилия.
Наконец, механическое применение упомянутых стратегий поэтапной разработки означает неявное принятие довольно-таки унылого и малопродуктивного конфигурационного ориентира иерархической модульной структуры. Ведь именно такие, строго иерархические отношения складываются, вообще говоря, между модулем, содержавшим на раннем этапе спецификацию/заглушку, и модулем, реализованным затем при ее уточнении.
Бороться с перечисленными недостатками можно по-разному. Первое, что приходит в голову, попытаться дополнить канонические средства пошагового уточнения, превратив формируемый полуфабрикат программы в некое подобие гипертекста, с помощью которого удалось бы материализовать зарождающиеся в голове разработчика размытые представления о проектируемой модульной структуре, совместив их с уже готовыми частями алгоритма. Определенные шансы в борьбе с засильем иерархии дает объектная ориентация.
Однако существует и еще одно решение, лежащее в совершенно иной плоскости. Предлагается сменить или, точнее, видоизменить используемый конфигурационный ориентир и затем сознательно поставить во главу угла стремление к этому новому ориентиру. В результате будет построена новая, избавленная от многих отмеченных недостатков стратегия поэтапной разработки программирование «вширь». Описанию этой стратегии посвящен следующий раздел.
5.1.2. Стратегия «вширь». При программировании «вширь» главным конфигурационным ориентиром провозглашается набор однородных модулей. Другими словами, при проведении модуляризации предписывается в первую очередь выявлять однородные части программы и определенным образом оформлять их. Желательно, чтобы суммарный объем выделенных однородных модулей оказался достаточно большим, они должны оттянуть на себя основную массу работ по созданию программы. Практика показывает, что главное это поверить в перспективность однородных наборов; после этого, как правило, однородные модули вычленяются относительно легко.
Приведем характерные примеры больших однородных наборов. В трансляторе значительную долю общего объема составляют однородные модули, реализующие отдельные конструкции входного языка. В операционной системе части, обслуживающие различные классы периферийных устройств. В текстовом редакторе процедуры, выполняемые при нажатии функциональных клавиш. В оптимизационных задачах реализации методов, используемых для приближения к экстремуму заданной функции. В задачах математической физики алгоритмы обработки различных типов узлов сетки. При моделировании некоторой установки или явления модули, учитывающие различные физические эффекты.
В одной программе может быть выявлено несколько однородных наборов. Так, если при моделировании применяются сеточные методы, то выделяются и типы узлов, и физические эффекты.
После того как однородные модули выявлены, приступают к непосредственному программированию, которое, собственно, и разбивается на этапы. На первом этапе создается лишь минимальное число представителей каждого из выделенных однородных наборов. Если поиск однородности проводился достаточно энергично, то обычно оказывается, что объем работ первого этапа реализации сравнительно невелик.
Несмотря на это программа, получаемая на первом этапе, технологически уже вполне самостоятельна: для ее отладки не требуется никаких заглушек. В приведенных выше примерах однородных наборов будет получен, скажем, транслятор, реализующий предельно малое подмножество языка, операционная система, обслуживающая лишь один класс периферийных устройств, редактор, реагирующий на единственную клавишу Конец сеанса, и т. д.
Последующие этапы разработки заключаются в непосредственном программировании все новых и новых однородных модулей. Если место размещения однородного набора оформить в виде наборного гнезда (заполняемого по ассоциативной схеме, см. разд. 3.6.3), то подключение новых модулей будет происходить безболезненно для окружения, т. е. не требуя редактирования текстов существующих программ. Подобно рассмотренному в предыдущей главе сменному вариантному модулю, вновь создаваемый модуль для наборного гнезда программируется в исключительно благоприятной атмосфере, его интерфейс с объемлющей средой не только заранее разработан, но и в значительной степени отлажен на однородных предшественниках этого нового модуля.
Поясним теперь, как программирование «вширь» соотносится со стратегиями «сверху вниз» и «снизу вверх» и почему тут удается преодолеть отмеченные недостатки этих стратегий. Прежде всего заметим, что основные постулаты сопоставляемых стратегий не являются взаимоисключающими, стратегии могут сравнительно легко сочетаться между собой.
Так, приняв за основу программирование «вширь» и дополнив его стратегией «сверху вниз», мы приходим к (довольно очевидной) мысли о том, что при непосредственном программировании очередного однородного модуля большого объема следует вновь попробовать вычленить в нем однородные наборы, к которым вновь будет применена стратегия «вширь». И обратно, можно попытаться несколько улучшить стратегию «сверху вниз», при прочих равных условиях отдавая предпочтение вычленению однородных модулей.
Подобные рассуждения в определенной степени применимы и к стратегии «снизу вверх». Например, вычленяя и реализуя нижнеуровневые фрагменты создаваемой программы, можно было бы особое внимание уделить однородным наборам. Заглушка для имитации объемлющей программы тут, к сожалению, понадобится, но написать ее придется только для отладки первого однородного модуля; все последующие модули из того же набора смогут, по-видимому, воспользоваться услугами той же заглушки.
И все же, несмотря на возможность сращивания с соседями, программирование «вширь» вправе претендовать на статус самостоятельной стратегии. Заложенные в его основание идеи достаточно самобытны и, главное, представляют несомненный практический интерес: ведь здесь, в частности, успешно преодолеваются серьезные недостатки других стратегий. Некоторые из таких недостатков упоминались в разд. 5.1.1. Рассмотрим, каким образом удается устранить их.
Программирование «вширь» позволяет избежать определенной части коллизий в отношениях между процессами проектирования (модуляризации) и непосредственного кодирования программ. Модуляризация тут движется, вообще говоря, более крупными шагами, чем при использовании стратегий «сверху вниз» и «снизу вверх». Возможно, вычленение большого однородного набора потребует дополнительных усилий, зато в результате обеспечивается требуемое опережение, создается крупный задел спроектированной модульной структуры. Возвращаться к вычленению новых модулей теперь, вероятно, долго не понадобится: открывается широкий фронт работ по непосредственному программированию модулей из выделенного набора.
Другой существенный недостаток стратегий «сверху вниз» и «снизу вверх» необходимость использования заглушек. Программированию «вширь» заглушки, вообще говоря, не требуются. Как видно из приведенных примеров, в отладке участвует только реализованная часть однородных модулей, обращение к нереализованным модулям, как правило, невозможно, и поэтому их поведение не надо имитировать.
Заглушки или интерпретируемые спецификации для однородных модулей могут применяться только в случае, если разработчик пожелает, чтобы при отладке были видны направления дальнейшего развития программы. Например, если при отладке редактора будет нажата некая вошедшая в проект, но пока еще не реализованная клавиша "X", то соответствующая заглушка позволит получить диагностику «Клавиша "X" еще не реализована». Если заглушки нет, диагностика будет грубее: «Нажата недопустимая клавиша "X"». Но обычно такие нюансы, как отмеченное отличие в выдаваемых сообщениях, не могут оказать заметного влияния на процесс отладки. Поэтому подобное применение заглушек скорее исключение, чем правило.
Часто присутствие заглушек однородных модулей в отлаживаемой на очередном этапе программе не только не требуется, но и нежелательно. Так, в оптимизационной задаче, вероятно, полезнее попытаться достигнуть экстремума с помощью уже запрограммированных методов, чем прекратить выполнение с диагностикой «Здесь самым перспективным является нереализованный метод ...».
При программировании «вширь» разгружаются и спецификации незапрограммированных однородных модулей. Прежде всего, тут нет необходимости отражать интерфейсные соглашения: все модули одного однородного набора имеют одинаковый интерфейс, который описан при объявлении гнезда. Кроме того, спецификации нередко служат просто для указания места расположения нереализованной части программы. Для однородного модуля указывать место не надо: оно задано расположением соответствующего наборного гнезда.
Еще одно важное достоинство жизнеспособность, самодостаточность результатов промежуточных этапов. Благодаря этим качествам появляется шанс раньше начать опытную эксплуатацию, раньше получить замечания пользователей и тем самым не дать укорениться в создаваемой программе последствиям допущенных просчетов.
Итак, предложив новый конфигурационный ориентир, нам удалось построить перспективную стратегию поэтапной разработки программы. Пока нельзя, к сожалению, сказать, что программирование «вширь» завоевало прочные позиции в системе программистских ценностей. Из крупных отечественных работ, пропагандирующих близкие конструкции, можно назвать только «вертикальное слоение» [Фуксман, 1979]. Хочется верить, что такое невнимание к данной стратегии явление временное, скоро она выйдет из полосы тени и займет подобающее ей положение.
Недостаточное распространение программирования «вширь» отчасти объясняется объективными причинами. Эта стратегия может принести весомую пользу и в неприспособленной операционной среде. Однако для того чтобы почувствовать все ее преимущества, требуются специализированные средства системной поддержки. Но, как уже не раз отмечалось, в традиционных операционных средах подобные средства обычно отсутствуют.
Какие же системные средства понадобятся для поддержки программирования «вширь»? Все эти средства группируются вокруг основной используемой здесь конструкции наборного гнезда.
|