image

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

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

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

Для начала по стандартной схеме объявим сущность Группа товаров в виде простого плоского класса с формами редактирования и списка:
CLASS Group 'Группа';
name 'Имя' = DATA ISTRING[50] (Group);

FORM group 'Группа'
   OBJECTS g = Group PANEL
   PROPERTIES(g) name
  
   EDIT Group OBJECT g
;

FORM groups 'Группы'
   OBJECTS g = Group
   PROPERTIES(g) READONLY name
   PROPERTIES(g) NEWSESSION NEWEDITDELETE
  
   LIST Group OBJECT g
;

NAVIGATOR {
   NEW groups;
}

Теперь сделаем из групп иерархию. Для этого введем свойство, которое будет содержать ссылку на родительскую группу:
parent = DATA Group (Group);
nameParent 'Родительская группа' (Group g) = name(parent(g));

Дальше сделаем свойство, которое будет рекурсивно определять связь между двумя группами:
level 'Уровень' (Group child, Group parent) =
   RECURSION 1l IF child IS Group AND parent = child
        STEP 2l IF parent = parent($parent) MATERIALIZED;

По какому именно принципу работает оператор RECURSION, в данной статье я описывать не буду, но свойство level будет возвращать 2 в степени “длина пути между child и parent в соответствующем направленном дереве”. MATERIALIZED указывает, что платформа должна хранить его в отдельной таблице, где для каждой пары связанных узлов будет отдельная запись со значением level в соответствующей колонке. При любом изменении структуры дерева эта таблица будет автоматически пересчитываться.

Например, для вот такого дерева:

image

Таблица будет выглядеть вот так:

image

В ней key0 — это код потомка, а key1 — код родителя. Количество записей в этой таблице будет приблизительно равно количеству групп, умноженное на среднюю глубину дерева. Такая схема хранения будет полезна тем, что при необходимости считать всех потомков группы не придется прибегать к CTE запросам, а можно будет воспользоваться обычным JOIN к этой таблице.

Дальше на основании построенного свойства можно высчитать каноническое имя группы:
canonicalName 'Каноническое имя' (Group group) =
   GROUP CONCAT name(Group parent), ' / ' ORDER DESC level(group, parent) CHARWIDTH 50;

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

Теперь расширим форму просмотра и редактирования групп вновь созданными свойствами:
EXTEND FORM group
   PROPERTIES(g) nameParent, canonicalName
;

EXTEND FORM groups
   PROPERTIES(g) READONLY nameParent, canonicalName
;

Форма со списком групп в плоском виде будет выглядеть следующим образом:

image

После того, как логика групп завершена, добавляем сущность Товар:
CLASS Product 'Товар';
name 'Имя' = DATA ISTRING[50] (Product);

Создаем ссылку товара на группу товара, к которой он относится:
group 'Группа' = DATA Group (Product);
canonicalNameGroup 'Каноническое имя группы' (Product p) = canonicalName(group(p));

Наконец, сделаем форму по вводу товаров, в которой будет два элемента: дерево групп и список товаров. Для выбранной группы дерева в списке будут отображаться только товары, которые принадлежат любому потомку выбранного узла. Сначала объявим форму и добавим на нее дерево со списком групп:
FORM products 'Товары'
   TREE groups g = Group PARENT parent
   PROPERTIES READONLY name(g)
;

При помощи команды TREE создается дерево из объектов класса Group, иерархия которых определяется по ранее созданному свойству parent.

Добавляем форму в навигатор:
NAVIGATOR {
   NEW products;
}

В данном примере, ввод и редактирование товаров будут осуществляться не через отдельные диалоги, а непосредственно в самой форме. Для этого создадим действие по созданию товара с привязкой к выбранной группе:
newProduct 'Добавить' (Group g) {
   NEW p = Product {
       group(p) <- g;
   }
}

Теперь, на созданную ранее форму, добавляем список товаров с редактируемыми колонками:
EXTEND FORM products
   OBJECTS p = Product
   PROPERTIES(p) name, canonicalNameGroup
   FILTERS level(group(p), g)
;

Закидываем на форму кнопки по добавлению и удалению товаров:
EXTEND FORM products
   PROPERTIES newProduct(g) DRAW p TOOLBARDELETE(p)
;

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

Осталось настроить дизайн, чтобы дерево отображалось слева, а список товаров — справа, а между ними был разделитель, при помощи которого можно изменять размеры объектов:
DESIGN products {
   OBJECTS {
       NEW pane {
           type = SPLITH;
           fill = 1;
           MOVE BOX(TREE groups);
           MOVE BOX(p);
       }
   }
}

Итоговая форма будет выглядеть следующим образом:

image

