Не очередной язык программирования. Часть 2: Логика представлений

  • Tutorial


Вторая часть трилогии о языке и платформе lsFusion. Первую часть можно найти тут.

В ней речь пойдет о логике представлений, а именно обо всем том, что связано с объединением данных и отображением их пользователю или другим информационным системам.

Понятно, что многим, возможно, не так интересно смотреть на презентацию бойца, и им хотелось бы увидеть реальный бой, по возможности с кровью (и она будет, благо обсуждение предыдущих статей помогло лучше понять незащищенные места потенциальных конкурентов и куда именно там надо бить). Но тут необходимо учитывать две вещи:

а) это Хабр. То есть технический ресурс, здесь не любят красивые картинки и рекламные лозунги – чтобы что-то заявлять, нужны подробности, как ты собираешься этого достичь.
б) это рынок разработки информационных систем, и он очень похож на рынок средств для похудения. Тут все заявляют, что у нас быстро и просто. Но когда дело доходит до деталей, в которых, как известно, и кроется дьявол, в качестве примеров приводят или простейшие CRUD’ы или прибегают к различным уловкам: показывают какие-то обрывки кода, а основную его часть прячут со словами «это не важно», «делается за пару минут» и все в таком роде.

Собственно, поэтому у нас было два варианта: либо начать с преимуществ и риска получить упреки в маркетинг-bullshit, либо начать с технического описания и вопросов «зачем нам еще один язык». Теоретически, конечно, все это можно было сделать в одной статье, но такую статью тяжело было бы не то что прочитать, а даже просто пролистать. Соответственно, мы выбрали второй вариант, хотя если кому-то все же важно узнать про причины появления и преимущества языка прямо сейчас (а не в будущих статьях), добро пожаловать на сайт. Он состоит из всего трех страниц: что, как и почему не, и дает, на мой взгляд, вполне достаточно информации для ответов на все эти вопросы. Плюс там же можно попробовать платформу онлайн, в том числе, чтобы убедиться, что никакого «рояля в кустах» там нет, и код из примеров в этой статье – это реально весь код, необходимый для запуска приложения.

Но хватит лирических отступлений, вернемся к презентации бойца описанию логики представлений.

Как и в логике предметной области (первой статье), все понятия логики представлений в lsFusion образуют стек:



и именно в порядке этого стека я буду про них рассказывать.


Формы


Форма – это самое главное (и на самом деле практически единственное) понятие в логике представлений, которое отвечает за все – как за взаимодействие с пользователем, так и за печать, экспорт и импорт данных.

Форму логически можно разделить на две части:

  • Структура формы определяет, какие данные показывает форма.
  • Представление формы определяет, в каком виде она эти данные показывает.

Структура формы


Начнем, естественно, со структуры формы.

Объекты


При создании формы первоначально необходимо определить, какие объекты она будет отображать. Тут стоит отметить, что дальше может быть небольшая путаница в терминологии между объектами формы и объектами, которые отображаются в этих объектах формы. Поэтому в дальнейшем, если это будет не очевидно из контекста, будем использовать термины «объекты формы» (для первого случая) и «объекты в базе» (для второго).

Для каждого объекта формы нужно задать его класс. Этот класс может быть как примитивным (встроенным), так и объектным (пользовательским).
FORM currentBalances 'Текущие остатки'
    OBJECTS s = Stock, i = Item // сверху склад, снизу товар
;
В соответствии с порядком добавления объектов на форму образуется упорядоченный список объектов. Соответственно, последним объектом для некоторого множества объектов будем называть объект из этого множества с максимальным порядковым номером в этом списке (то есть максимально поздний).

Каждый объект на форме в любой момент времени имеет текущее значение. Его изменение происходит в зависимости от представления, либо в результате соответствующих действий пользователя в интерактивном представлении, либо «виртуально» в процессе чтения данных в статичном представлении.

Свойства и действия


После определения объектов на форму можно добавить свойства и действия, подставив описанные выше объекты им на вход в качестве аргументов.

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

Объект отображения


Каждое свойство отображается ровно в одном объекте на форме (будем называть его объектом отображения этого свойства). По умолчанию объектом отображения является объект, последний для множества объектов, которые передаются на вход этому свойству. Например, если у нас есть форма текущих остатков с двумя объектами – складом и товаром, и тремя свойствами – именами склада и товара и остатком товара на складе:
FORM currentBalances 'Текущие остатки'
    OBJECTS s = Stock, i = Item // сверху склад, снизу товар
    PROPERTIES name(s), name(i), currentBalance(s, i)
;
То для имени склада объектом отображения будет s (склад), а для имени товара и остатка – i (товар).

Впрочем, при необходимости разработчик может задавать объект отображения явно (то есть, к примеру, в интерактивном представлении показать свойство с остатком в таблице складов, а не товаров).

Фильтры и сортировки


Для каждой формы разработчик может задать фильтры и порядки, которые будут ограничивать список доступных к просмотру / выбору объектов на форме, а также порядок их отображения.

Для задания фильтра необходимо указать свойство, которое будет использоваться в качестве критерия фильтрации. Фильтр будет применяться к таблице того объекта, который является последним для множества объектов, передаваемых на вход этому свойству (то есть аналогично с определением объекта отображения свойства). При этом будут показаны только те наборы объектов (ряды), для которых значения свойства не равняются NULL. Например, если мы добавим в форму выше фильтр currentBalance(s,i) OR isActive(i):
FORM currentBalances 'Текущие остатки'
    OBJECTS s = Stock, i = Item // сверху склад, снизу товар
    PROPERTIES name(s), name(i), currentBalance(s, i)
    FILTERS currentBalance(s, i) OR isActive(i)
;
Скриншот формы


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

Сортировки задаются как список свойств на форме, в порядке которых надо показывать объекты. В остальном все аналогично фильтрам.

Группы объектов


В платформе также существует возможность объединять объекты в группу объектов. В этом случае в таблицах / списках будет показываться «декартово произведение» этих объектов (то есть для двух объектов – все пары, трех объектов – тройки и т.п.).
FORM currentBalances 'Текущие остатки'
    OBJECTS (s = Stock, i = Item) // будет одна таблица из складов и товаров
    PROPERTIES name(s), name(i), currentBalance(s, i)
    FILTERS currentBalance(s, i) OR isActive(i)
;
Скриншот формы


Соответственно, практически везде как до, так и после, вместо одиночных объектов формы можно использовать группы объектов.

Собственно, в документации так и сделано: везде используется более общий термин «группа объектов», но чтобы в этой статье все не усложнять (а группы объектов, состоящие из нескольких объектов, используются значительно реже), было решено про группы объектов забыть и считать, что группа объектов всегда состоит ровно из одного объекта и, соответственно, использовать везде термин «объект» вместо более сложных «группа объектов» и «набор объектов».

Группы свойств


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

Объекты-в-колонки


По умолчанию свойство отображается в своем объекте отображения ровно один раз. При этом в качестве значений объектов, отличных от объекта отображения этого свойства (назовем их верхними), используются их текущие значения. Однако в платформе также существует возможность отображать одно свойство несколько раз таким образом, чтобы в качестве значений некоторых верхних объектов использовались не их текущие значения, а все объекты в базе, подходящие под фильтры. При таком отображении свойства образуется своего рода «матрица» – (объект отображения) x (верхние объекты). Соответственно, чтобы создать такую матрицу, необходимо при добавлении свойства на форму указать, какие именно верхние объекты необходимо использовать для создания колонок (будем называть эти объекты объектами-в-колонки).
FORM currentBalances 'Текущие остатки'
    // так как по складу нет ни одного свойства где он является объектом отображения,
    // его таблица показываться не будет    
    OBJECTS s = Stock, i = Item
    // покажет имя товара и все активные склады в колонки с именами этих складов    
    PROPERTIES name(i), currentBalance(s, i) COLUMNS (s) HEADER name(s)    
    FILTERS isActive(i), isActive(s)
