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.