Тема рефлексии нечасто поднималась на форумах или блогах Cache. Быть может потому, что понятие рефлексии как таковое в Cache явно не обозначено. Тем не менее рефлексия в Cache присутствует и может стать очень полезным инструментом в разработке.
Что такое рефлексия
Понятие рефлексии или отражения означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Некоторые программы способны обрабатывать собственные конструкции как данные, выполняя рефлексивные модификации.
Рефлексивно-ориентированное программирование включает в себя самопроверку, самомодификацию и самоклонирование. Тем не менее, главное достоинство рефлексивно-ориентированной парадигмы заключается в динамической модификации программы, которая может быть определена и выполнена во время работы программы.
Данное понятие впервые ввел Brian Cantwell Smith в своей кандидатской диссертации.
На сайте oracle.com об использовании рефлексии говорится, что она обычно используется программами, которые требуют проверки или изменения поведения приложения во время исполнения кода. Это относительно продвинутая технология и должна использоваться только разработчиками, имеющими серьезное понимание основ языка. Имея это в виду, Вы сможете использовать рефлексию как мощный инструмент и сделаете возможным то, что ранее казалось не возможным в Вашем приложении.
Если Вы еще не знакомы с Reflection на практике, то данное понятие не слишком наглядно.
Обычно, рефлексия сводится к тому, чтобы, продумав заранее инструкции к работе программы, не зная в какой момент времени она будет работать с конкретным объектом, изменять поведение приложения тем или иным способом.
Рефлексия в Cache
Как такового раздела или выделенного понятия мне так и не встретилось.
Тем не менее, некоторые функции относятся к этой замечательной теме.
Итак, встречаем:
$CLASSMETHOD — выполняет заданный метод класса в желаемом классе (из любой точки программы);
$CLASSNAME — возвращает имя класса;
$ISOBJECT — проверяет является ли указанное выражение объектом или нет;
$METHOD — позволяет вызвать метод у заданного экземпляра класса;
$NAME — возвращает наименование переменной;
$PROPERTY — ссылается на конкретное свойство объекта и возвращает его значение;
$PARAMETER — возвращает значение указанного параметра класса.
И отдельно хочу выделить
$XECUTE — выполняет код, переданный в виде строки, с указанными параметрами.
Вы можете почитать подробнее об этих функциях и найти примеры их использования в документации.
Пример использования:
$XECUTE ("set name = ##class(Data.SampleDict1).%OpenId("_param_").Name")
т.е. мы можем любую строку выполнить как код. Все бы ничего, но не стоит забывать о безопасности. Если у нас веб приложение и в какой-либо момент подсунуть этой команде смогут строку типа “убить систему”(тут мог быть смайлик, но правила запрещают).
И не забываем, что это компиляция во время выполнения, поэтому использовать только в крайних случаях.
Остальные рефлексивные функции работают примерно так же, как и в других языках.
Применим на практике
Пусть требуется создать веб приложение, состоящее из рабочей области с двумя вкладками. Вкладки должны иметь разный состав полей и работа с ними будет ассоциирована с реальными объектами в базе данных.
Например, первая вкладка отвечает за справочную информацию одного рода, вторая другого.
Пользователь должен иметь возможность создать запись в справочнике и просмотреть результаты работы в рабочей области в виде журнала записей. С возможностью поиска и сортировки по указанному параметру. Наше приложение — это демонстрационная версия применения нескольких рефлексивных функций при работе с базой данных.
Вы можете скачать исходный код по ссылке: пример приложения и просто импортировать в Studio.
Рассмотрим как будет выглядеть csp страничка dict1.CSP:
код dict1.csp
<script language="Cache" runat="Server">
do ##class(Front.Blocks).PrintHeader("sampleDict",%session)
</script>
<script language="Cache" method="logout">
do %session.Logout()
</script>
<script language="Cache" runat="Server">
// Готовим структуры данных для полей поиска:
set searchFields = ##class(%ListOfDataTypes).%New()
do searchFields.Insert(
##class(Front.Helpers.SearchColumn).%New("Name","String")
)
// - для полей формы списка
set listFields = ##class(%ListOfDataTypes).%New()
do listFields.Insert(
##class(Front.Helpers.ListColumn).%New("ID","#",100)
)
do listFields.Insert(
##class(Front.Helpers.ListColumn).%New("Name","Название",200)
)
do listFields.Insert(
##class(Front.Helpers.ListColumn).%New("Code","Код",300)
)
// - для полей формы редактирования
set detailFields = ##class(%ListOfDataTypes).%New()
do detailFields.Insert(
##class(Front.Helpers.DetailColumn).%New("Name","Название",300, 1)
)
do detailFields.Insert(
##class(Front.Helpers.DetailColumn).%New("Code","Код",300, 1)
)
// - для заголовков табиков
set titleFields = ##class(%ListOfDataTypes).%New()
do titleFields.Insert(
"ID"
)
do titleFields.Insert(
"Name"
)
do ##class(Front.Blocks).PrintAngularJs()
do ##class(Front.Blocks).PrintGridJs()
do ##class(Front.Blocks).PrintNgGridFlexibleHeightPluginJs()
do ##class(Front.Blocks).PrintBootstrapJs()
do ##class(Front.Blocks).PrintDatePickerJs()
do ##class(Front.Blocks).PrintDftabmenuJs()
do ##class(Front.Blocks).PrintModalJs()
do ##class(Front.LDController).Initialize(searchFields, listFields, detailFields,"Data.SampleDict1","Тестовая область",titleFields, 1)
do ##class(Front.Blocks).PrintFooter()
</script>
do ##class(Front.Blocks).PrintHeader("sampleDict",%session)
</script>
<script language="Cache" method="logout">
do %session.Logout()
</script>
<script language="Cache" runat="Server">
// Готовим структуры данных для полей поиска:
set searchFields = ##class(%ListOfDataTypes).%New()
do searchFields.Insert(
##class(Front.Helpers.SearchColumn).%New("Name","String")
)
// - для полей формы списка
set listFields = ##class(%ListOfDataTypes).%New()
do listFields.Insert(
##class(Front.Helpers.ListColumn).%New("ID","#",100)
)
do listFields.Insert(
##class(Front.Helpers.ListColumn).%New("Name","Название",200)
)
do listFields.Insert(
##class(Front.Helpers.ListColumn).%New("Code","Код",300)
)
// - для полей формы редактирования
set detailFields = ##class(%ListOfDataTypes).%New()
do detailFields.Insert(
##class(Front.Helpers.DetailColumn).%New("Name","Название",300, 1)
)
do detailFields.Insert(
##class(Front.Helpers.DetailColumn).%New("Code","Код",300, 1)
)
// - для заголовков табиков
set titleFields = ##class(%ListOfDataTypes).%New()
do titleFields.Insert(
"ID"
)
do titleFields.Insert(
"Name"
)
do ##class(Front.Blocks).PrintAngularJs()
do ##class(Front.Blocks).PrintGridJs()
do ##class(Front.Blocks).PrintNgGridFlexibleHeightPluginJs()
do ##class(Front.Blocks).PrintBootstrapJs()
do ##class(Front.Blocks).PrintDatePickerJs()
do ##class(Front.Blocks).PrintDftabmenuJs()
do ##class(Front.Blocks).PrintModalJs()
do ##class(Front.LDController).Initialize(searchFields, listFields, detailFields,"Data.SampleDict1","Тестовая область",titleFields, 1)
do ##class(Front.Blocks).PrintFooter()
</script>
Для отрисовки тех или иных частей страницы (Header, Footer) используются специальные методы в Front.Blocks.
Чтобы задать поля, по которым будем производить поиск формируем список searchFields, для области редактирования список detailFields и для области просмотра listFields.
Затем инициализируем наш контент в LDController.
do ##class(Front.LDController).initialize(searchFields, listFields, detailFields," Data.SampleDict1","Тестовая область1",titleFields, 1)
В dict2.CSP будет соответственно
do ##class(Front.LDController).initialize(searchFields, listFields, detailFields," Data.SampleDict2","Тестовая область2",titleFields, 1)
LDController — универсальный класс. В нем заложены основные функции работы типовых csp страниц, таких как наши тестовые справочники.
В данном случае, он позволяет получать список записей справочной информации из базы, фильтровать его по заданному полю(мы задали только фильтр по наименованию), знать количество записей всего и сколько на странице, создавать и редактировать записи.
Методы класса LDController:
initialize — представляет собой html код формы редактирования и области просмотра. Это бешеный микс скриптов и кашешного кода, я не буду вдаваться в подробности, описывая его.
saveItemData – сохраняет данные из области редактирования в базу.
В общем-то, везде, кроме, возможно, метода initialize удалось сохранить понятную и простую структуру кода, реализовать нужный нам функционал и добиться расширяемости приложения. Даже новичку не составит труда создать отображение для других объектов базы данных в виде новой csp странички. Функциональные части разбиты на логические составляющие, понятно что и где нужно будет менять, если понадобится.
Выглядит приложение следующим образом.
Область просмотра и поиска существующих записей
Область редактирования элемента первого справочника
Область редактирования элемента второго справочника
Область просмотра с сохраненной записью
Разберем пример создания/редактирования объекта на примере метода saveItemData. Параметрами являются список полей объекта, данные с формы в виде JSON структуры и имя класса. Используя функцию $CLASSMETHOD мы можем создать/открыть объект зданного класса(по его имени). Заполнить свойства класса значениями из структуры данных с вкладки используя функцию $PROPERTY и сохранить объект в базу данных.
код метода saveItemData
ClassMethod saveItemData(ListColumnsJSON As %String, ItemData As %String, DatasourceClassName As %String)
{
set listColumns = ##class(Utils.JSON).Decode(ListColumnsJSON)
$$$THROWONERROR(st,##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(ItemData,,.itemData,1))
if (itemData.ID =0) {
set obj = $CLASSMETHOD(DatasourceClassName,"%New")
}
else {
set obj = $CLASSMETHOD(DatasourceClassName,"%OpenId",itemData.ID)
}
for {
set field=listColumns.GetNext(.idx)
quit:idx=""
set fieldName = field.GetAt("Field")
if (fieldName '= "ID") {
set $PROPERTY(obj,fieldName) = $PROPERTY(itemData,fieldName)
}
}
do obj.%Save()
}
{
set listColumns = ##class(Utils.JSON).Decode(ListColumnsJSON)
$$$THROWONERROR(st,##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(ItemData,,.itemData,1))
if (itemData.ID =0) {
set obj = $CLASSMETHOD(DatasourceClassName,"%New")
}
else {
set obj = $CLASSMETHOD(DatasourceClassName,"%OpenId",itemData.ID)
}
for {
set field=listColumns.GetNext(.idx)
quit:idx=""
set fieldName = field.GetAt("Field")
if (fieldName '= "ID") {
set $PROPERTY(obj,fieldName) = $PROPERTY(itemData,fieldName)
}
}
do obj.%Save()
}
Тоже самое возможно сделать при получении записей из базы и их удалении.
Как видите, пользоваться рефлексией в Cache очень просто и легко.
Пусть не слишком много возможностей раскрыто, но задача выполнена – удалось уйти от объектов и работы с объектами как таковыми, а так же получилось упростить структуру кода, делать ее понятной. Для тех, кто только начинает изучать возможности языка, данный пример может стать хорошим стартом для собственных открытий и решений.
Список источников:
1. Reflection (computer programming), Wikipedia
2. Procedural reflection in programming languages, Brian Cantwell Smith
3. The Reflection API, Oracle
4. Cache ObjectScript Functions, Intersystems
5. CacheJSON is a JSON encoder/decoder for Intersystems Cache
eduard93
Метод saveItemData может быть переработан с использованием стандартного механизма десериализации json<->bin.
$$$THROWONERROR(st, ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(ListColumnsJSON ,,.listColumns ,1)) преобразует ListColumnsJSON в экземпляр класса %Library.ListOfObjects.
olgafm Автор
Да, действительно может. Но в таком случае у нас получится список объектов класса Front.Helpers.ListColumn, а не %Library.ArrayOfDataTypes.
Нам необходимо будет немного переделать код в тех местах, где он использует методы для работы с массивом.
Естественно, я так набросала быстренько, но ведь это еще не все.
Если пользоваться дебагером, что в нашем случае оказалось вполне реально(я вообще со студийным дебаггером немного в контрах), то можно увидеть ошибку при попытке использования стандартного(встроенного) конвертора как раз из-за отсутствия передаваемых параметров, ну и исправить ее. Чтобы стандартный метода заработал, нужно изменить сигнатуру метода %OnNew() в классе Front.Helpers.ListColumn. Сделать возможным, чтобы он принимал пустые строки.
Пожалуй, это хороший альтернативный вариант для данного случая.
Но вообще мне для работы с JSON и REST приложениями нравится Utils.JSON. В общем-то достаточно удобная вещь. С ее помощью можно формировать сложные структуры. Например JSON для объекта, имеющего внутри и списки и массивы данных, делать вложенные структуры. Наименовать JSON на выходе не только «childrens», но как-нибудь по другому=)
eduard93
>на выходе не только «childrens», но как-нибудь по другомy
Можно с помощью класса %ZEN.proxyObject — подробнее в спойлере.
{
set obj=##class(%ZEN.proxyObject).%New()
set obj.property = "value"
set obj.objproperty = ##class(%ZEN.proxyObject).%New()
set obj.objproperty.simpleproperty="value"
set obj.arrayofdt = ##class(%ListOfDataTypes).%New()
do obj.arrayofdt.Insert(1)
do obj.arrayofdt.Insert("string")
set obj.arrayofobj = ##class(%ListOfObjects).%New()
set obj2=##class(%ZEN.proxyObject).%New()
set obj2.property = "value"
do obj.arrayofobj.Insert(obj2)
do obj.arrayofobj.Insert(obj2)
do obj.%ToJSON() //object root
////////////////////////////////////////////////
set arrayofdt = ##class(%ListOfDataTypes).%New()
do arrayofdt.Insert(1)
do arrayofdt.Insert("string")
do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(arrayofdt) //array root, %ListOfObjects is pretty much the same
////////////////////////////////////////////////
set array(1) = $lb("Sam","M",20)
set array(2) = $lb("John","M",25)
set array(3) = $lb("Kate","F",30)
do ##class(%ZEN.Auxiliary.jsonProvider).%ArrayToJSON($lb("name","sex","age"),.array)
}
doublefint
а давайте сделаем предположение, что одно из полей с типом %Date ;)
olgafm Автор
Спасибо за комментарий. Я Вам отвечу, как только снова доберусь до компьютера!
Я могла бы, конечно, сказать, что это же демо, я не все случаи рассматриваю. Но я с Вами соглашусь — это упущение, забыть про %Date! ;)
doublefint
Дело не в полноте примера, а в скорее в том, что он, возможно, не совсем подходит.
Приведенный вами пример, можно, и, имхо, нужно реализовывать с помощью «стандартного» ООП — инкапсуляция, наследование, полиморфизм и т.д.
В том ключе, в котором вы описываете рефлексию, в качестве примера можно было бы привести систему класса " а у нас пользователь всё может настроить сам, без программирования ", с созданием новых типов данных, свойств и т.д.
olgafm Автор
Да, я думала об этом.
Мне не хотелось просто рассказывать как работают перечисленные выше функции, это есть и в документации.
А как бы Вы сделали?
doublefint
olgafm Автор
Да, точно, Вы же написали выше.
Соглашусь, было бы очень интересно.
TrueMaker
Не знаю почему, но сразу за рефлексией в Cache, я почему-то подумал про телепортацию в масло. А вы? :)
olgafm Автор
Нет=)