;
Скриншот формы


Итак, с тем, что отображает форма, более-менее разобрались, перейдем к тому, как она это может делать.

Представления формы


Существует три представления формы:



Скриншоты представлений
Интерактивное:



Печатное:



Структурированное:



  • Интерактивное. Представление, с которым пользователь может взаимодействовать – изменять данные и текущие объекты путем инициирования различных событий. Собственно, это представление обычно и принято называть формой.
  • Печатное. Обычно принято называть отчетом – выгрузка всех данных формы и представление их в графическом виде. В том числе с возможностью вывода их на печать (откуда и получило свое название).
  • Структурированное – представление формы в различных структурированных форматах (JSON, XML, DBF и т.п.). Как правило используется для дальнейшей интеграции с другими системами.

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

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

Описание представлений начнем, пожалуй, с самого сложного – интерактивного представления.

Интерактивное представление


В интерактивном представлении объекты формы отображаются в виде таблицы. Ряды в этой таблице соответствуют объектам в базе, удовлетворяющим заданным фильтрам, колонки, в свою очередь, соответствуют свойствам.

Впрочем, при необходимости, свойство можно отображать не в виде колонки таблицы, то есть для всех ее рядов, а в виде отдельного поля на форме, то есть только для текущего значения объекта формы. Например:
currentBalance 'Текущий остаток' (Stock s) = GROUP SUM currentBalance(s, Item i);
FORM currentBalances 'Текущие остатки'
    OBJECTS s = Stock, i = Item
    // currentBalance(s) отобразиться в панели, только для текущего склада    
    PROPERTIES name(s), currentBalance(s) PANEL
            name(i), currentBalance(s, i)
    FILTERS currentBalance(s, i)
;
Скриншот формы


Изменение текущего значения объекта формы происходит либо в результате изменения пользователем текущего ряда таблицы, либо в результате выполнения действия, созданного при помощи специального оператора поиска (SEEK).

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

Все таблицы в интерактивном представлении по умолчанию являются динамическими, то есть читается только ограниченное число объектов в базе, а остальные дочитываются по мере изменения текущего объекта в таблице. Количество отображаемых объектов при этом может как определяться автоматически на основании высоты видимой части таблицы, так и задаваться разработчиком явно при создании формы.

Также форма в интерактивном представлении полностью реактивна, то есть автоматически обновляет все данные на форме при изменении любых данных, которые на них влияют (такой React, только в общем случае). Плюс все это делается не полным перерасчетом (как в том же React), а инкрементально, причем на SQL сервере.

Вообще забавно, когда при сравнении с другими технологиями пытаешься включить верхние три требования в задачу, люди часто делают круглые глаза, как будто их просят запустить человека в космос. Хотя невыполнение второго требования любым нормальным пользователем будет классифицировано как баг, а первое и третье требование – это то, что разработанная форма заработает нормально, когда в базе появится хоть немного данных (несколько десятков тысяч записей, например).

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

Деревья объектов


Помимо таблиц платформа также позволяет организовывать отображение объектов в виде деревьев, как плоских («вложенных» друг в друга таблиц), так и рекурсивных (например, «вложенных» друг в друга объектов в базе).

Плоские деревья, по сути, являются обобщением таблиц, когда в одну таблицу «объединяется» сразу несколько таблиц:
FORM currentBalances 'Текущие остатки'
    TREE tree s = Stock, i = Item
    // имя склада отобразиться над именем товара
    // а остаток склада отобразиться над остатком по товару на складе
    PROPERTIES name(s), currentBalance(s), 
               name(i), currentBalance(s, i)
;
Скриншот формы


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

А вот рекурсивные деревья, наоборот, используются достаточно часто (например, для реализации классификаторов). Чтобы отобразить объект формы в виде такого дерева, для него необходимо задать дополнительный фильтр – свойство, значение которого для нижних объектов должно равняться верхнему объекту. Первоначально считается, что верхний объект равен NULL.
parent = DATA ItemGroup (ItemGroup) IN base;
group = DATA ItemGroup (Item) IN base;
// определяем вложенность групп
level 'Уровень' (ItemGroup child, ItemGroup parent) = RECURSION 1 AND child IS ItemGroup AND parent = child STEP 1 IF parent = parent($parent);
currentBalance 'Текущий остаток' (ItemGroup ig, Stock s) = GROUP SUM currentBalance(s, Item i) IF level(ig, group(i));

FORM currentBalances 'Остатки по группам'
    OBJECTS s=Stock PANEL // выбранный склад
    PROPERTIES (s) name
    TREE tree ig = ItemGroup PARENT parent, i = Item // дерево по группам товаров / товарам
    PROPERTIES name(ig), currentBalance(ig, s)
    PROPERTIES name(i), currentBalance(s, i)
    FILTERS group(i) = ig
;
Скриншот формы


Управление формой пользователем


Для обеспечения лучшей эргономики системы (в том числе чтобы не создавать формы под каждый чих) часть операций по настройке интерактивного представления формы могут выполнять сами пользователи. Например, такими операциями являются:

  • настройка таблиц (видимые колонки, порядок, фонты и т.п.),
  • создание пользовательских фильтров и сортировок,
  • группировка данных по значениям колонок,
  • печать таблицы и выгрузка ее в Excel.

Также разработчик может создавать так называемые группы фильтров, которые пользователь может включать / выключать самостоятельно. Например:
EXTEND FORM currentBalances // расширяем ранее созданную форму с остатками
    FILTERGROUP stockActive // создаем группу фильтров с одним фильтрам, которая будет показываться в виде флажка, по которому пользователь сможет включать/отключать фильтр
        FILTER 'Активные' active(st) 'F11' // добавляем отбор по только активным складам, который будет применяться по нажатию клавиши F11
    FILTERGROUP bal
        FILTER 'С положительным остатком' currentBalance(st, sk) > 0 'F10'
        FILTER 'С отрицательными остатком' currentBalance(st, sk) < 0 'F9'
        FILTER 'С остатком' currentBalance(st, sk) 'F8' DEFAULT
        FILTER 'Без остатка' NOT currentBalance(st, sk) 'F7'
;
Это далеко не все возможности по настройке системы пользователем, но к остальным возможностям вернемся уже в третьей статье, так как большинство из них все же не имеют прямого отношения к логике представлений.

Отметим, что описанный выше функционал относится скорее к функционалу ERP-платформ, что уже совсем диссонирует с заголовком статьи. С другой стороны, как упоминалось в первой статье, в перспективе язык / платформа претендует на замену, в том числе и этого класса платформ, поэтому совсем не упомянуть эти возможности, на мой взгляд, было бы неправильно.

Операторы работы с объектами


Одним из наиболее частых сценариев работы с формой является добавление / удаление объекта, а также редактирование его в новой форме. Для реализации таких сценариев в платформе существует предопределенный набор операторов, которые позволяют создавать нужные действия одним словом прямо в операторе создания формы:

  • NEW – создание объекта
  • EDIT – редактирование объекта
  • NEWEDIT – создание и редактирование объекта
  • DELETE – удаление объекта

