Класс удаленного прокси — это не (очень) больно

Fish Out Of Watermelon by Joan Pollak

(Динамическая диспетчеризация спешит на помощь)


После нескольких статей про 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=[1@Sample.SimplePerson]
+----------------- general information ---------------
|      oref value: 1
|      class name: Sample.SimplePerson
|           %%OID: $lb("2","Sample.SimplePerson")
| reference count: 2
+----------------- attribute values ------------------
|       %Concurrency = 1  
|                Age = 9
|           Contacts = 23
|               Name = "Waal,Nataliya Q."
+-----------------------------------------------------

Т.е. имеем персистентный класс — с 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
}

}

  1. Параметр класса #LoggingGlobal задаёт имя глобала, где будем хранить лог (в данном случае в глобале с именем ^Sample.LogginGlobal);


  2. Есть два простых метода Log(Arg) и LogArgs(prefix, args...) которые пишут протокол в глобал, заданный свойством выше;


  3. %DispatchMethod, %DispatchGetProperty и %DispatchSetProperty обрабатывают соответствующие сценарии с вызовами неизвестного метода или обращения к свойству. Они протоколируют через LogArgs каждый случай обращения, а затем напрямую вызывают метод или свойство объекта из ссылки ..%OpenedObject;


  4. Также там задан метод «фабрики класса» %CreateInstance, который открывает экземпляр заданного класса по его идентификатору %ID. Созданный объект «оборачивается» в объект Sample.LogginProxy, ссылка на которого и возвращается из этого метода класса.
    8c71316bb51547fc90ecd308eee28b21.png

    Никакого шаманства, ничего особенного, но уже в этих 70 строках Caché ObjectScript мы попытались показать шаблон вызова метода/свойства с побочным эффектом (более полезный пример такого шаблона будет показан ниже).


Комментарии (0)

© Habrahabr.ru