3.5. Модификации обращения к процедуре
Предшествующие разделы были посвящены цепочечному подходу к построению пакета программ. Главный его конкурент каркасный подход, имеющий не меньшие заслуги в области обеспечения безболезненности развития и охватывающий существенно более широкий класс предметных областей. Но о каркасном подходе речь еще впереди. А пока разберем два более известных подхода вызов по образцу и обмен сообщениями, появившихся вне всякой связи с пакетной проблематикой и тем не менее также способных в определенной мере облегчить подключение новых модулей к программному фонду пакета.
Наш интерес к этим подходам мотивируется следующими рассуждениями. Что обычно мешает безболезненно подключить к существующей программе новый модуль, оформленный в виде процедуры? Ответ на вопрос, поставленный таким образом, достаточно очевиден. Для того чтобы процедура когда-либо выполнилась, где-то в программе должно появиться обращение к ней. Но для включения обращения потребуется, вообще говоря, отредактировать текст существующей программы и тем самым нарушить искомую безболезненность подключения модуля.
Причина неудачи коренится в жесткости связи, устанавливаемой в традиционной операционной среде между процедурой и точкой обращения к ней: здесь обычно требуется, чтобы в тексте каждого из этих двух взаимодействующих объектов было явно записано имя процедуры. Оба рассматриваемых в данном разделе подхода предлагают, каждый по-своему, ослабить эту жесткую связь, освободив обращение к процедуре от явного упоминания ее имени. Оказывается, что такая модификация обращения способствует достижению интересующей нас безболезненности подключения к пакету новых модулей-процедур.
3.5.1. Вызов по образцу. В ранних работах по искусственному интеллекту (в задачах планирования перемещений робота, доказательства теорем, понимания естественного языка и др.) можно встретить несколько необычную схему обращения к процедуре вызов по образцу.
Вызов по образцу основывается на использовании специальных языковых конструкций, записываемых как в точке вызова процедуры, так и в ее заголовке. В точке вызова фигурирует не имя процедуры, а ее образец, характеризующий результат, которого ждут от выполнения процедуры. В заголовке процедуры, вызываемой по образцу, специфицируется решаемая ею задача. Вызывающая программа находит вызываемую процедуру путем просмотра спецификаций имеющихся процедур и сопоставления их с образцом, т. е. посредством ассоциативного поиска.
Допустимой считается ситуация, когда найденная таким образом процедура выполнилась, но не справилась с поставленной перед ней задачей. В этом случае осуществляется откат (backtracking) к состоянию программы, в котором она находилась непосредственно перед вызовом несправившейся процедуры, вслед за чем может быть вызвана другая процедура, отвечающая данному образцу, и т. д. Если неудача постигает все отвечающие образцу процедуры, то несправившейся признается процедура, инициировавшая данный вызов по образцу, и происходит откат к объемлющему вызову.
Ассоциативный механизм и здесь (как и при автоматическом составлении цепочек см. разд. 3.3, 3.4) приводит к тому, что подключение к пакету вновь написанной процедуры не отвечает требованию безболезненности для работоспособности. Если новая процедура снабжена спецификацией, функционально эквивалентной уже имеющимся, то при включении этой процедуры в пакет к ней может неожиданно обратиться задача, успешно решавшаяся ранее без ее участия. Поэтому если новая процедура недостаточно оттестирована и содержит ошибки, то при ее подключении некоторые отлаженные ранее программы могут, вообще говоря, утратить работоспособность.
Вместе с тем процедура, вызываемая по образцу (опять же, подобно модулю при автоматическом планировании), во многих отношениях близка к идеалу безболезненности для окружения. Ее появление в пакете не влечет за собой никаких изменений ни в вызывающей, ни в соседних процедурах. Несмотря на это, с каждой новой процедурой автоматически, без каких-либо организационных усилий увеличивается мощность пакета: некоторые из задач, ставившихся ранее, но не решенных старым составом пакета, теперь могут быть решены за счет участия вновь подключенной процедуры.
В современных системах искусственного интеллекта (в частности, в экспертных системах) также широко используются поиск и вызов по образцу. Однако объектами поиска и вызова тут обычно служат не привычные программистскому взгляду процедуры, а правила-продукции вида «ЕСЛИ ... ТО ...». С помощью правил-продукций удается записать множество разнообразных знаний о реальном мире, а вызов по образцу дает эффективный механизм использования таких знаний. Из-за отмеченных достоинств, а также благодаря ряду получивших широкую известность приложений правила-продукции завоевали в своем классе задач довольно устойчивые позиции.
Энергичное развитие данной ветви искусственного интеллекта подводит нас к следующему весьма заманчивому умозаключению. Если вызываемые по образцу правила-продукции отлично зарекомендовали себя в качестве средства накопления произвольных знаний, то почему бы их аналогу специфицированным процедурам, вызываемым по образцу, не превратиться со временем в основной способ оформления многократно используемых программистских знаний? К сожалению, в практическом программировании данное умозаключение весомых подтверждений пока не находит: область применения вызова по образцу разочаровывает своей ограниченностью.
Вызов по образцу неплохо работает лишь в некоторых специфических предметных областях, где узловой проблемой, требующей наибольших усилий, является поиск некоторой комбинации существующих модулей, реализующей искомый алгоритм. За пределами этого класса задач обращения к вызову по образцу встречаются крайне редко. Одна из причин непопулярности данной схемы кроется в том, что подавляющее большинство практических программистов имеет дело с предметными областями, где «скелет» алгоритма достаточно очевиден, и поэтому там нет необходимости подключать дополнительные нетрадиционные механизмы, сопровождающие вызов по образцу.
Другая причина непопулярности, как и в случае цепочечного подхода, заключается в слабости реально доступного конфигурационного ориентира. В узкой трактовке вызов по образцу ведет к чисто иерархическим модульным структурам, но уже и здесь подключается весьма непростой формально-логический аппарат. Если же сориентироваться на более продуктивные конфигурации, то потребуются логические конструкции, сложность которых совершенно неприемлема для нужд повседневного программирования.
Вызов по образцу применяется, как правило, только в работах по искусственному интеллекту. Искусственный интеллект давно обособился в самостоятельную ветвь программирования, живущую по своим законам и лишь время от времени обменивающуюся с остальным миром интересными находками. (Даже термин «пакет программ» в данном контексте выглядит не совсем уместным, поскольку в искусственном интеллекте он употребляется довольно редко.) Однако и в массовом программировании существует родственный вызову по образцу подход, который также вправе претендовать на обеспечение безболезненного развития пакета. Этот подход обмен сообщениями.
3.5.2. Обмен сообщениями. Во многих программных средах (Smalltalk [Фути, 1988], Windows, сетевые протоколы и др.) взаимодействие частей программы опирается на обмен сообщениями между ними. В частности, нередко допускается обращение к процедуре не с помощью непосредственного вызова, а путем передачи ей определенного сообщения. Прием этого сообщения служит сигналом к выполнению процедуры. Завершаясь, процедура посылает в обратном направлении итоговое сообщение.
Сообщение может адресоваться либо конкретному получателю, либо некоторому их классу, либо всем, кто способен его принять. Для нас интересны два последних, широковещательных варианта рассылки, поскольку они иногда вплотную соприкасаются с обеспечением безболезненного развития.
В самом деле, пусть имеется некоторый коллектив модулей, обменивающихся сообщениями. И пусть требуется подключить новый модуль, скажем, подсчитывающий число посланных широковещательных сообщений определенного сорта. Подключение такого модуля удается провести безболезненно, не редактируя тексты написанных ранее программ. Новый модуль просто встанет в один ряд с уже имевшимися потребителями этих сообщений и наравне с его предшественниками будет обслуживаться средствами системной поддержки обмена сообщениями. Все, что потребуется от старого коллектива модулей, продолжать как ни в чем не бывало посылать те же сообщения, что и ранее.
Небольшое осложнение с безболезненностью возникает только при выяснении вопроса о том, каким образом компоновщик догадается включить в собираемую программу вновь появившийся модуль. Если операционная среда требует, чтобы разработчик явно перечислял все входящие в программу модули, то безболезненное подключение невозможно, ибо придется редактировать первичный объект список модулей, участвующих в сборке.
Но тут спасает уже известный нам (см. разд. 3.4.2) ассоциативный механизм: оснащенный им компоновщик позволит включать в программу не только явно перечисленные модули, но и, например, все модули, имеющие заданный атрибут. Тогда при записи в программный фонд нового модуля достаточно будет снабдить его этим атрибутом, и искомая компоновка пройдет безболезненно (точнее, безболезненно для окружения см. разд. 3.4).
Новый модуль способен довольно-таки энергично воздействовать на внешний мир. Он может периодически записывать в файл или выводить на принтер накапливаемые результаты. Он имеет также полное право при инициализации заказать себе окно, послав обусловленное сообщение модулю, ведающему пространством экрана. В результате у него появится возможность непрерывно отображать свое состояние на дисплее и, более того, завязать в своем окне диалог с пользователем. И все эти затеи удается реализовать, не выходя за рамки безболезненного подключения модуля!
В приведенном примере лавры организатора безболезненного подключения делят между собой аппарат обмена сообщениями и ассоциативный компоновщик. Только их совместными усилиями удалось решить такую непростую на первый взгляд программистскую задачу. Попытаемся теперь определить, чей вклад был более весомым.
Ассоциативная сборка является одним из столпов, на которых держится здание расширяемого программирования. Мы уже встречались (см., например, разд. 2.2.3) и еще не раз встретимся с ситуациями, где только с ее помощью удается обеспечить безболезненность развития. В нашем же примере, если не прибегать к ассоциативной сборке, придется считать, что все поступающие в программный фонд модули включаются в формируемую выполняемую программу. С технологической точки зрения такая сборка практически непригодна: в частности, здесь не удалось бы организовать эффективное оформление варианта (см. разд. 1.8).
Иначе обстоит дело с обменом сообщениями. Незаменимость данного механизма вызывает серьезные сомнения. Приведенный пример можно, вообще говоря, спроецировать на более традиционную среду, где нет обмена сообщениями, а есть только обычные вызовы процедур. Для каждого класса сообщений пишется соответствующая процедура, и посылки сообщений этого класса повсюду заменяются обращениями к ней. Выполнение процедуры заключается в последовательном или параллельном вызове всех модулей, объявивших себя потребителями этих сообщений. Кроме того, разумеется, процедура осуществляет транзитную пересылку содержания сообщения от отправителя к получателям, для чего применяется привычный механизм передачи параметров.
Таким образом, в модифицированном примере передача сообщений не используется. Тем не менее и здесь возможно безболезненное подключение нового модуля потребителя сообщений. Для этого при подключении к транзитной процедуре модулей-потребителей следует применить, как и ранее, не перечислительный, а ассоциативный механизм. Тогда при размещении нового модуля в программном фонде нужно будет (как и при использовании обмена сообщениями) снабдить его атрибутом «потребитель сообщений данного класса», и далее ассоциативные средства автоматически включат его в собираемую программу.
Но просто включить модуль в программу тут недостаточно. Надо еще каким-то образом передать ему управление. В рассмотренном примере эта проблема была с успехом перепоручена средствам системной поддержки обмена сообщениями. Теперь же требуется пополнить имеющийся ряд модулей-потребителей, вызываемых транзитной процедурой, т. е. вписать в текст транзитной процедуры оператор вызова нового модуля. В традиционной среде это обернулось бы полным провалом: пришлось бы редактировать транзитную процедуру, потеряв тем самым искомую безболезненность подключения модуля.
Если же среда располагает ассоциативными средствами, то безболезненное подключение оператора вызова становится совершенно обыденным делом. В том месте текста, где должны помещаться вызовы модулей-потребителей, записывается специальная статическая конструкция. При сборке программы эта конструкция расширяется в ряд операторов вызова для всех модулей программного фонда, снабженных указанным атрибутом. (В гл. 5 такая сборка рассматривается более подробно.)
Итак, при ближайшем рассмотрении выяснилось, что в приведенном примере основные заслуги в обеспечении безболезненности принадлежат вовсе не обмену сообщениями, а ассоциативному механизму сборки.
Заступаясь за обмен сообщениями, кто-то может отметить, что данный подход выглядит элегантнее, особенно при распределенных или многопроцессорных вычислениях. Но ему сразу же вполне резонно возразят, что все-таки это дело вкуса. Дискуссия такого рода выходит за круг проблем, охватываемых книгой. Тем не менее, поскольку обмен сообщениями вызывает у современных программистов повышенный интерес, уделим ему еще немного внимания.
Обсуждая обмен сообщениями, нельзя не упомянуть о том, что данная схема взаимодействия получила широкое признание во многом благодаря охватившему массы увлечению объектно-ориентированным стилем программирования. Первые проповедники объектной ориентации решительно настаивали на полном изгнании традиционного вызова процедуры, предлагая повсюду заменять его обменом сообщениями. Однако со временем выяснилось, что для объектно-ориентированного стиля обмен сообщениями вовсе не обязателен, и традиционному вызову, по-видимому, суждена еще долгая жизнь.
Модуляризация при объектной ориентации схожа с модуляризацией в пакете программ. В обоих подходах конфигурационным ориентиром является коллектив достаточно самостоятельных модулей, что позволяет надеяться на последующее безболезненное развитие, облекаемое в форму пополнения коллектива новыми модулями. Но если для пакета программ возможность безболезненного развития один из важнейших, а нередко и наиболее важный показатель, то в объектно-ориентированном программировании этой стороне дела внимания уделяется меньше. Для пакета более актуален применяемый механизм сборки программы, а для объектной ориентации механизмы взаимодействия модулей, в частности обмен сообщениями.
Из сферы внимания объектно-ориентированного стиля нередко выпадают некоторые важные технологические аспекты разрабатываемой программы. Например, там далеко не всегда находится место для оформления такого весьма интересного и полезного модуля, как «все выдаваемые программой тексты на естественном языке». Этот модуль ни с кем никакими сообщениями не обменивается (поскольку вообще не может быть выполнен), но оказывается незаменимым при составлении инструкций или при переносе программы в страну, говорящую на другом языке.
Как уже не раз отмечалось, объектно-ориентированное программирование имеет множество несомненных заслуг, однако было бы ошибкой воспринимать этот стиль как волшебную палочку, чудесным образом превращающую программу из консервативной в расширяемую. Кроме того, многие конфигурационные решения оказывается существенно удобнее анализировать изолированно, абстрагируясь от относительно мелких особенностей того или иного стиля программирования, подобных только что рассмотренному обмену сообщениями. Этими двумя причинами объясняется относительно скромное место, отведенное объектной ориентации на страницах книги.
Так или иначе, оба приведенных выше подхода вызов по образцу и обмен сообщениями по разным причинам не могут играть заметных ролей при построении пакетов программ. Поэтому, как уже отмечалось, основная борьба за право конфигурационного обслуживания пакетов разворачивается между уже знакомым нам цепочечным подходом (см. разд. 3.2, 3.3) и каркасным подходом, о котором речь впервые пойдет в следующем разделе.
|