Также, так как очень часто возникает необходимость выполнить эти действия в новой сессии (если нужно отделить действия создания объектов от действий на форме, из которой эти объекты создаются), в платформе поддерживается соответствующий синтаксический сахар – опции NEWSESSION и NESTEDSESSION, которые работают аналогично одноименным операторам создания действий, но, как и сами операторы работы с объектами, не требуют от разработчика создания и именования новых действий. Например:
FORM teams
    OBJECTS t=Team
    // создает / удаляет команду и открывает форму редактирования в новой сессии
    PROPERTIES (t) NEWSESSION NEWEDITDELETE
    OBJECTS p=Player
    FILTERS team(p)=t
    // создает удаляет игрока прямо в таблице в этой же сессии
    PROPERTIES (p) NEWDELETE
;
По умолчанию при редактировании объекта вызывается форма редактирования, автоматически сгенерированная для класса переданного объекта формы. Однако часто необходимо эту форму переопределить (например, добавить дополнительную информацию, изменить дизайн и т.п.) Для того чтобы сделать это, достаточно создать необходимую форму редактирования и указать, что она является формой по умолчанию для редактирования объектов заданного класса:
FORM order 'Заказ'
    OBJECTS o = Order PANEL
    PROPERTIES(o) date, number
    
    OBJECTS d = OrderDetail
    PROPERTIES(d) nameBook, quantity, price, NEWDELETE
    FILTERS order(d) = o
    
    EDIT Order OBJECT o
;
Аналогичным образом переопределяются формы выбора объектов заданного класса.

Дизайн формы


Как и в большинстве существующих GUI, дизайн интерактивного представления формы представляет собой иерархию, узлами которой являются компоненты. Компонентами, в свою очередь, могут быть:

  • контейнеры – компоненты, которые содержат в себе другие компоненты.
  • базовые компоненты – графические представления базовых элементов: таблицы, панели свойств, групп фильтров и т.п.

Механизм расположения компонент внутри контейнеров по сути повторяет CSS Flexible Box Layout (а в веб-клиенте при помощи него и реализуется), поэтому сильно подробно останавливаться на этом механизме не будем.

Отметим, что дизайн формы обычно не создают с нуля (так как это довольно трудоемко). Как правило, дизайн формы создается автоматически на основе структуры формы, а далее разработчик лишь немного изменяет его: к примеру, добавляет новый контейнер, и переносит в него существующие компоненты:

Пример дизайна формы по умолчанию
FORM myForm 'myForm'
    OBJECTS myObject = myClass
    PROPERTIES(myObject) myProperty1, myProperty2 PANEL
    FILTERGROUP myFilter
        FILTER 'myFilter' myProperty1(myObject)
;
Иерархия контейнеров и компонентов в дизайне по умолчанию будет выглядеть следующим образом:


FORM myForm 'Моя форма'
    OBJECTS u = CustomUser
    PROPERTIES(u) name, NEWDELETE
     
    OBJECTS c = Chat
    PROPERTIES(c) message, NEWDELETE
    FILTERS user(c) = u
;
 
DESIGN myForm {
    NEW middle FIRST {
        type = CONTAINERH;
        fill = 1// растягиваем во все стороны
        MOVE BOX(u);
        MOVE BOX(c);
    }
}
Скриншот формы


Дизайн формы 2.0 (React)


Посмотрев на интерактивные представления форм в приведенных выше скриншотах (или, к примеру, в онлайн-демо), можно заметить, что текущий дизайн форм в пользовательском интерфейсе, скажем так, весьма аскетичный. Это, конечно, никогда не было особой проблемой для информационных систем, где основными пользователями являются сотрудники или партнеры компании, владеющей этими информационными системами. Тем более что, несмотря на аскетичность, текущий механизм дизайна формы позволяет реализовывать весьма непростые случаи, например, POS:

Скриншот формы


Но если речь заходит, скажем, о SaaS B2B или тем более B2C (например, какие-нибудь интернет-банкинги), то тут сразу начинают появляться вопросы, как сделать дизайн более эргономичным.

На текущем этапе для решения этой проблемы разработана специальная javascript-библиотека, основная задача которой – создание и обновление специального js-объекта, содержащего данные формы. Соответственно, этот объект можно использовать в качестве state для React компоненты и тем самым создавать любой дизайн и любую дополнительную интерактивность разрабатываемой формы. Например:

Пример формы на React (на codesandbox)

Или более сложный пример – с выпадающими списками и использованием для этого REST (а точнее Stateless) API:

Пример формы на React с выпадающими списками (на codesandbox)

Правда, проблема такого подхода в том, что формы из примеров выше не будут встроены ни в общий интерфейс платформы, ни в общий control flow действий. Поэтому одной из самых ближайших задач в разработке платформы является перевод механизма дизайна формы на схему используемую в дизайне отчета (печатного представления):

  • Платформа автоматически генерирует react-дизайн на основе структуры формы как в примере выше.
  • При необходимости разработчик может его сохранить и отредактировать как захочет. Соответственно, далее платформа при открытии формы будет использовать именно этот отредактированный дизайн.

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

Правда, отметим, что и старый механизм дизайна тоже будет поддерживаться, так как, для тех же бизнес-приложений, он отлично выполняет свою функцию.

События формы


События формы – второй ключевой механизм после событий предметной области, который отвечает за определение моментов, когда надо выполнять действия.

В платформе существует целый набор различных событий формы, возникающих в результате тех или иных действий пользователя, но в этой статье рассмотрим только одно, самое часто используемое из них – событие CHANGE. Это событие возникает, когда пользователь инициировал изменение свойства / вызов действия, например, нажав на любую не системную клавишу на клавиатуре, находясь на поле изменяемого свойства, или кликнув на это поле мышкой.

Как и для событий предметной области, для событий формы можно задать обработку – действие, которое будет выполняться при наступлении заданного события. Отметим, что у большинства событий формы уже есть некоторые обработки по умолчанию, которые из коробки реализуют наиболее ожидаемое со стороны пользователя поведение (например, при вышеупомянутом событии CHANGE – запросить ввод у пользователя и изменить свойство на введенное значение). Впрочем, на практике иногда все же возникают ситуации, когда для некоторого события формы необходимо задать какую-то специфическую обработку, например:
changeQuantity (Order o, Book b)  { 
    INPUT q = INTEGER DO { // запрашиваем число
        IF lastOrderDetail(o, b) THEN { // проверяем, есть ли хоть одна строка
            IF q THEN // ввели число
                quantity(OrderDetail d) <- q IF d = lastOrderDetail(o, b) WHERE order(d) = o AND book(d) = b; // записываем количество в последнюю строку с такой книгой
            ELSE // сбросили число - удаляем строку
                DELETE OrderDetail d WHERE order(d) == o AND book(d) == b;   
        } ELSE
            IF q THEN
                NEW d = OrderDetail { // создаем новую строку
                    order(d) <- o;
                    book(d) <- b;
                    quantity(d) <- q;
                }
    }
}  

EXTEND FORM order
    OBJECTS b = Book
    PROPERTIES name(b) READONLY, quantity(o, b) ON CHANGE changeQuantity(o, b)
