Модифицированный цепочечный подход

УДК 519.683.8

МОДИФИЦИРОВАННЫЙ ЦЕПОЧЕЧНЫЙ ПОДХОД

© 2005 г. А.Н. Кручинин, А.Н. Литвиненко

In this paper new observations in the field of component-software architecture are presented and discussed. There is obtained modification of threaded approach. The paper considers a new advance of construction a project in the context of threaded approach.

Введение

Исходные тексты программ имеют специальную структуру, определяемую языком программирования. Часто удобно и важно накладывать на эти тексты другую структуризацию, связанную с тем или иным аспектом рассмотрения этих текстов. Мы стремимся разделять код на различные процедуры, модули, компоненты с целью изолирования различного поведения некоторых сущностей, для отделения интерфейса от сложностей выполнения или для повторного использования (reuse) в будущем кода, на который однажды было потрачено наше время и силы. Каждая цель рассмотрения программы, например анализ реализации некоторой функциональности, требует выделения одних фрагментов программы и «фонового» рассмотрения других. Почти всегда при подобном делении мы достигаем сразу нескольких целей, что дает нам уверенность в правильно выбранном пути, возможность быстрого восстановления логики всего проекта, если по какой-либо причине мы долгое время им не занимались, и нередко помогает избежать запутанности кода. Однако при делении программы на различные программные единицы (будь то подпрограммы, компоненты или классы), можно столкнуться со сложностями, обусловленными некоторой неоднородностью выходных данных исходных программных единиц. Конечным результатом любой программной единицы являются данные, которые были получены в процессе ее работы. Неоднородность может, например, складываться из разнотиповых переменных, таблиц базы данных, объектов некоторого класса, записей и пр. Основная задача, возникающая в данном контексте, - передача данных между отдельными программными единицами. Обмен информацией возникает на различных уровнях взаимодействия. Ими могут быть проекты, независимые программы, внешние функции и пр. При рассмотрении программ с такой точки зрения обычно говорят об исследовании возникающих или возможных потоков неоднородных данных.

Взаимодействие программных единиц

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

тономно от других программных модулей. Как правило, они оформляются в виде отдельных файлов: один вход, понимаемый как некий шлюз для потока ввода, и один выход. На входе программный модуль получает определенный набор исходных данных, выполняет содержательную обработку и возвращает один набор результирующих данных, т.е. реализуется стандартный принцип IPO (Input - Process - Output) - вход-процесс-выход [1]; в задачи модуля входит выполнение перечня регламентированных операций, достаточных для завершения начатой обработки и реализации каждой отдельной функции в полном составе. Построение программы, основанное на взаимодействии модулей, называют модульным программированием.

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

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

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

пример, подход структурирования программы на основе ООП никак не вписывается в рамки одного лишь последовательного взаимодействия модулей. Однако существуют задачи, решение которых полностью укладывается в приведенную схему, называемую цепочечным подходом (например, нередко реализация численных методов решения математических задач может быть описана с точки зрения взаимодействия независимых модулей). Тем не менее из существующей практики и анализа подхода цепочка модулей признана бедной [2] как подход к моделированию программы. Такая бедность здесь не должна пониматься как запрет на использование, а скорее рассматривается как неэффективный метод в случае ее исключительного использования. Другими словами, применение цепочечного подхода весьма эффективно наряду с другими применяемыми подходами и мало эффективно в противном случае.

Цепочечный подход

Связанные модули взаимодействуют между собой через передачу данных; при этом данные являются входными для одного и выходными для другого (рис. 1).

Рис. 1

Определение 1. ПустьX, Y, Z - множества данных. Будем называть модулем, действующим на множестве данных M (с X), всякое отображение f : M ^ N, где N (с Y). Здесь M, N - множество входных и выходных данных соответственно.

Определение 2. Назовем два модуля f : M (с X) ^ Y и g : N (с Y) ^ 2 связанными, если

