3. Представление

Вид материалаОбзор

Содержание


7.2. Языки LOOPS и FLAVORS
7.2.1. Передача сообщений
7.2. Формирование объекта класса на языке CLIPS
defclass ship (is-a INITIAL_OBJECT) (slot x-velocity (create-accessor read-write)) (slot y-velocity (create-accessor read-write)
definstances ships (titanic of ship (x-velocity 12) (y-velocity (10)
defmessage-handler ship speed () (sqrt ( + { ?self:x-velocity ?self:x-velocity) ( ?self:y-velocity ?self:y-velocity))) )
7.2.2. Проблема наложения методов
Рис. 7.2. Иерархическая система классов окон
найти подходящие методы в базовых классах; скомбинировать их таким образом, чтобы получить желаемый эффект.
Рис. 7.4. Структура классов в языке LOOPS
Рис. 7.5. Вставка метакласса, определенного пользователем
Подобный материал:
1   ...   21   22   23   24   25   26   27   28   ...   110
^

7.2. Языки LOOPS и FLAVORS

Объектно-ориентированный стиль программирования идеально подходит для решения проблем, требующих детального представления объектов реального мира и динамических отношений между ними. Классическим примером применения данного подхода являются задачи моделирования. В таких программах компоненты сложной системы представляются структурами, инкапсулирующими и данные, и функции, моделирующие поведение соответствующих компонентов. Первым языком, в котором была реализована такая идея, стал SmallTalk [Goldberg andRobson, 1983].

Для задач искусственного интеллекта были разработаны языки LOOPS и FLAVORS, причем оба представляли собой объектно-ориентированные расширения языка LISP. Хотя в настоящее время эти языки практически не используются, реализованные в них базовые идеи унаследованы множеством языков представления знаний, появившихся позже. В частности, это можно сказать о языках CLOS (Common LISP Object System) и CLIPS. Ниже мы кратко опишем основные функциональные возможности языков LOOPS и FLAVORS и обратим ваше внимание на некоторые сложности, связанные с реализацией объектно-ориентированного стиля программирования.

^

7.2.1. Передача сообщений

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

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

Предположим, мы определили объект, представляющий класс ship (корабль), и наделили его свойствами x-velocity (скорость по х) и y-velocity (скорость по у). Теперь можно создать экземпляр класса ship, назвать его Titanic и одновременно присвоить свойствам x-velocity и y-velocity нового экземпляра исходные значения. Практически нет никаких отличий между этой процедурой и процедурой создания нового экземпляра фрейма, рассмотренной в предыдущей главе.

Предположим теперь, что нам понадобилось определить процедуру speed, которая будет вычислять скорость судна на основании значений свойств x-velocity и у-velocity (скорость вычисляется как корень квадратный из суммы квадратов компонентов). Такая процедура будет принадлежать абстрактному типу данных, представляющему любые суда (в терминологии языка SmallTalk speed — это метод класса ships, а в терминологии C++ — функция-член класса ships.)

Идея состоит в том, чтобы закодировать в объекте (классе) не только декларативные знания о судах, но и процедурные, т.е. методы использования декларативных знаний. Для того чтобы активизировать процедуру вычисления скорости определенного судна, в частности "Титаника", нужно передать объекту Titanic сообщение, которое побудит его обратиться к ассоциированной процедуре в контексте данных о компонентах скорости именно этого объекта. Titanic — это экземпляр класса ships, от которого он унаследовал процедуру speed. Все это представляется довольно очевидным, но описанный механизм срабатывает только в случае, если соблюдаются следующие соглашения.

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

Для того чтобы определить компоненту X текущего положения "Титаника", программа должна послать запрос объекту Titanic, который имел бы следующий смысл: "передай текущее значение координаты X". Как в объекте формируется это значение или как оно хранится — дело только самого объекта и никого более не касается. Ни объекты других классов, ни какие-либо другие компоненты программы этого не знают. Более того, внутренний механизм доступа к этой информации должен быть скрыт, чтобы никто не мог добраться к ней, минуя сам объект. Это соглашение принято называть инкапсуляцией.

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

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

^

7.2. Формирование объекта класса на языке CLIPS

Ниже показано, как на языке CLIPS определяется класс ship и формируется экземпляр этого класса titanic. Сначала определим класс ship, в котором имеются два слота: x-velocity и y-velocity:

^

(defclass ship

(is-a INITIAL_OBJECT)

(slot x-velocity (create-accessor read-write))

(slot y-velocity (create-accessor read-write)) )

Теперь сформируем экземпляр этого класса,, которому будет дано наименование "Titanic". Проще всего это сделать с помощью функции definstaces, которая в качестве аргументов принимает список параметров формируемых экземпляров. Определенные таким способом экземпляры класса будут инициализироваться при каждом перезапуске интерпретатора CLIPS.

^

(definstances ships (titanic of ship

(x-velocity 12) (y-velocity (10)

Завершается определение созданием обработчика событий для класса ship. Все экземпляры класса будут использовать этот обработчик для вычисления собственной скорости. Обратите внимание на то, что член в этом определении ссылается на значение слота того экземпляра класса, скорость которого требуется вычислить.

^

(defmessage-handler ship speed () (sqrt

( +

{ ?self:x-velocity ?self:x-velocity)

( ?self:y-velocity ?self:y-velocity)))

)

Если файл со всеми представленными выше выражениями загрузить в среду CLIPS, а затем ввести с клавиатуры (send [titanic] speed), то в ответ интерпретатор CLIPS выведет скорость объекта titanic

^

7.2.2. Проблема наложения методов

Та простая картина, которая вырисовывается из представленного выше механизма прямого наследования, несколько усложняется, если мы попытаемся заменить прямое наследование множественным. В главе 6 уже отмечалось, что это может привести к неоднозначности в наследовании свойств. Но в контексте объектно-ориентированного подхода при множественном наследовании появляется и неоднозначность поведения.

С этой проблемой впервые столкнулись при разработке объектно-ориентированного языка FLAVORS, который поддерживает множественное наследование и наложение методов [Cannon, 1982]. Язык FLAVORS позволяет объектам иметь несколько родителей и таким образом наследовать процедуры и данные из нескольких источников. Для FLAVORS характерна не иерархия объектов, а гетерархия. Если графически изобразить отношения между разными объектами в FLAVORS, то схема будет больше походить на решетку, чем на дерево. Каков во всем этом смысл? Рассмотрим следующий пример, взятый из статьи Кэннона.

Отображение окон на дисплее рабочей станции реализуется, как правило, с использованием объектно-ориентированного стиля программирования. Будем считать, что окна на экране дисплея представлены в виде LISP-объектов, в каждом из которых записаны свойства окна (размеры и положение на поле экрана) и процедуры работы с окном (открытие, закрытие, перерисовка и т.п.). Существует несколько разновидностей окон и соответственно объектов окон — с рамкой, без рамки, со строкой заголовка, без заголовка и т.д.

Класс окно с рамкой .является подклассом (или производным классом) класса окно. Точно так же подклассом класса окно является и класс окно с заголовком. В иерархической системе классы окно с рамкой и окно с заголовком представляют собой отдельные узлы одного и того же уровня иерархии. Они наследуют определенные методы, например refresh (освежить), от базового класса окно, но имеют и собственные методы выполнения таких операций, как перерисовка рамки или строки заголовка.

А теперь предположим, что нам потребовался еще один вид окна — окно с рамкой и строкой заголовка. Окно такого типа должно быть представлено новым классом окно с рамкой и заголовком. В иерархической системе новый класс будет наследником класса окно и независимым "близким родственником" уже существующих классов окно с рамкой и окно с заголовком на том же уровне иерархии (рис. 7.2). Но даже интуитивно чувствуется, что такая организация избыточна. Ведь фактически мы стремимся "смешать" два набора уже существующих качеств и получить в результате новый комбинированный набор. Кажется, что целесообразнее сделать новый класс "дитятей" двух родителей, — классов окно с рамкой и окно с заголовком (рис. 7.3).

^

Рис. 7.2. Иерархическая система классов окон

Но здесь возникают вопросы: а как новый класс будет наследовать процедуры, определенные в двух базовых классах? Устроит ли нас "смешанное" поведение нового класса? Эту проблему можно разложить на две составляющие:

^

найти подходящие методы в базовых классах;

скомбинировать их таким образом, чтобы получить желаемый эффект.

Рис. 7.3. Гетерархическая система классов окон

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

7.2.3. Метаклассы

Отличительной чертой языка LOOPS является поддержка концепции метаклассов, т.е. классов, членами которых являются другие классы. Впервые метаклассы появились в языке SmallTalk. В первой системе реализации этого языка имелся единственный метакласс Class, членами которого были все прочие классы в системе, в том числе и Class. В более поздних реализациях SmallTalk метакласс формировался автоматически всякий раз, когда создавался новый класс, и этот класс становился экземпляром класса Metaclass. Метаклассы в SmallTalk-80 сами по себе не являлись экземплярами метаклассов, а принадлежали единственному метаклассу Metaclass. Во избежание путаницы мы в дальнейшем будем называть "классами объектов" те классы, которые не являются метаклассами.

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

В языке LOOPS метакласс не создается для каждого класса. Этот язык имеет более простую структуру классов, представленную схематически на рис. 7.4. Узлы в форме эллипсов представляют классы объектов и метаклассы, а узлы в форме прямоугольников представляют экземпляры классов объектов. Тонкие стрелки означают отношения вида является подклассом В", а толстые — отношения вида является экземпляром В".

^

Рис. 7.4. Структура классов в языке LOOPS

LOOPS поддерживает три стандартных метакласса: Object, Class и Metaclass. Тонкие стрелки на рис. 7.4 означают, что Object является суперклассом класса Class, a Class является суперклассом класса Metaclass. Кроме того, Object является членом Class, Class — членом Metaclass, a Metaclass — членом самого себя. Таким образом, оказывается, что Object является корнем иерархии классов (не имеет суперкласса), а Metaclass является корнем иерархии экземпляров (не имеет ни одного класса, кроме самого себя).

Узлы, вычерченные утолщенными линиями — ship (корабль) и liner (лайнер), — типичные классы, определенные пользователем. Обратите внимание на то, что все классы такого рода являются членами класса Class, от которого они наследуют свое поведение. Таким образом, для того чтобы сформировать экземпляр Titanic, нужно переслать сообщение new классу liner, который наследует метод new от класса Class. Поведение экземпляра Titanic, естественно, будет унаследовано от его класса— в данном случае liner.

Использование метаклассов позволяет запрограммировать поведение по умолчанию и определенные виды структур в объектно-ориентированной системе на самом высоком уровне. Экземпляры классов объектов, таких как liner, обычно формируются стандартным способом, но если для одного или семейства классов желательно использовать какой-то более специфический вариант, нужно включить между liner и Class определенный пользователем метакласс (например, metaliner), в котором и "прописать" желаемые модификации (рис. 7.5). Таким образом, структура классов в языке LOOPS позволяет в одной системе объединить мощность и гибкость представления объектов реального мира.

^

Рис. 7.5. Вставка метакласса, определенного пользователем