;
Как видим, в обработке выше есть определенная магия – в ней мы работаем и с сервером, и с клиентом одновременно (еще большая магия, на самом деле, состоит в том, что платформа поддержит операции групповой корректировки и paste для этой обработки, но этот функционал уже выходит за рамки данной статьи). Возможность такого обращения сервера к клиенту является одной из важных особенностей платформы и поддерживается сразу в нескольких операторах работы с формами. И тут мы уже плавно переходим к операторам формы.

Операторы формы


Для работы с формой в платформе поддерживается несколько специальных операторов, позволяющих, например, управлять фокусом, пользовательскими фильтрами и сортировками, текущими значениями объектов формы и так далее, но самым важным все же, наверное, является вышеупомянутый оператор ввода значения INPUT.

Для этого оператора можно задать примитивный тип, предыдущее значение, действие которое необходимо выполнить, если ввод был успешно завершен, а также действие, которое необходимо выполнить, если ввод был отменен. Например:
FORM order
    OBJECTS o = Order
    PROPERTIES(o) customer ON CHANGE {
            INPUT s = STRING DO {
                customer(o) <- s;
                IF s THEN
                    MESSAGE 'Customer changed to ' + s;
                ELSE
                    MESSAGE 'Customer dropped';
            }
        }
;
Соответственно, когда платформа видит такой оператор, она автоматически:

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

Надо сказать, что INPUT – не единственный оператор ввода данных. Помимо него за ввод данных также отвечают диалоговая форма оператора показа сообщения (ASK):
DELETE Order o WHERE selected(o);
ASK 'Вы собираетесь удалить ' + (GROUP SUM 1 IF DROPPED(Order o)) + ' заказов. Продолжить ?' DO {
    APPLY;
}
а также диалоговый режим оператора открытия формы (DIALOG) в интерактивном представлении, но к этому режиму, как и к самому оператору открытия формы, мы вернемся после того, как рассмотрим остальные представления.

На этом с интерактивным представлением закончим и перейдем к статичным представлениям.

Статичные представления


Как уже отмечалось ранее, статичные представления читают все данные формы на момент ее открытия. Соответственно, для этих представлений нужно определить, в каком именно порядке читать эти данные. Для этого объекты формы необходимо организовать в иерархию, в рамках которой данные для объектов будут своего рода «вкладываться» друг в друга. К примеру, если у нас есть объекты A и B, и A является родителем B, то в статичном представлении сначала будут отображаться все свойства A для первого объекта в базе из A, затем все свойства B и все свойства пары (A, B) для всех объектов в базе из B, затем будет отображаться аналогичная информация для второго объекта в базе из A и всех объектов в базе из B и так далее (этот механизм вложения объектов друг в друга очень похож на уже упоминавшийся механизм «плоских» деревьев в интерактивном представлении).

Иерархию объектов платформа строит автоматически на основании структуры формы, хотя нельзя сказать, что полный алгоритм построения этой иерархии сильно очевиден:

  • Сначала строятся связи между объектами по следующим правилам:
  • После того как связи построены, иерархия строится таким образом, что родителем объекта A выбирается наиболее поздний в списке объектов объект B, от которого A зависит (напрямую или косвенно).

Например:
FORM myForm 'myForm'
    OBJECTS A, B SUBREPORT, C, D, E
    PROPERTIES f(B, C), g(A, C)
    FILTERS c(E) = C, h(B, D) 
;


Впрочем, на практике настолько сложные формы, как в примере выше, практически не встречаются, и обычно иерархия объектов формы достаточно очевидна из ее структуры.

Печатное представление


Для того чтобы отобразить считанные данные в графическом формате используется LGPL библиотека формирования отчетов – JasperReports.

Как и в других библиотеках формирования отчетов, в JasperReports каждый отчет может содержать группировки данных, а также другие подотчеты. Соответственно, иерархия объектов преобразуется в отчет следующим образом:

  • «цепочки» объектов (то есть, O1, O2, O3,… On, где O2 – единственный прямой потомок O1, O3 – единственный прямой потомок O2 и т.д.) превращаются в группировки;
  • если у объекта несколько потомков, то для каждого такого потомка создаются подотчеты.

При необходимости любую цепочку объектов можно «разбить» и принудительно создать для объекта подотчет, задав этому объекту опцию SUBREPORT (обычно это необходимо делать, если для объекта нужно отображать данные даже при отсутствии данных в объекте-потомке):



Пример формы в печатном представлении:
FORM shipment
    OBJECTS s=Shipment // накладная
    PROPERTIES (s) date, customer = nameCustomer, stock = nameStock // печатаем дату, имя покупателя (с именем customer) и имя склада (с именем stock)
    PROPERTIES total = (GROUP SUM quantity(ShipmentDetail d)*price(d) IF shipment(d)=s) // печатаем общую сумму накладной
    OBJECTS sd=ShipmentDetail // строки накладной     
    FILTERS shipment(sd) = s // строки накладной из накладной
    PROPERTIES (sd) index, item = nameItem // печатаем номер, имя товара (с именем item)
    PROPERTIES (sd) price, quantity // печатаем количество, цену
    PROPERTIES sum 'Сумма' = (quantity(sd) * price(sd)) // печатаем кол-во * цену (с именем sum)    
;