Ух £ М Бу £ N : f (х) = у и У у £ N Б2 £ К (с 2):

g (у) = 2 ; g (f (х)) = 2 или х ^ f ^ g ^ 2, где х -

входные данные; 2 - выходные.

Рассмотрим пример:

Пусть а, Ь, с, 4 е, g (с М) - набор входных данных, а Ща), :Е(Ь, с), е, g) - модули; причем, известно, что существует связь ЩЩа), е, 12(Ь, с)). Требуется получить выходные данные от модуля й.

При обычном подходе решение поставленной задачи сводится к обратному ходу вычислений: на первой стадии исполняются П(а) и 12(Ь, с), затем результаты пересылаются в модуль й, который, получив весь необходимый набор данных, производит с ними необходимые действия. Конечно, такое решение может показаться достаточно очевидным. Действительно вначале мы осуществляем процесс сбора и подготовки данных, затем их передачу тому модулю, который должен произвести над ними некоторые преобра-

зования. Особенностью здесь является то, что заранее известно о существовании f3, а следовательно, о структуре входных данных: типах переменных и особенностях их вызова. Но что будет, если в нашем распоряжении окажется модифицированный _f3, зависящий от других входных данных? Ясно, что в такой ситуации, согласно обратному ходу, придется снова заранее исполнить все требуемые модули и передать их на уровень выше, но теперь это уже могут быть совсем другие программные единицы. Таким образом, получается, что для того, чтобы заменить f3 на _f3, потребуется также менять их окружение. В результате при модификации программа будет подвергаться значительным изменениям, которые однажды могут привести ее к неработоспособности. Следовательно, нарушится один из базовых принципов «правильной модификации» - безболезненность.

Каким могло бы быть решение поставленной задачи? Оказывается, что достаточно определить необходимые структуры заранее: реализовать так называемый банк данных, в который наряду с заданными входными данными будут также включаться получающиеся от исполнения существующих в системе модулей, способные доопределить неполный исходный набор. То есть достаточно потребовать, чтобы связанные модули работали с единой структурой, в которой входные и выходные данные неотличимы. Если предположить, что некоторый модуль f3 заменяется модулем _f3 (где _f3 может зависеть, вообще говоря, от других параметров, нежели f3), то для решения задачи необходимо расширить входные данные (но не заменить их!) путем подключения к системе недостающих модулей, способных сгенерировать на основе существующих требуемые параметры, переменные и пр. Подобным образом реализуется полный набор данных для всех связанных модулей, работающих над ним. В случае, если текущая функция нас более не устраивает, мы можем безболезненно для окружения заменить ее новой. Такой подход позволяет не менять всю сложившуюся структуру взаимодействия модулей, ведь могут существовать такие, которые используют существующие f1(a) и f2(b,c) в их исходном варианте. Как было установлено, такое взаимодействие модулей удобно реализуется через их параметризацию с помощью языка разметки, например, XML. Действительно XML позволяет моделировать сколь угодно сложные структуры благодаря возможности декларативно, с высокой долей наглядности описывать вложенность и взаимодействие, вообще говоря, произвольных составляющих. XML - это самоописывающий язык: для описания используются контейнеры (элементы или атрибуты), чье использование подчинено строгим правилам. Наш интерес к XML применительно к рассматриваемой технологии обусловливается, прежде всего, четырьмя причинами: XML не зависит от платформы

(по состоянию на 2004 г. об его полной поддержке заявили IBM, Microsoft, Oracle, Sybase и др.). Здесь основным преимуществом выступает переносимость - возможность обмена между инструментами проектирования и разработки [3]; XML позволяет достичь повторного использования на недостижимом ранее уровне: широкомасштабном (broad scale), повторном использовании, когда созданное однажды применяется повторно, причем даже и для других целей или вне рамок контекста, который задумывался при создании [3]; XML чрезвычайно гибок: его структура может разрабатываться в расчете на оперативное сокращение или расширение. Другими словами, из всего объема данных используется ровно столько, сколько необходимо рассматриваемой стороне [4]; XML расширяем: его структуры и схемы могут быть расширены сами или добавлены для расширения другого.

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