После того, как иерархия товаров и групп создана, часто возникает потребность в задании некоторого параметра на любом из уровней. При этом, чем на более низком уровне иерархии он задан, тем его значение приоритетнее. Например, если для группы Молочная продукция задано значение 30, а для группы Молоко — 20, то выбраться должно последнее из них.

Предположим нужно таким образом определить параметр Надбавка. Для этого нужно сначала создать соответствующее свойство для группы:
markup 'Надбавка, %' = DATA NUMERIC[10,2] (Group);

Для того, чтобы найти нужное значение, достаточно просто воспользоваться группировкой с выбором последнего значения:
parentMarkup 'Надбавка (от верхней группы), %' (Group child) =
   GROUP LAST markup(Group parent) ORDER DESC level(child, parent) WHERE markup(parent);

Если переводить на обычный язык, то это выражение находит (GROUP) последнюю (LAST) надбавку (markup) по верхней группе (Group parent), в порядке убывания расстояния до нее (ORDER DESC level(child, parent)), для которой эта надбавка задана (WHERE markup(parent)). Здесь хочется отметить, насколько язык lsFusion соответствует естественному языку.

Добавим созданные выше свойства на форму с товарами в дерево групп:
EXTEND FORM products
   PROPERTIES (g) markup, parentMarkup READONLY
;

Предположим, что существует потребность задавать надбавку непосредственно для товара, и чтобы она была приоритетнее надбавки для группы. Для этого сначала создаем первичное свойство для товара:
dataMarkup 'Надбавка по товару, %' = DATA NUMERIC[10,2] (Product);

Затем объявляем свойство, которое будет возвращать надбавку от товара, если она задана, или надбавку от группы:
markup 'Надбавка, %' (Product p) = OVERRIDE dataMarkup(p), parentMarkup(group(p));

После этого добавляем оба свойства на форму:
EXTEND FORM products
   PROPERTIES(p) dataMarkup, markup READONLY
;

Механизм задания надбавок для групп и товаров будет выглядеть следующим образом:

image

Заключение


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

Исходный код
CLASS Group 'Группа';
name 'Имя' = DATA ISTRING[50] (Group);

FORM group 'Группа'
    OBJECTS g = Group PANEL
    PROPERTIES(g) name
    
    EDIT Group OBJECT g
;

FORM groups 'Группы'
    OBJECTS g = Group
    PROPERTIES(g) READONLY name
    PROPERTIES(g) NEWSESSION NEWEDITDELETE
    
    LIST Group OBJECT g
;

NAVIGATOR {
    NEW groups;
}

parent = DATA Group (Group);
nameParent 'Родительская группа' (Group g) = name(parent(g));

level 'Уровень' (Group child, Group parent) = 
    RECURSION 1l IF child IS Group AND parent = child
         STEP 2l IF parent = parent($parent) MATERIALIZED;

canonicalName 'Каноническое имя' (Group group) = 
    GROUP CONCAT name(Group parent), ' / ' ORDER DESC level(group, parent) CHARWIDTH 50;

EXTEND FORM group
    PROPERTIES(g) nameParent, canonicalName
;

EXTEND FORM groups
    PROPERTIES(g) READONLY nameParent, canonicalName
;

CLASS Product 'Товар';
name 'Имя' = DATA ISTRING[50] (Product);

group 'Группа' = DATA Group (Product);
canonicalNameGroup 'Каноническое имя группы' (Product p) = canonicalName(group(p));

FORM products 'Товары'
    TREE groups g = Group PARENT parent
    PROPERTIES READONLY name(g)
;

NAVIGATOR {
    NEW products;
}

newProduct 'Добавить' (Group g) {
    NEW p = Product {
        group(p) <- g;
    }

EXTEND FORM products
    OBJECTS p = Product
    PROPERTIES(p) name, canonicalNameGroup
    FILTERS level(group(p), g)
;

EXTEND FORM products
    PROPERTIES newProduct(g) DRAW p TOOLBARDELETE(p)
;

DESIGN products {
    OBJECTS {
        NEW pane {
            type = SPLITH;
            fill = 1;
            MOVE BOX(TREE groups);
            MOVE BOX(p);
        }
    }
}

markup 'Надбавка, %' = DATA NUMERIC[10,2] (Group);

parentMarkup 'Надбавка (от верхней группы), %' (Group child) = 
    GROUP LAST markup(Group parent) ORDER DESC level(child, parent) WHERE markup(parent);

EXTEND FORM products
    PROPERTIES (g) markup, parentMarkup READONLY
;

dataMarkup 'Надбавка по товару, %' = DATA NUMERIC[10,2] (Product);
markup 'Надбавка, %' (Product p) = OVERRIDE dataMarkup(p), parentMarkup(group(p));

EXTEND FORM products
    PROPERTIES(p) dataMarkup, markup READONLY
;

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

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