run() {
    // печатаем накладную с номером 12345
    PRINT shipment OBJECTS s = (GROUP MAX Shipment s IF number(s) = '12345'
    XLSX TO exportFile;
}
Скриншот формы


Дизайн отчета


За дизайн отчета отвечает уже упомянутый JasperReports (собственно, это и есть его основная функция в lsFusion). Как и дизайн формы, дизайн отчета обычно не создается с нуля. Так при первом запуске отчета платформа автоматически создает необходимые файлы на основе структуры формы, а далее разработчик прямо из формы предпросмотра может изменять этот дизайн при помощи специальной программы JasperSoft Studio.

Тут кстати мы единственный раз пожалели, что lsFusion-плагин под IDEA, а не под Eclipse, так как для разработки дизайна отчета приходится устанавливать отдельную программу (в Eclipse поддержку JasperReports можно устанавливать в виде плагина). С другой стороны IDEA поддерживает такую замечательную штуку, как language injection, которую мы использовали прямо внутри jrxml-файлов, в которых хранятся отчеты, в результате чего, к примеру, поиск использования и переименование свойств на форме, поддерживается в том числе и в отчетах, что не раз спасало нам жизнь. Ну и мы, если честно, так и не нашли в Eclipse аналогов GrammarKit с autocomplete (правда, его пришлось самим допиливать), stub-индексов, поддержку lazy chameleon-узлов (по сути двухфазного парсера), а это все очень важно для поддержки сложных языков, а главное для их эффективной работы на больших файлах и проектах. Но это уже отдельная тема.

Структурированное представление


Все структурированные представления (форматы) можно условно разделить на два типа:

  • Иерархические (XML, JSON) – один текстовый файл, а информация для объектов помещается в виде списка (массива) внутрь информации для объекта-родителя.
  • Плоские (DBF, CSV, XLS) – по одному файлу-таблице для каждого объекта. При этом для каждого объекта с глубиной в иерархии больше единицы в ее таблице должна присутствовать колонка с именем parent, содержащая номер «верхней» строки в таблице объекта-родителя.

Отметим, что работа с плоскими форматами при глубине иерархии больше единицы не очень удобна (из-за необходимости поддержки дополнительной колонки), поэтому, как правило, плоские форматы используются только для работы с простыми формами (с глубиной иерархии меньше единицы). В остальных случаях обычно используются иерархические форматы. Соответственно, с них и начнем.

Иерархические форматы


В мире существует два общепризнанных иерархических формата – XML, JSON. У каждого есть свои плюсы и минусы, но в целом они не так сильно отличаются друг от друга, поэтому в этой статье рассмотрим только JSON (для XML все с большего аналогично).

Если попытаться сформулировать процесс экспорта / импорта формы в JSON в двух словах, то все достаточно просто: объекты соответствуют массивам в JSON-объекте, группы свойств – объектам, свойства – полям. Формальный алгоритм же немного сложнее:

Алгоритм преобразования формы в JSON
Перед тем как непосредственно приступить к экспорту / импорту формы платформа строит иерархию свойств, групп объектов / свойств следующим образом:

  • Строится иерархия объектов / групп свойств в соответствии с иерархией объектов и объектами отображения свойств: группа отображения свойства считается родителем этого свойства, иерархия объектов сохраняется.
  • Затем для каждого объекта X:
    • для всех потомков X определяются группы свойств, которым они принадлежат, после чего эти группы свойств и их предки автоматически включаются в иерархию. При этом:
      • родителями потомков X становятся группы свойств, которым они принадлежат
      • иерархия групп свойств сохраняется
      • родителями самых верхних (то есть без родителей) использованных групп свойств становится объект X.

После того как иерархия построена, форма экспортируется / импортируется рекурсивно по следующим правилам:

JSON результат ::=
   { JSON с свойствами, группами объектов / свойств без родителей }
 
JSON с свойствами, группами свойств / объектов ::=
    JSON свойства 1 | JSON группы свойств 1 | JSON группы объектов 1
    JSON свойства 2 | JSON группы свойств 2 | JSON группы объектов 2
    ...
    JSON свойства M | JSON группы свойств M | JSON группы объектов M
 
JSON свойства ::=
    "имя свойства на форме" : значение свойства
 
JSON группы свойств ::=
    "имя группы свойств" : { JSON с дочерними свойствами, группами свойств / объектов }
 
JSON группы объектов ::=
    "имя группы объектов" : [
        { JSON с дочерними свойствами, группами свойств / объектов 1 },
        { JSON с дочерними свойствами, группами свойств / объектов 2 },
        ...
        { JSON с дочерними свойствами, группами свойств / объектов N },
    ]


Пример экспорта:
GROUP money;

FORM shipment
    OBJECTS dFrom=DATE, dTo=DATE
    OBJECTS s=Shipment // накладная
    PROPERTIES (s) date, customer = nameCustomer, stock = nameStock // выгружаем дату, имя покупателя (с именем customer) и имя склада (с именем stock)
    FILTERS dFrom <= date(s) AND date(s) <= dTo // накладные за заданные даты
    OBJECTS sd=ShipmentDetail // строки накладной     
    FILTERS shipment(sd) = s // строки накладной из накладной
    PROPERTIES (sd) IN money index, item = nameItem, price, quantity // выгружаем номер, имя товара (с именем item),  количество, цену в объекте money
;

run() {
    EXPORT shipment OBJECTS dFrom = 2019_02_20, dTo = 2019_04_28// выгружаем накладные в заданном периоде
}
Результат
{
  "s": [
    {
      "date": "21.02.19",
      "sd": [
        {
          "money": {
            "item": "Товар 3",
            "quantity": 1,
            "price": 5,
            "index": 1
          }
        }
      ],
      "stock": "Склад 2",
      "customer": "Поставщик 2"
    },
    {
      "date": "15.03.19",
      "sd": [
        {
          "money": {
            "item": "Товар 1",
            "quantity": 1,
            "price": 5,
            "index": 1
          }
        },
        {
          "money": {
            "item": "Товар 2",
            "quantity": 1,
            "price": 10,
            "index": 2
          }
        },
        {
          "money": {
            "item": "Товар 3",
            "quantity": 1,
            "price": 15,
            "index": 3
          }
        },
        {
          "money": {
            "item": "Товар 4",
            "quantity": 1,
            "price": 20,
            "index": 4
          }
        },
        {
          "money": {
            "item": "Milk",
            "quantity": 1,
            "price": 50,
            "index": 5
          }
        }
      ],
      "stock": "Склад 1",
      "customer": "Поставщик 3"
    },
    {
      "date": "04.03.19",
      "sd": [
        {
          "money": {
            "item": "Товар 1",
            "quantity": 2,
            "price": 4,
            "index": 1
          }
        },
        {
          "money": {
            "item": "Товар 2",
            "quantity": 3,
            "price": 4,
            "index": 2
          }
        },
        {
          "money": {
            "item": "Товар 1",
            "quantity": 2,
            "price": 5,
            "index": 3
          }
        }
      ],
      "stock": "Склад 1",
      "customer": "Поставщик 2"
    },
    {
      "date": "04.03.19",
      "sd": [
        {
          "money": {
            "item": "Товар 1",
            "quantity": 3,
            "price": 1,
            "index": 1
          }
        },
        {
          "money": {
            "item": "Товар 2",
            "quantity": 2,
            "price": 1,
            "index": 2
          }
        }
      ],
      "stock": "Склад 1",
      "customer": "Поставщик 2"
    },
    {
      "date": "14.03.19",
      "sd": [
        {
          "money": {
            "item": "Товар 2",
            "quantity": 1,
            "price": 2,
            "index": 1
          }
        }
      ],
      "stock": "Склад 1",
      "customer": "Поставщик 2"
    },
    {
      "date": "17.04.19",
      "sd": [
        {
          "money": {
            "item": "Товар 2",
            "quantity": 5,
            "price": 6,
            "index": 1
          }
        },
        {
          "money": {
            "item": "Товар 1",
            "quantity": 2,
            "price": 6,
            "index": 2
          }
        }
      ],
      "stock": "Склад 1",
      "customer": "Поставщик 1"
    },
    {
      "date": "21.02.19",
      "sd": [
        {
          "money": {
            "item": "Товар 3",
            "quantity": 1,
            "price": 22,
            "index": 1
          }
        }
      ],
      "stock": "Склад 2",
      "customer": "Поставщик 1"
    },
    {
      "date": "21.02.19",
      "sd": [
        {
          "money": {
            "item": "Товар 3",
            "quantity": 1,
            "price": 22,
            "index": 1
          }
        }
      ],
      "stock": "Склад 2",
      "customer": "Поставщик 1"
    },
    {
      "date": "20.02.19",
      "sd": [
        {
          "money": {
            "item": "Товар 3",
            "quantity": 1,
            "price": 22,
            "index": 1
          }
        }
      ],
      "stock": "Склад 2",
      "customer": "Поставщик 1"
    }
  ]
}


Вообще, при работе с JSON форму можно считать своего рода JSON-схемой. Так, к примеру, в IDE можно по заданному JSON сгенерировать форму, ну а открытие формы в структурированном представлении выполняет обратную операцию – по заданной форме сгенерировать JSON. Единственное, пока при работе со структурированными форматами не поддерживаются рекурсивные деревья (то есть, по сути, JSON-объекты с динамической глубиной), но их поддержка это вопрос времени. В остальном с использованием описанного выше механизма можно экспортировать / импортировать любой JSON файл.

Плоские форматы


В плоских форматах каждый файл объекта формы является таблицей, в которой:

  • Рядами являются объекты в базе этого объекта формы.
  • Колонками – свойства, объекты отображения которых равны этому объекту формы.

Как уже отмечалось ранее, плоские форматы, как правило, используются только для работы с простыми формами (то есть состоящими из одного объекта). Соответственно, чтобы не создавать такие формы явно, для работы с ними в платформе существует специальный синтаксический сахар, позволяющий создавать эти формы прямо по месту экспорта / импорта:
run() {
    EXPORT XLSX FROM item = upper(name(Item i)), currentBalance(i, Stock s), 
            stock = name(s), barcode(i), salePrice(i)
            WHERE (name(i) LIKE '%а%' OR salePrice(i) > 10AND currentBalance(i, s);
}
Отметим, что экспорт в плоском формате очень похож на SELECT в SQL. Более того, очень долго был соблазн назвать этот оператор также, но все же в конечном итоге было решено сохранить единообразие с экспортом формы, а также симметрию с импортами (как формы, так и плоским импортом).

Итого, не вдаваясь в детали, с представлениями форм разобрались, осталось совсем немного – разобраться с их открытием в этих представлениях.

Открытие формы


При открытии формы для любого ее объекта можно передать значение из контекста вызова, которое в зависимости от представления, в котором открывается форма, будет использовано следующим образом:

  • В интерактивном представлении – переданное значение будет установлено в качестве текущего объекта.
  • В статичном представлении – будет установлен дополнительный фильтр: объект должен быть равен переданному значению.
run(Genre g) {
    SHOW booksByGenre OBJECTS g=g;
    PRINT booksByGenre OBJECTS g=g;
    EXPORT booksByGenre OBJECTS g=g;
}
Передача объектов – основной (и практически единственный) общий механизм при открытии формы во всех ее представлениях. Остальные механизмы зависят от конкретного представления.

В интерактивном представлении (SHOW, DIALOG)


С точки зрения управления потоком существуют два режима открытия формы в интерактивном представлении:

  • Синхронный (WAIT) – ожидает момента, пока пользователь не закроет форму, и только после этого, записав все результаты выполнения, передает управление следующему за ним действию.
  • Асинхронный (NOWAIT) – передает управление следующему за ним действию сразу после открытия формы на клиенте.

По умолчанию форма открывается в синхронном режиме.

С точки зрения расположения формы, открываемая форма может быть показана двумя способами:

  • Как окно (FLOAT) – форма показывается в виде плавающего окна.
  • Как закладка (DOCKED) – форма открывается в виде закладки в системном окне System.forms.

По умолчанию в синхронном режиме работы форма показывается как окно, а в асинхронном – как закладка.

Но, наверное, самой важной возможностью этого оператора является его так называемый диалоговый режим (DIALOG). В этом режиме оператор позволяет вернуть последнее текущее значение заданного объекта (или, при необходимости, нескольких объектов), и тем самым, по сути, осуществить ввод значения.

Соответственно, также, как и в операторе ввода значения в поле (INPUT), в диалоговом режиме этого оператора можно задавать начальные значения объектов (через механизм передачи объектов), а также действия, которые будут выполняться в случае, если ввод был успешно завершен (пользователь нажал ОК), или, наоборот, отменен (пользователь нажал отмену).
FORM booksByGenre
     OBJECTS g = Genre PANEL
     PROPERTIES (g) name
     OBJECTS b = Book
     PROPERTIES (b) name
     FILTERS genre(b) = g
;

EXTEND FORM ordersByGenre
    PROPERTIES (o) nameBook
                    ON CHANGE {
                        DIALOG booksByGenre OBJECTS g = g, b = book(o) INPUT DO
                            book(o) <- b;
                    }    
;

В печатном представлении (PRINT)


При открытии формы в печатном представлении можно задать конкретный графический (или псевдографический) формат, в который будет преобразован сформированный JasperReports отчет: DOC, DOCX, XLS, XLSX, PDF, HTML, RTF и другие форматы, поддерживаемые JasperReports. Результирующий файл при этом можно как записать в заданное свойство, так и отправить его пользователю, где он, в свою очередь, сможет открыть его средствами ОС (а точнее, ассоциированной с заданным расширением программой).

Кроме того, в десктоп-клиенте можно открыть отчет в режиме предварительного просмотра (PREVIEW), в котором пользователь сможет самостоятельно выбрать, в какой формат экспортировать этот отчет и / или отправить его на печать. Ну и наконец, можно просто отправить отчет на печать, ни о чем не спрашивая пользователя.

В структурированном представлении (EXPORT, IMPORT)


Также, как и в печатном представлении, при открытии формы в структурированном представлении необходимо задать формат, в который будет экспортирована форма: XML, JSON, DBF, CSV, XLS, XLSX. Сформированный файл при этом записывается в заданное свойство.

Также, в отличие от графических представлений, для структурированного представления поддерживается операция, обратная операции открытия – импорт формы. Оператор импорта формы принимает на вход файл(ы) в структурированном формате, после чего разбирает его (их) и записывает полученные данные в свойства заданной формы таким образом, чтобы при экспорте этой формы назад в импортируемый формат получить исходный файл.

Так как оператор импорта – это, по сути, «оператор ввода», на импортируемую форму накладываются следующие ограничения:
  • Все объекты формы должны быть числовых или конкретных пользовательских классов.
  • Свойства на форме и фильтры должны иметь возможность изменения на заданное значение (то есть, как правило, быть первичными, хотя, к примеру, можно импортировать значение TRUE в свойство f(a) = b – в этом случае в f(a) запишется b)

Фильтры при импорте изменяются на значение TRUE (если быть точным, на значения по умолчанию классов значений этих фильтров, то есть 0 для чисел, пустые строки для строк и т.п., но, как правило, фильтры все же имеют значения логических типов).
// для импорта примитивных данных, для которых нужно найти объекты в системе
inn = DATA LOCAL BPSTRING[9] (Shipment);
barcode = DATA LOCAL BPSTRING[13] (ShipmentDetail);

FORM shipments
    OBJECTS s=Shipment EXTID 'shipments' // используем EXTID чтобы оставить короткое имя объекта s, но при экспорте его именем будет считаться shipments 
    PROPERTIES (s) number, date, inn
    OBJECTS sd=ShipmentDetail EXTID 'detail' // используем EXTID чтобы оставить короткое имя объекта sd, но при экспорте его именем будет считаться detail
    FILTERS shipment(sd) = s // автоматически заполнит shipment для detail
    PROPERTIES (sd) barcode, price, quantity
;

run() {
    FOR jsonFile = JSONFILE('\{ shipments : [ ' + // jsonFile должен / может передаваться в параметре run, {} надо escape'ить так как фигурные скобки используются в интернационализации
                ' \{number : "13423", date : "01.01.2019", inn : "2", detail : [\{ barcode : "141", quantity : 5, price : 10 \}, \{ barcode : "545", quantity : 2, price : 11 \}] \},' + 
                ' \{number : "12445", date : "01.02.2019", inn : "1", detail : [\{ barcode : "13", quantity : 1, price : 22 \}] \} ]\}')
                 DO {
        IMPORT shipments FROM jsonFile; // тип импорта автоматически определяется из типа файла
        FOR BPSTRING[9] inn = inn(Shipment s) DO { // для всех принятых inn
            customer(s) <- legalEntityINN(inn); // в поставку записываем клиента с принятым INN
            stock(s) <- GROUP MAX st AS Stock; // запишем какой-нибудь склад (с максимальным id) 
        }        
        FOR barcode(Item item) = barcode(ShipmentDetail sd) DO // еще один способ связать примитивные данные с объектными
            item(sd) <- item;
            
        APPLY;
        exportString() <- IF canceled() THEN applyMessage() ELSE 'Накладная записана успешно';
   }
}

Навигатор


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

Навигатор в каком-то смысле является весьма специфической разновидностью интерактивного представления формы, адаптированной под быструю и удобную работу с иерархией элементов в режиме большого окна (рабочего стола). Соответственно, отличительной особенностью навигатора является то, что он может отображать только действия без параметров (самыми частыми из которых являются открытия формы). Плюс дизайн навигатора также сильно отличается от дизайна формы.

Дизайн навигатора


В навигаторе все элементы выстраиваются в иерархию. Помимо действий элементами навигатора могут быть папки, основная функция которых — группировать общий функционал в одном месте.

Непосредственно дизайн навигатора состоит из множества окон – компонентов рабочего стола, каждый из которых отображает некоторые элементы навигатора. Соответственно, каждому элементу навигатора можно задать, в какое окно должны рисоваться его потомки. Таким образом, для каждого окна однозначно определяется множество поддеревьев элементов навигатора, которое в нем отображается.



Каждое окно занимает предопределенный участок рабочего стола.



Весь рабочий стол имеет размеры 100x100 точек. При создании окна необходимо указать левую верхнюю координату, ширину и высоту окна, выраженную в точках. Желательно, чтобы окна «накрывали» всю область рабочего стола. Если этого не происходит, то свободная область будет отдана одному из окон (не гарантируется какому именно). Также допускается, чтобы два окна имели абсолютно идентичные координаты и размеры. В таком случае они будут отображаться на одном и том же месте, но переключение между ними будет идти при помощи вкладок.

В любой момент времени в каждом окне может быть одна текущая выбранная пользователем папка навигатора. Соответственно, если для элемента навигатора какая-то из его верхних по иерархии папок находится в другом окне, и при этом не выбрана, то этот элемент навигатора не показывается.

Также в платформе помимо всего вышеперечисленного:
  • для окон можно задавать типы графических компонент, который будут использоваться для отображения в них элементов навигатора – тулбар, панель, дерево или меню.
  • существует несколько встроенных системных окон – forms, log, status, root, toolbar, tree, которые можно использовать в различных типовых задачах (например, root, как правило, используют для разбиения действий на модули)
FORM items;
FORM stocks;
FORM legalEntities;
FORM shipments;
hello()  { MESSAGE 'Hello world'; }
hi()  { MESSAGE 'Hi'; }

NAVIGATOR {
    NEW FOLDER catalogs 'Справочники' WINDOW toolbar { // создаем новую папку навигатора и делаем, чтобы все ее потомки отображались в окно с вертикальным тулбаром
        NEW items; // создаем в папке элемент-форму для формы items, имя элемента по умолчанию равняется имени формы 
    }
    catalogs {  // инструкция редактирования элемента навигатора
        NEW FORM stocksNavigator 'Склады' = stocks; // создаем элемент-форму stocksNavigator для формы stocls и добавляем в папку catalogs последним элементом
        NEW legalEntities AFTER items; // создаем элемент-форму с именем legalEntities в папку catalogs непосредственно за элементом items
        NEW shipments;
    }
    NEW FOLDER documents 'Документы' WINDOW toolbar { // создаем еще одну папку, элементы которой будут также отображаться в окно с вертикальным тулбаром
                                                      // сами папки будут отображаться в окне root, и при выборе одной из них в окне с вертикальным 
                                                      // тулбаром будут показаны потомки именно этой папки
        NEW ACTION hi;   // создаем элемент-действие
        NEW ACTION h=hello;   // создаем элемент-действие
        MOVE shipments BEFORE h; // инструкция перемещения элемента shipments из папки catalogs в папку document перед элементом hello     
    }
}
Пример навигатора


Вообще, навигатор – это большой синтаксический сахар, который позволяет в несколько строк из коробки получить готовое к эксплуатации решение, и такой функционал, также как и управление формой пользователем, как правило, присутствует только в ERP-платформах. Как следствие, у многих людей, читающих статью про «язык программирования», может возникнуть закономерный вопрос: навигатор в языке? Серьезно? Почему не в библиотеке? Ну, во-первых, lsFusion идеологически language-based (как SQL или ABAP), а не library-based (как Java или 1C) язык / платформа. Поэтому, если учесть, что навигатор – не такая уж domain-specific абстракция и присутствует в том или ином виде во всех информационных системах, включение его в язык не кажется настолько нелогичным решением. Во-вторых, я, если честно, никогда не понимал, чем изучение библиотеки проще изучения синтаксической конструкции. Я бы даже сказал наоборот: языковой интерфейс общения, по идее, должен быть более понятен человеку, так как используется им в повседневной жизни. Но, видимо, у многих просто есть аллергия на новые языки, слишком уж много их развелось в последнее время (даже когда в этом скорее всего не было никакой необходимости).

На этом с логикой представления закончим. В следующей статье перейдем уже к физической модели – всему, что связано:

  • с разработкой больших проектов: пространства имен, типизация, модульность, интеграция, метакоды, миграция и интернационализация;
  • с оптимизацией производительности проектов с большим количеством данных: материализации, таблицы и индексы.

Подытоживая все вышесказанное, хотелось бы обратить внимание на следующие три особенности платформы:

  • Одна общая логика формы для всех ее представлений. Это значительно упрощает обучение разработке всех «классических» элементов логики представлений: интерактивных форм, отчетов, экспортов / импортов. Так, человек, освоив всего один механизм, например, интерактивных форм, может практически сразу же заниматься и разработкой отчетов, и интеграцией с внешними системами.
  • Возможность обращения сервера к клиенту. Это позволяет собрать весь control flow в одном месте без этого избыточного разделения логики на сервер и клиент. Впрочем, этому вопросу уделим больше внимания потом – в сравнении с другими технологиями.
  • Полная реактивность и работа с данными на SQL сервере (без ORM). Эту особенность мы уже упоминали, когда рассказывали про интерактивное представление, но хотелось обратить внимание на нее еще раз, так как на самом деле эта особенность является очень важной при разработке сложных информационных систем с большим количеством данных.

Заключение


Как очень часто бывает, вторые части не всегда получаются такими интересными, как первые (хотя, скорее всего, и первая была не такой интересной, в конце концов, это tutorial, и такой материал, если его нужно уместить в разумный объем, по определению будет достаточно суховатым). Но, как уже упоминалось во вступлении, целью этой статьи (как и первой) не была «продажа», целью было дать возможность читателю составить хотя бы частичную картину того, что из себя представляет разработка на lsFusion.

Также необходимо иметь ввиду, что все, написанное в этой статье – это далеко не все возможности платформы. Многие вещи приходилось пропускать, некоторые упрощать для лучшего понимания. Как следствие, скорее всего, по завершению основного цикла статей будет еще одна статья «Не очередной язык программирования. За кадром.», где я соберу все то, что по разным причинам не попало в первые три статьи (хотя по-хорошему должно было попасть).

Что еще хотелось бы отметить. По опыту предыдущей статьи у значительного числа людей первая реакция на многие заявляемые вещи: «Это невозможно реализовать в общем случае». Ну и далее часто следовала просьба рассказать в двух словах, как именно нам удалось это сделать. Так вот, проблема в том, что реализация платформы lsFusion по сложности сопоставима с реализацией современных SQL-серверов. Чтобы заставить работать весь заявленный функционал на больших объемах, используется множество оптимизаторов, причем работают они в связке, как в самолете – каждый верхний оптимизатор страхует нижний, если что-то пошло не так, и таких уровней оптимизации там не менее шести. Причем многие решают те же задачи, которые по идее должны были бы решать сами SQL-сервера (как например в недавней статье). Соответственно, рассказать про это все вкратце попросту невозможно. Мы, конечно, обязательно расскажем обо всех этих механизмах подробнее (в том числе о том, как ими можно управлять), но сделаем это чуть позже, после того, как закончим с описанием функционала и сравнением с другими технологиями. То есть сначала расскажем «Что?», потом «Почему не ...?» и только потом «Как?».

UPD: Третью часть статьи можно найти тут.
lsFusion
45,16
Не очередной язык программирования
Поделиться публикацией

Комментарии 4

    0
    Спасибо за интересную статью.

    БД в вашей архитектуре берет на себя нагрузку по хранению данных и метаданных, обеспечению ссылочной целостности, рендерингу страниц, согласно метаданным.
    Т.е. несколько слоев завязаны на БД.
    Вопросы по БД:
    1. Как справляется базка при большой нагрузке?
    2. Реализовано ли кэширование данных для снижения нагрузки?
    3. Есть ли разграничение прав доступа при формировании запросов к БД?
    4. Есть ли защита от дурака, когда некорректный JOIN или DELETE без фильтра может «положить» среду?


    Вопросы по базовым компонентам и среде:
    1. Можно-ли расширять список базовых компонентов?
    2. Можно-ли расширять список свойств у базовых компонентов?
    3. Можно-ли добавлять кастомный код обработки на клиентской строне (JavaScript)?
    4. Можно-ли добавлять кастомный код обработки на серверной строне, и если да, то какой используется язык?

      0
      Как справляется базка при большой нагрузке?

      Я бы сказал, это одно из самых важных преимуществ платформы. Собственно у нас основные клиенты сейчас — FMCG-сети именно потому, что там большие объемы данных и сложная логика, и там скажем ORM'ы очень плохо выживают.
      Реализовано ли кэширование данных для снижения нагрузки?

      Кэшируется все, что связано с изменениями в сессии. Сами данные базы не кэшируются а) за это отвечают всякие shared buffers в самих СУБД, б) непонятно как ACID (в частности MVCC) поддерживать
      Есть ли разграничение прав доступа при формировании запросов к БД?

      Политика безопасности поддерживается на уровне платформы. Там много разных настроек, на уровне СУБД пока ничего не проверяется, если вы об этом. И если честно не совсем понятно зачем это делать.
      Есть ли защита от дурака, когда некорректный JOIN или DELETE без фильтра может «положить» среду?

      Напрямую к базе никто запросы не формирует. Другое дело, что никто не запретит написать invoice(InvoiceDetail id) < — NULL. Но обычно в сложных логиках существует вагон ограничений и шансов что такое действие выполнится 0. Да, теоретически запрос может повиснуть, но это обычно приводит к тому, что одно ядро проца забивается, но не более. Но для всего этого есть монитор процессов где видны все залипшие запросы и это очень быстро диагностируется.
      Можно-ли расширять список базовых компонентов?

      Пока нет, так как поддерживается и десктоп и веб-клиент, и непонятно даже на каком языке это делать. Но вообще в следующей версии скорее всего перейдем на схему с генерацией реакта и там можно будет любые базовые компоненты Reacta добавлять.
      Можно-ли расширять список свойств у базовых компонентов?

      Честно говоря не до конца понял вопрос. Но возможно в контексте предыдущего ответа это не актуально.
      Можно-ли добавлять кастомный код обработки на клиентской строне (JavaScript)?

      Как ни парадоксально, но наоборот можно только в десктоп-клиенте на Java (добавляется этот код все равно на сервер, где класс сериализуется, пересылается на десктоп-клиента и там выполняется). Но даже в этом случае идеологически не предполагается прямого изменения компоненты (этот код в основном используется для работы с оборудованием), предполагается, что все работает через реактивность как в React. То есть данные изменяются на сервере, клиент это чисто рендерер и генератор событий.
      Можно-ли добавлять кастомный код обработки на серверной строне, и если да, то какой используется язык?

      Да, естественно, Java. Создается Java-класс MyAction, дальше регистрируется myAction INTERNAL 'my.java.MyAction'; и этот action выполняется как будто он на lsFusion написан. Ну и через DI можно на сервере приложений инстанцировать свои объекты (например сервера какие-нибудь).
        0
        Спасибо за ответы.

        >> Можно-ли расширять список свойств у базовых компонентов?
        > Честно говоря не до конца понял вопрос.
        > Но возможно в контексте предыдущего ответа это не актуально.
        Может быть и актуально: если UI генерится через метаданные и есть логическое разделение между метаданными и базовыми элементами, которые отрабатывают переданные им настройки, то можно расширять фукнционал базовых элементов путем расширения метаданных и обработки доп. свойст на стороне базовых элементов.

        >> Можно-ли добавлять кастомный код обработки на серверной строне, и если да, то какой используется язык?
        > Да, естественно, Java.
        Помимо Action, есть ли возможность «врезаться» в базову логику ядра?
        Т.е. добавить кастомную логику в события ядра системы.
        Например, стандартный обработчик изменения данных по сабмиту формы — событие изменение данных, перед сохранением: перед сохранением нужно сделать проверку на имя таблицы и сделать доп. инициализацию некоторых полей перед записью в таблицу.
        Такое возможно?
          0
          Может быть и актуально: если UI генерится через метаданные и есть логическое разделение между метаданными и базовыми элементами, которые отрабатывают переданные им настройки, то можно расширять фукнционал базовых элементов путем расширения метаданных и обработки доп. свойст на стороне базовых элементов.

          UI генерится через метаданные, но логического разделения между базовыми элементами как такового нет: а) для упрощения разработки, б) так как там как минимум 2 вида клиента один на Java, второй на JavaScript (фактически он на самом деле тоже на Java который через Gwt компайлится в JavaScript).

          Это разделение возникнет при добавлении React схемы (описанной в статье), когда платформа будет генерить React-дизайн, который потом можно менять (по аналогии с отчетами). Но даже в этом случае логично с дополнительными «настройкам» работать как с данными. Например как сейчас в отчетах, надо сделать выделение цветом, просто делается свойство на форме background = IF smth(a) THEN RGB(4,3,5) ELSE RGB(12,4,5) и дальше этот background можно использовать в самом отчете (jrxml).
          Помимо Action, есть ли возможность «врезаться» в базову логику ядра?
          Т.е. добавить кастомную логику в события ядра системы.
          Например, стандартный обработчик изменения данных по сабмиту формы — событие изменение данных, перед сохранением: перед сохранением нужно сделать проверку на имя таблицы и сделать доп. инициализацию некоторых полей перед записью в таблицу.
          Такое возможно?

          Есть набор стандартных событий, в том числе и ON OK:
          EVENTS
              ON OK { posted(i) <- TRUE; }, // указываем, что при нажатии пользователем OK должно выполняться действия, которое выполнит действия по "проведению" данного инвойса

          С добавлением своих событий, такая же ситуация как и с добавлением своих базовых компонент (в React'е все что угодно, в текущей схеме непонятно где и на каком языке). Хотя никто не мешает fork'уть репозитарий и добавить любое событие — собственно после этого mvn clean package -P assemble и можно после этого использовать свою jar вместо jar с центральных репозитариев. Но это уже конечно хардкор.

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

        Самое читаемое