Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В различных приложениях регулярно возникает задача по поддержке логики изменения во времени некоторого атрибута объекта относительно некоторого субъекта (или субъектов). Например, это может быть изменение розничной цены товара в магазинах или показателей KPI для сотрудников.
В этой статье я покажу, какую доменную логику и интерфейсы можно построить для решения этой задачи. Сразу оговорюсь, что речь будет касаться именно управленческого воздействия пользователем на атрибут, а не отражения исторического изменения.
Реализация будет представлена на базе открытой и бесплатной платформы lsFusion, но подобную схему можно применять и при использовании любой другой технологии.
Введение
Для более простого изложения и понимания статьи в качестве атрибута возьмем цену, в качестве объекта — товар, а субъектом будет склад. При этом минимальным возможным интервалом задания атрибута будет дата. Таким образом, пользователь сможет определять, какая будет на конкретную дату цена для любого товара и склада.
Схема ввода пользователем изменений цены будет похожа на ту, которая используется в классических системах контроля версий. Любое изменение, с точки зрения доменной логики, будет представлять собой один коммит, на основе которых будет высчитываться состояние на определенную дату. Во многих предметных областях такие коммиты называют документами или транзакциями. В данном случае, под этим коммитом будем подразумевать так называемый прайс-лист. В каждом прайс-листе будут задаваться товары и склады, которые в него входят, а также период действия.
Описанная схема имеет следующие преимущества:
- Атомарность. Каждое изменение оформлено как отдельный документ. Таким образом эти документы можно временно сохранять, но не проводить. При ошибочном вводе легко откатить все изменение.
- Прозрачность. Легко определить, кто и когда сделал изменение, а также указать причину, внеся ее в комментарий к документу.
Основное отличие от системы контроля версий в том, что коммиты в явную не зависят друг от друга. Таким образом, можно относительно безболезненно удалять все коммиты в любой момент времени. Кроме того, у каждого такого коммита может быть задана окончания, когда он перестает действовать, чего конечно же нету в системе контроля версий.
Реализация
Определение доменной логики начнем со складов. Немного усложним решение, объединив склады в иерархию группы динамической глубины. По какому принципу это делается описано в соответствующей статье, поэтому просто приведу фрагмент кода, который объявляет группы и создает формы по их редактированию:
Объявление групп складов
CLASS Group 'Группа складов'; |
Пример иерархии групп
Дальше объявим склады, которые могут быть привязаны к любой из групп:
Объявление складов
CLASS Stock 'Склад'; |
Пример складов
И, наконец, объявим логику товаров:
Объявление товаров
CLASS Product 'Товар'; |
Пример товаров
Перейдем непосредственно к созданию логики прайс-листов. Сначала зададим сам класс Прайс-лист, а также период его действия:
CLASS PriceList 'Прайс-лист'; |
Добавим событие, которое будет при создании прайс-листа автоматически проставлять текущей дату, с которой он начнет действовать.
WHEN LOCAL SET(PriceList p IS PriceList) DO |
Затем добавим пользователя, который его создал, и время создания:
createdTime 'Время создания' = DATA DATETIME (PriceList); |
WHEN SET(PriceList p IS PriceList) DO { |
Дальше создадим строки прайс-листа, в которых будут заданы товары и цены:
CLASS PriceListDetail 'Строка прайс-листа'; |
Для последующего использования создадим свойства, которые будут определять период действия строк прайс-листов:
fromDate 'Дата с' (PriceListDetail d) = fromDate(priceList(d)); |
dataIn 'Вкл' = DATA BOOLEAN (PriceList, Group); |
in 'Вкл (итого)' (PriceList p, Group child) = |
dataIn 'Вкл' = DATA BOOLEAN (PriceList, Stock); |
in 'Вкл' (PriceList p, Stock s) = dataIn(p, s) OR in(p, group(s)); |
stocks 'Склады' (PriceList p) = CONCAT ' / ', |
priceListDetail (Product p, Stock s, DATE dt) = |
На основе полученной строки прайс-листа определим значение цены и ее период действия:
price 'Цена' (Product p, Stock s, DATE dt) = price(priceListDetail(p, s, dt)); |
Дальше перейдем к построению пользовательского интерфейса. Сначала нарисуем форму по редактированию прайс-листа. Создаем форму и добавляем туда “шапку” документа:
FORM priceList 'Прайс-лист' |
EXTEND FORM priceList |
EXTEND FORM priceList |
Настраиваем дизайн формы, чтобы товары и склады рисовались в отдельные вкладки:
DESIGN priceList { |
Осталось построить основную форму по управлению ценами. Она будет состоять из двух вкладок. На первой будет показываться список всех прайс-листов (по аналогии со списком коммитов). На второй вкладке будут отображаться текущие цены по конкретному складу на выбранную дату.
Для реализации первой вкладки добавим на форму список прайс-листов с строками для быстрого предпросмотра:
FORM managePrices 'Управление ценами' |
EXTEND FORM managePrices |
Дальше добавляем на форму список товаров, для которых есть действующие цены по складу на выбранную дату:
EXTEND FORM managePrices |
Для того, чтобы пользователь понимал откуда взялась такая цена, добавим вниз список строк прайс-листов с подходящими товарами и складами:
EXTEND FORM managePrices |
Также, для удобства пользователя, добавим возможность сразу из этой истории открывать форму редактирования соответствующего прайс-листа в новой сессии:
edit (PriceListDetail d) + { edit(priceList(d)); } |
И, наконец, формируем итоговый дизайн формы:
DESIGN managePrices { |
Результат
Рассмотрим основные варианты использования получившейся логики.
Предположим, у нас есть два отдельных прайс-листа на разные группы товаров. Тогда, в зависимости от выбранного склада во вкладке с ценами, будут показываться только товары из соответствующих прайс-листов:
Теперь создадим новый прайс-лист с ограниченным периодом действия, урезанным списком складов и новой ценой. На второй вкладке, если мы выберем дату в интервале действия нового прайс-листа, то получим из него новую цену. Как только период действия закончится, то опять вернется старая цена из исходного прайса:
С помощью этого же механизма можно “отменять” действие конкретных цен с определенной даты. Например, если ввести новый прайс, не указав при этом цену, то получится, что цена сбросится, и товар пропадет из фильтра. При этом при удалении введенного документа все возвращается к старому состоянию:
Полученное свойство с ценой товара по складу на дату можно в дальнейшем использовать в различных событиях или других формах. Например, можно сделать автоматическое проставление цены в заказе на основе этой логики определения цены:
WHEN LOCAL CHANGED(sku(UserOrderDetail d)) OR CHANGED(stock(d)) OR CHANGED(dateTime(d)) DO |
При желании можно сделать редактируемой колонку с ценой на вкладке с текущими ценами и добавить кнопку, которая будет создавать новый коммит для измененных цен.
Заключение
В решении на уровне платформы не используются ни справочники, ни документы со строками, ни регистры, ни отчеты и прочие лишние абстракции. Все сделано исключительно на понятиях классов и свойств. Отметим, что эта достаточно сложная логика была реализована приблизительно в 150 значащих строках кода на lsFusion. Реализовать ее в такой же постановке в других платформах (например, 1С) является значительно более сложной задачей.
Описанная выше схема широко используется в ERP-решении на базе lsFusion. При помощи нее с различными модификациями поддерживаются прайс-листы поставщиков, управленческие розничные цены, акции и многие другие управленческие параметры.
Шаблон может быть усложнен путем добавления в документ нескольких субъектов (например, к складу может быть добавлен поставщик), а также определения сразу нескольких атрибутов в одном документе. В частности, можно добавить сущность Вид цены, а в строке документа задавать цену для кортежа строки и соответствующего вида цен. В описанную выше логику нужно будет просто добавить несколько дополнительных параметров в некоторые свойства.
При помощи нескольких дополнительных строк кода существует возможность денормализовать все записи изменений в одну таблицу, на которой построить соответствующий индекс. Тогда выборка любого значения на любую дату будет произведена за логарифмическое время. Такая оптимизация необходима тогда, когда в этой таблице будет находится несколько сот миллионов записей.
Построенный пример можно попробовать онлайн на соответствующей странице сайта (раздел Платформа). Вот исходный код целиком, который нужно вставить в нужное поле:
Исходный код
REQUIRE Authentication, Time; |