Определение 3. Метод изменения содержимого программного фонда называется безболезненным для окружения, если его применение не влечет за собой необходимости изменения существующих ранее текстов программ [2].

Основное отличие нового решения от первого -интуитивного - состоит в достижении безболезненности для окружения, которое появляется вследствие отсутствия требований модификации модулей f1, f2. Предлагается создавать и включать в существующий набор новые программные единицы f 1', f2', быть может, связанные с f1, f2. Таким образом, в случае существования неких модулей fk(f1,f2), ke N, их работоспособность не нарушается ввиду отсутствия соответствующих модификаций, тогда как при первом подходе были бы затронуты fk. Безболезненно для окружения выполняется и обратная операция, в этом случае она сводится к уничтожению одного из модулей f 1' или f2', что не влияет на работоспособность программы. Банк общих данных расширяется на основе данных от новых модулей, связанных с существующими. В свою очередь для реализации таких модулей банком данных предоставляются все имеющиеся у него данные. Другими словами, модуль теперь -это инструмент для выработки новых данных, применяемый в том случае, когда банку данных требуется пополнение.

Нетрудно предположить, что алгоритмы работы модулей f1 и f1', а также f2 и f2' могут быть схожими, ведь часто они являются несложной модификацией друг друга. В рамках предлагаемого подхода, например, можно реализовать копирование модулей и, затем, осуществить модификацию «клона», оставив не-

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

Более рациональным решением было бы реализовать модуль П' (или 12') таким образом, чтобы он был связан с модулем й(или 12) через некую структуру данных. Создается «надстройка» одного модуля над другим, которая не влияет на существующие программные единицы, а сама пользуется данными, получаемыми от них. Таким образом, образуется межмодульное взаимодействие вида _13 ^ 12'^ 12. Отличительной чертой здесь является тот факт, что предложенная связь неединственная. Так, например, представляется возможным и появление следующей цепочки вызовов: _13 ^ 13 ^ 12. Выбирать следует тот способ, который требует минимальных усилий со стороны разработчика на написание кода модуля. В нашем случае необходимо проанализировать, будет ли проще реализовать связь _13 ^ 12' или _13 ^ 13, при этом рационально пользоваться в максимальной степени данными банка данных и модулями, которые могут его пополнить.

Принимая во внимание обозначения, принятые в определениях 1, 2, сформулируем:

Определение 4. Рассмотрим такой модуль И : К '(с г) ^ г', что 42 е К' 32' е г': И(2) = 2'. То

есть И^(/(х))) = 2' или х ^ / ^ g ^ И ^ 2'. Продолжая данный процесс ввода новых модулей, можно построить конструкцию вида

х ^ /1 ^ /2 ^... ^ /„ ^ х',

где х, х' - входные и выходные данные. Приведенный алгоритм назовем алгоритмом построения цепочки, а структуру (1) - цепочкой или цепочечной.

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

Модифицированный цепочечный подход

Целью такого подхода выступает минимизация затрат на разработку кода: если раньше добавление функциональности описывалось модулями относи-

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

Предлагается вариант модификации цепочечного подхода, одна из идей которого - перенесение связей между модулями к компонентам и их спецификациям.

Определение 5. Спецификация - это полное и однозначное формализованное описание на языке, отличном от языка реализации.

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

Модуль 1 Модуль 2 Модуль 3

J

Спецификация 1 Спецификация 2 Спецификация 3

Компонент1

Компонент 2

Рис. 2

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

