Класс удаленного прокси — это не (очень) больно
(Динамическая диспетчеризация спешит на помощь)
После нескольких статей про MapReduce нам показалось необходимым еще раз отойти в сторону и поговорить про инфраструктуру, которая поможет облегчить построение решения MapReduce. Мы, по-прежнему, говорим про InterSystems Caché, и, по-прежнему, пытаемся построить MapReduce систему на базе имеющихся в системе подручных материалов.
На определенном этапе написания системы, типа MapReduce, встает задача удобного вызова удаленных методов и процедур (например, посылка управляющих сообщений с контроллера на сторону управляемых узлов). В среде Caché есть несколько простых, но не очень удобных методов достичь этой цели, тогда как хочется бы получить именно удобный.
Хочется взять простой и последовательный код, тут и там вызывающий методы объекта или класса, и волшебным мановением руки сделать его работающим уже с удаленными методами. Конечно же, степень «удаленности» может быть различной, мы, например, можем просто вызывать методы в другом процессе того же самого узла, суть от этого сильно не поменяется — нам нужно получить удобный способ маршаллизации вызовов «на ту сторону» вовне текущего процесса, работающего в текущей области.
После нескольких фальстартов автор вдруг осознал, что в Caché ObjectScript есть очень простой механизм, который позволит скрыть все низкоуровневые детали под удобной, высокоуровневой оболочкой — это механизм динамической диспетчеризации методов и свойств.
Если оглянуться (далеко) назад, то можно увидеть, что начиная с Caché 5.2 (а это на минуточку с 2007 года) в базовом классе %RegisteredObject
есть несколько предопределенных методов, наследуемых каждым объектом в системе, которые вызываются при попытке вызова неизвестного во время компиляции метода или свойства (в настоящий момент эти методы переехали в интерфейс %Library.SystemBase
, но это сильно не поменяло сути) .
Имя | Значение |
---|---|
Method %DispatchMethod (Method As %String, Args...) |
Вызов неизвестного метода или доступ к неизвестному многомерному свойству (их синтаксис идентичен) |
ClassMethod %DispatchClassMethod (Class As %String, Method As %String, Args...) |
Вызов неизвестного метода класса для заданного класса |
Method %DispatchGetProperty (Property As %String) |
Чтение неизвестного свойства |
Method %DispatchSetProperty (Property As %String, Val) |
Запись в неизвестное свойство |
Method %DispatchSetMultidimProperty (Property As %String, Val, Subs...) |
Запись в неизвестное многомерное свойство (не используется в данном случае, будет частью другой истории) |
Method %DispatchGetModified (Property As %String) |
Доступ к флагу «modified» («изменен») для неизвестного свойства (также, не используется в данной истории) |
Method %DispatchSetModified (Property As %String, Val) |
Дополнение к методу выше — запись в флаг «modified» («изменен») для неизвестного свойства (не используется в данной истории) |
Для простоты эксперимента, мы будем использовать только функции, отвечающие за вызов неизвестных методов и скалярных свойств. В продуктовой среде вам на определенном этапе может понадобиться переопределить все или большинство описанных методов, т.ч. будьте бдительны.
Сначала попроще — протоколирующий объект-прокси
Напомним, что ещё со времен «царя Гороха» в стандартной библиотеке CACHELIB были стандартные методы и классы для работы с проекцией JavaScript объектов в XEN — %ZEN.proxyObject
, он позволял манипулировать динамическими свойствами даже во времена, когда еще не было работ по документной базе DocumentDB (не спрашивайте) и тем более не было нативной поддержки JSON объектов в ядре среды Caché.
Давайте, для затравки, попытаемся создать простой, протоколирующий все вызовы, прокси объект? Где мы обернем все вызовы через динамическую диспетчеризацию с сохранением протокола о каждом произошедшем событии. [Очень похоже на технику mocking в других языковых средах.]
[[Как это переводить на русский? «мОкать»?]]
В качестве примера возьмем сильно упрощенный класс Sample.SimplePerson
(по странному стечению обстоятельств очень похожего на Sample.Person
из области SAMPLES в стандартной поставке: wink:)
DEVLATEST:15:23:32:MAPREDUCE>set p = ##class(Sample.SimplePerson).%OpenId(2)
DEVLATEST:15:23:34:MAPREDUCE>zw p
p=
Т.е. имеем персистентный класс — с 3-мя простыми свойствами: Age, Contacts и Name. Обернем доступ ко всем свойствам этого класса и вызов всех его методов в своем классе Sample.LoggingProxy
, и каждый такой вызов или доступ к свойству будем протоколировать… куда-нибудь.
/// Простой протоколирующий прокси объект:
Class Sample.LoggingProxy Extends %RegisteredObject
{
/// Кладем лог доступа в глобал
Parameter LoggingGlobal As %String = "^Sample.LoggingProxy";
/// Храним ссылку на открытый объект
Property OpenedObject As %RegisteredObject;
/// просто сохраняем строку как следующий узел в глобале
ClassMethod Log(Value As %String)
{
#dim gloRef = ..#LoggingGlobal
set @gloRef@($sequence(@gloRef)) = Value
}
/// Более удобный метод с префиксом и аргументами
ClassMethod LogArgs(prefix As %String, args...)
{
#dim S as %String = $get(prefix) _ ": " _ $get(args(1))
#dim i as %Integer
for i=2:1:$get(args) {
set S = S_","_args(i)
}
do ..Log(S)
}
/// открыть экземпляр другого класса с заданным %ID
ClassMethod %CreateInstance(className As %String, %ID As %String) As Sample.LoggingProxy
{
#dim wrapper = ..%New()
set wrapper.OpenedObject = $classmethod(className, "%OpenId", %ID)
return wrapper
}
/// запротоколировать переданные аргументы и передать управление через прокси ссылку
Method %DispatchMethod(methodName As %String, args...)
{
do ..LogArgs(methodName, args...)
return $method(..OpenedObject, methodName, args...)
}
/// запротоколировать переданные аргументы и прочитать свойство через прокси ссылку
Method %DispatchGetProperty(Property As %String)
{
#dim Value as %String = $property(..OpenedObject, Property)
do ..LogArgs(Property, Value)
return Value
}
/// запротоколировать переданные аргументы и записать свойство через прокси ссылку
/// log arguments and then dispatch dynamically property access to the proxy object
Method %DispatchSetProperty(Property, Value As %String)
{
do ..LogArgs(Property, Value)
set $property(..OpenedObject, Property) = Value
}
}
Параметр класса
#LoggingGlobal
задаёт имя глобала, где будем хранить лог (в данном случае в глобале с именем^Sample.LogginGlobal
);Есть два простых метода
Log(Arg)
иLogArgs(prefix, args...)
которые пишут протокол в глобал, заданный свойством выше;%DispatchMethod
,%DispatchGetProperty
и%DispatchSetProperty
обрабатывают соответствующие сценарии с вызовами неизвестного метода или обращения к свойству. Они протоколируют черезLogArgs
каждый случай обращения, а затем напрямую вызывают метод или свойство объекта из ссылки..%OpenedObject
;- Также там задан метод «фабрики класса»
%CreateInstance
, который открывает экземпляр заданного класса по его идентификатору%ID
. Созданный объект «оборачивается» в объектSample.LogginProxy
, ссылка на которого и возвращается из этого метода класса.Никакого шаманства, ничего особенного, но уже в этих 70 строках Caché ObjectScript мы попытались показать шаблон вызова метода/свойства с побочным эффектом (более полезный пример такого шаблона будет показан ниже).