
Тема рефлексии нечасто поднималась на форумах или блогах Caché. Быть может потому, что понятие рефлексии как таковое в Caché явно не обозначено. Тем не менее рефлексия в Caché присутствует и может стать очень полезным инструментом в разработке.
Что такое рефлексия
Понятие рефлексии или отражения означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Некоторые программы способны обрабатывать собственные конструкции как данные, выполняя рефлексивные модификации.
Рефлексивно-ориентированное программирование включает в себя самопроверку, самомодификацию и самоклонирование. Тем не менее, главное достоинство рефлексивно-ориентированной парадигмы заключается в динамической модификации программы, которая может быть определена и выполнена во время работы программы.
Данное понятие впервые ввел Brian Cantwell Smith в своей кандидатской диссертации.
На сайте oracle.com об использовании рефлексии говорится, что она обычно используется программами, которые требуют проверки или изменения поведения приложения во время исполнения кода. Это относительно продвинутая технология и должна использоваться только разработчиками, имеющими серьезное понимание основ языка. Имея это в виду, Вы сможете использовать рефлексию как мощный инструмент и сделаете возможным то, что ранее казалось не возможным в Вашем приложении.
Если Вы еще не знакомы с Reflection на практике, то данное понятие не слишком наглядно.
Обычно, рефлексия сводится к тому, чтобы, продумав заранее инструкции к работе программы, не зная в какой момент времени она будет работать с конкретным объектом, изменять поведение приложения тем или иным способом.
Рефлексия в Caché
Как такового раздела или выделенного понятия мне так и не встретилось.
Тем не менее, некоторые функции относятся к этой замечательной теме.
Итак, встречаем:
$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()
}
Тоже самое возможно сделать при получении записей из базы и их удалении.
Как видите, пользоваться рефлексией в Caché очень просто и легко.
Пусть не слишком много возможностей раскрыто, но задача выполнена – удалось уйти от объектов и работы с объектами как таковыми, а так же получилось упростить структуру кода, делать ее понятной. Для тех, кто только начинает изучать возможности языка, данный пример может стать хорошим стартом для собственных открытий и решений.
Список источников:
1. Reflection (computer programming), Wikipedia
2. Procedural reflection in programming languages, Brian Cantwell Smith
3. The Reflection API, Oracle
4. Caché ObjectScript Functions, Intersystems
5. CacheJSON is a JSON encoder/decoder for Intersystems Cache