Приведенная схема построения программы является синтезом цепочечного и каркасного подхода с привлечением новых методов. Данные теперь стали менее пассивны в том смысле, что они позволяют определить требования к написанию недостающих функций на основе их входного и выходного потоков. С одной стороны, соблюдается один из основных принципов изоляции и скрытости кода, так называемый принцип «черного ящика» [5], а с другой - вынос необходимых параметров за пределы программной единицы. Такие параметры лежат в основе «протокола общения» между различными составляющими программы. В отличие от принятого понимания интерфейса рассматриваемые параметры сами являются элементами кода, зачастую описанными и генерируемыми по спецификациям. Предлагаемый подход бо-

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

Определение 6. Однородность - это общее свойство множества информационных объектов, относительно которого можно определить операции, применимые ко всему этому множеству.

Определение 7. Однородное пространство -совокупность однородных элементов и некоторого базового набора подпрограмм, работающих с этой совокупностью элементов универсальным образом (единообразно).

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

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

Компонент 3

завязанную на единственное описание. Ее изменение одновременно влечет изменения во всех использующих ее системах.

В HTML каждая ссылка определяется стандартным html тегом <А> с атрибутом href. В нем указывается URL адрес страницы; в теле тега задается текст, который будет выведен при просмотре WWW приложения в браузере. Пусть пустая html страница задается следующим образом: <HTML>

<HEAD>

<TITLE>Example</TITLE>

</HEAD> <BODY>

<!~!{LinksPoint} ~> </BODY> </HTML>

Спецификация, описывающая ссылки, имеет вид:

<?xml version=«1.0»>

<LINKLIST>

<!--!{(LinksPoint,1)} -->

<REF href=«1.html»>The first line</REF>

<REF href=«2.html»>The second line</REF>

</LINKLIST>

Аналогично тегу <A> каждый тег <REF> задает ссылку с адресом, указанным в атрибуте «href» и текстом, прописанным в его контейнере. Совокупность таких тегов заключается в корневой тег <LINKLIST>, что требуется правилами написания XML текста. Комментарий <!--!{ (LinksPoint, 1)} --> служит указанием места вставки для программы, осуществляющей данную модификацию.

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

<PLUGIN>mod0.pt</PLUGIN>

<ADD>

<REF href=«html.html»>New Line</REF>

</ADD>

</ACTIONS>

Структура спецификации второго уровня складывается из двух независимых частей: контейнера <PLUGIN> и <ADD>. Первый необходим для привязки к спецификации первого уровня. В контейнере

Рис. 3

может содержаться имя файла, в котором находится XML документ. Второй контейнер <ADD> необходим для указания модулю разбора ссылки и ее параметров для добавления в исходную спецификацию. Подобным образом возникают цепочки спецификаций (рис. 3).

По цепочке вначале будет получена спецификация:

<?xml version=«1.0»>

<LINKLIST>

<!--!{ (LinksPoint, 1)} -->

<REF href=«1.html»>The first line</REF>

<REF href=«2.html»>The second line</REF>

<REF href=«html.html»>New Line</REF>

</LINKLIST>

А затем и сама страница:

<HTML>

<HEAD>

<TITLE>Example</TITLE>

</HEAD> <BODY>

<!--!{LinksPoint} --> <A href=«1.html»>The first line</A> <A href=«2.html»>The second line</A> <A href=«html.html»>New Line</A> </BODY> </HTML>

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

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

Заключение

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

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

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

Ростовский государственный университет, Южно-Российский региональный центр информатизации

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

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

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

Литература

1. Модульное программирование // http://www.stu.ru/inform/glaves2/glava18/gl_18_2.html

2. Горбунов-Посадов М.М. Расширяемые программы. М., 1999 // http://www.keldysh.ru/gorbunov/

3. БинДж. XML для проектировщиков. М., 2004.

4. Агафонов В.Н. // Требования и спецификации в разработке программ. М., 1984. С. 285.

5. Hursch W., Lopes Cr. Separation of Concerns // College of Computer Science, Northeastern University, Boston, 1995. MA 02115, USA.

28 февраля 2005 г.