Тема рефлексии нечасто поднималась на форумах или блогах 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(searchFieldslistFieldsdetailFields,"Data.SampleDict1","Тестовая область",titleFields, 1)        
    
do ##class(Front.Blocks).PrintFooter()
</
script>

Для отрисовки тех или иных частей страницы (Header, Footer) используются специальные методы в Front.Blocks.
Чтобы задать поля, по которым будем производить поиск формируем список searchFields, для области редактирования список detailFields и для области просмотра listFields.

Затем инициализируем наш контент в LDController.
 do ##class(Front.LDController).initialize(searchFieldslistFieldsdetailFields," Data.SampleDict1","Тестовая область1",titleFields, 1)

В dict2.CSP будет соответственно
 do ##class(Front.LDController).initialize(searchFieldslistFieldsdetailFields," 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()
}

Тоже самое возможно сделать при получении записей из базы и их удалении.

Как видите, пользоваться рефлексией в 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

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


  1. eduard93
    24.07.2015 18:46
    +1

    Метод saveItemData может быть переработан с использованием стандартного механизма десериализации json<->bin.

    $$$THROWONERROR(st##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(ListColumnsJSON ,,.listColumns ,1)) преобразует ListColumnsJSON в экземпляр класса %Library.ListOfObjects.


    1. olgafm Автор
      25.07.2015 02:08
      +1

      Да, действительно может. Но в таком случае у нас получится список объектов класса Front.Helpers.ListColumn, а не %Library.ArrayOfDataTypes.
      Нам необходимо будет немного переделать код в тех местах, где он использует методы для работы с массивом.

      for {
              set listColumn=listColumns.GetNext(.idx)  //получаем один из объектов списка типа Front.Helpers.ListColumn
              quit:idx=""         
              set fieldName = listColumn.Field   //обращаемся к свойству класса напрямую, в fieldName запишется его значение         
              if (fieldName '= "ID") {
                  set $PROPERTY(obj,fieldName) =$PROPERTY(itemData,fieldName)  //устанавливаем в свойство нашего объекта значение полученное из данных вкладки по имени поля
              }
          }     
      


      Естественно, я так набросала быстренько, но ведь это еще не все.
      Если пользоваться дебагером, что в нашем случае оказалось вполне реально(я вообще со студийным дебаггером немного в контрах), то можно увидеть ошибку при попытке использования стандартного(встроенного) конвертора как раз из-за отсутствия передаваемых параметров, ну и исправить ее. Чтобы стандартный метода заработал, нужно изменить сигнатуру метода %OnNew() в классе Front.Helpers.ListColumn. Сделать возможным, чтобы он принимал пустые строки.
      Пожалуй, это хороший альтернативный вариант для данного случая.

      Но вообще мне для работы с JSON и REST приложениями нравится Utils.JSON. В общем-то достаточно удобная вещь. С ее помощью можно формировать сложные структуры. Например JSON для объекта, имеющего внутри и списки и массивы данных, делать вложенные структуры. Наименовать JSON на выходе не только «childrens», но как-нибудь по другому=)


      1. eduard93
        25.07.2015 07:03
        +1

        >на выходе не только «childrens», но как-нибудь по другомy
        Можно с помощью класса %ZEN.proxyObject — подробнее в спойлере.

        Генерация json
        ClassMethod json()
        {
         
        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)
        }


  1. doublefint
    24.07.2015 23:04
    +1

    а давайте сделаем предположение, что одно из полей с типом %Date ;)


    1. olgafm Автор
      25.07.2015 02:18

      Спасибо за комментарий. Я Вам отвечу, как только снова доберусь до компьютера!
      Я могла бы, конечно, сказать, что это же демо, я не все случаи рассматриваю. Но я с Вами соглашусь — это упущение, забыть про %Date! ;)


      1. doublefint
        25.07.2015 10:16
        +1

        Дело не в полноте примера, а в скорее в том, что он, возможно, не совсем подходит.
        Приведенный вами пример, можно, и, имхо, нужно реализовывать с помощью «стандартного» ООП — инкапсуляция, наследование, полиморфизм и т.д.
        В том ключе, в котором вы описываете рефлексию, в качестве примера можно было бы привести систему класса " а у нас пользователь всё может настроить сам, без программирования ", с созданием новых типов данных, свойств и т.д.


        1. olgafm Автор
          25.07.2015 14:27

          Да, я думала об этом.
          Мне не хотелось просто рассказывать как работают перечисленные выше функции, это есть и в документации.
          А как бы Вы сделали?


          1. doublefint
            26.07.2015 15:12
            +1

            говорится, что она (рефлексия) обычно используется программами, которые требуют проверки или изменения поведения приложения во время исполнения кода.

            можно было бы привести систему класса " а у нас пользователь всё может настроить сам, без программирования", с созданием новых типов данных, свойств и т.д.


            1. olgafm Автор
              26.07.2015 15:16

              Да, точно, Вы же написали выше.
              Соглашусь, было бы очень интересно.


  1. TrueMaker
    25.07.2015 08:15

    Не знаю почему, но сразу за рефлексией в Cache, я почему-то подумал про телепортацию в масло. А вы? :)


    1. olgafm Автор
      25.07.2015 14:27
      +1

      Нет=)