- PVSM.RU - https://www.pvsm.ru -
Декларативный подход и MDA ахритектура имеют целый ряд преимуществ, позволяющих существенно сократить издержки на разработку и поддержку информационных систем (ИС: CRM, WMS, Project Management, etc). Этот подход уже используется в ряде продуктов (таких как 1С, например). Тем не менее, декларативный подход в них используется для решения слишком узкого круга задач. В этой статье мы рассмотрим преимущества декларативного подхода, покажем как можно значительно расширить область его применения в построении ИС, проверим построенную модель на реальных задачах и продемонстрируем работу прототипа.
Моя бакалаврская и магистерская были связаны с MDA, а применением этих идей к построению информационных систем мы с бывшим одногруппником занимаемся уже год. Мы не представляем никакой коммерческий продукт, все что мы сделали/придумали, разрабатывалось «на коленках» в свободное от работы время.
Наши идеи могут быть применены, как в сложных конструкторах информационных систем (таких как 1С), так и в веб-фреймворках (Django, RoR). Интересно узнать ваше мнение и замечания. Кроме того, мы ищем фирмы, которых заинтересует сотрудничество с целью использования наших наработок в своих продуктах.
Все началось с того, что совершенно случайно возникла задача по созданию небольшой CRM заточенной под воркфлоу конторы. Я стал смотреть на разные решения, от MS Access, до веб-фреймворков (Django, RoR). Но все они оказались слишком низкоуровневыми. Мне хотелось работать в терминах «пользователей», «событий», «задач» и т.п., а не объектов БД, и иметь соответствующие визуальные представления (календари, списки с фильтрами и т.д.).
Для разработки информационных систем чаще всего используется объектно-реляционная нотация (OORDBMS, ORM), в рамках которой бизнес-правила описываются императивно на одном из ЯП. Мы предлагаем расширить объектно-реляционную нотацию новыми понятиями, что позволит декларативно описывать бизнес-правила. Мы стремимся максимально приблизить язык «реализации» к языку формализации требований.
Использование высокоуровневых абстракций вкупе с декларативным подходом позволяет не только упростить и ускорить разработку и адаптацию ИС. Перенос бизнес-правил из уровня кода в уровень данных, качественно влияет не только на процесс разработки, но и на свойства продукта:
Последний пункт стоит отметить особо — на данный момент «конфигурабельность» облачных решений остается на крайне низком уровне — в лучшем случае можно добавить несколько своих атрибутов к уже существующим сущностям. Перенос бизнес-правил в область данных, позволяет для разных пользователей сервиса иметь эти данные совершенно различными, без вмешательства в работу сервера. Каждый пользователь в облаке сможет иметь систему, адаптированную именно для него.
Рассмотрим набор характерных задач, которые встают при проектировании ИС и от которых мы отталкивались при построении языка:
Рассмотрим основные понятия, которые мы добавляем в объектно-реляционную модель:
Классы уже существуют в объектно-реляционном языке, однако мы это понятие существенно изменили (расширили и урезали одновременно), приблизив его к понятию «сущность» в ER [1] нотации:
Примерами «включений» являются связи: класс и его поля, таблица и ее атрибуты, пользователь и его профайл. Объект «поле класса» не является самостоятельным, он не может существовать вне своего класса-родителя.
Возможность задавать выражение на XPath-похожем языке в качестве значения для поля объекта. Понятие вычислимых выражений и вычислимых полей используется во всех остальных расширениях нашего языка. Необходимо отметить, что пользователи ORM фреймворков или PL/SQL используют возможности своего языка при реализации get/set методов, но это совсем не тоже самое, т.к. информация о вычислении описывается в тьюринг-полном языке. Мы специально упрощаем язык выражений до не-тьюринг-полного, чтобы:
Таким образом, информация о способе вычислении поля содержится в описании структуры объекта, что позволяет по этой структуре автоматически
Интерфейсы хорошо известны в ООП, однако несправедливо забыты при описании баз данных. Введем интерфейсы следующим образом:
Понятие «предоставления интерфейса» является важным инструментом повторного использования. Его, так же как реализацию интерфейса, можно сравнить с реляционными представлениями. Существенным отличием «предоставления» от представления является способ вычисления: раз один и тот же интерфейс предоставлять могут разные сущности и не по одному разу, то получить список всех «предоставлений» одним SELECT-ом крайне затруднительно (например, чтобы отобразить все события на календаре).
Рассмотрим как приведенные выше задачи реализуются с помощью введенных нами конструкций:
Отображать стоимость как произведение цены на количество, цену с учетом НДС
Это полностью ложится на понятие «вычислимые поля», а язык описания выражений, безусловно, позволяет описывать сумму и произведение :)
Более сложный пример: суммарная стоимость закупки по всем товарам расцененная по заданному прайс-листу.
Пусть закупка описывается как объект с полями «суммарная стоимость» и полем «товары», содержащим список объектов «товар-количество-стоимость».
Зададим поле «стоимость» в каждом объекте «товар-количество-стоимость» выражением:
В классе «Закупка» полю «суммарная стоимость» задать выражение «сумма всех записей товара-количество по вычислимому полю „стоимость“».
Задача системы — построить транзитивное замыкание зависимостей, чтобы изменение «количество» в одной из записей «товар-количество-стоимость» приводило к пересчету суммарной стоимости
Отображать на календаре события, порожденные самыми разными источниками
Для этого необходимо ввести интерфейс «Событие» с полями «название», «дата», «период», «длительность». Код пользовательского интерфейса всегда использует интерфейс «Событие», для поиска и отображения событий.
Для того, чтобы календарь «находил» дни рождения пользователей, необходимо описать «предоставление» интерфейса «Событие» пользователем следующим образом:
Специализировать структуру «пользователя» системы
Для этого понятие «пользователь» опишем интерфейсом (например, с полями «логин» и «пароль»). Интерфейсы неотличимы от классов с точки зрения манипулирования, и интерфейс можно реализовать классом с произвольной структурой
Разный набор параметров для различных категорий пользователей, например для клиентов и сотрудников компании
Так-как реализовывать один и тот же интерфейс могут разные классы, классов пользователей может быть произвольно число. Единственное что для этого необходимо, чтобы код авторизации осуществлял всю работу с пользователями через интерфейс.
Повторное использование кода на объектах с разной структурой.
Очевидно реализуется интерфейсами или абстрактными классами.
Важно заметить, что типы полей при реализации можно уточнять: если есть интерфейс «перемещение» и он обладает списочным полем типа «запись перемещения» с полями «инвентарь», «количество», то сущность «покупка» при реализации интерфейса может иметь в качестве соответствующего списочного поля поле с типом «запись покупки» с полями «инвентарь», «количество», «закупочная стоимость», и это никак не отразится на коде обработки проведения перемещения.
Остальные пункты, связанные с описанием бизнес-процессов и управлением прав доступа, требуют понятий которые мы рассмотрим в следующей статье, если эта тема вам интересна.
Мы достигли уровня «first-playable» и создали конфигурацию для клининговой компании нашего знакомого, которая реализует учет инвентаря. Посмотреть ее можно здесь demo.meta4.info [2] (логин/пароль root/root). Весь вид форм, диалоги, логика работы определяются следующей конфигурацией (в нее, правда, не вошло описание ограничений прав доступа):
<projectDescription name="CleaningWMS">
<!-- Интерфейс инвентаря для которого ведется учет -->
<interface name="Ware class">
<attribute name="in stock" type="float"/>
<attribute name="local count" type="float"/>
</interface>
<!-- Интерфейс записи при проведении операций: закупки, списания, перемещения -->
<interface name="Wares">
<attribute name="amount" type="float"/>
<relation name="clazz" to="WareClass" kind="reference"/>
</interface>
<!-- Интерфейс склада. В качестве склада могут выступать заказчики
(у которых храниться инвентарь) -->
<interface name="Warehouse">
<relation name="entries" to="Wares" kind="embedding" list="true"/>
</interface>
<!-- Интерфейс для операций над инвентарем -->
<interface name="Transfer">
<attribute name="approved" type="boolean"/>
<relation name="from" to="Warehouse" kind="reference"/>
<relation name="to" to="Warehouse" kind="reference"/>
<relation name="entries" to="Wares" kind="embedding" list="true"/>
</interface>
<!-- Триггер осуществляющий проведения/возврат для операций -->
<trigger class="com.meta4.cms.warehouse.TransferTrigger" on="Transfer.approved"/>
<!-- Триггеры накопительных регистров -->
<trigger class="com.meta4.cms.warehouse.WareClassTrigger" on="Transfer.approved"/>
<trigger class="com.meta4.cms.warehouse.RequestTrigger" on="Перемещение.проведено"/>
<trigger class="com.meta4.cms.warehouse.RequestTrigger" on="Списание.проведено"/>
<enum name="Отдел">
<value name="УПР"/>
<value name="УРР"/>
<value name="ОСС"/>
</enum>
<enum name="Тип операции">
<value name="Списание"/>
<value name="Перемещение"/>
<value name="Закупка"/>
</enum>
<enum name="Статус заявки">
<value name="Не обработана"/>
<value name="Ждет поставки"/>
<value name="Собрана"/>
<value name="Отправлена"/>
<value name="Обработана"/>
<value name="Подтверждена"/>
</enum>
<enum name="ЕдиницаИзмерения">
<value name="шт"/>
<value name="кг"/>
</enum>
<enum name="Категория">
<value name="Техника"/>
<value name="Расх.Мат."/>
<value name="Химия"/>
<value name="Инвентарь"/>
</enum>
<enum name="Роль">
<value name="Супермен"/>
<value name="Кладовщик"/>
<value name="Начальник УПР"/>
<value name="Начальник УРР"/>
<value name="Начальник УСС"/>
<value name="Водитель"/>
</enum>
<entity name="Сотрудник" parent="BaseUser">
<!-- Встроенный аттрибут для текстового представления объекта.
В данном случае мы реализуем этот атрибут шаблонным выражением, которое получается
подстановкой атрибута имя_фамилия -->
<attribute name="__label" type="string" template="$имя_фамилия" specialize="__label"/>
<attribute name="Имя Фамилия" optional="false" type="string" specialize="login"/>
<attribute name="Роль" type="Роль"/>
<attribute name="Пароль" type="password" specialize="password"/>
<attribute name="отдел" type="Отдел" list="true"
expressionClass="com.meta4.cms.warehouse.DepartmentExpression"/>
<presentation tableColumns="имя_фамилия, роль">
<l10n>
много=Сотрудники кого=сотрудника
</l10n>
<table>
<row>
<subordinate name="имя_фамилия"/>
<subordinate name="пароль"/>
</row>
<row>
<subordinate name="роль"/>
</row>
</table>
</presentation>
</entity>
<entity name="Заказчик" parent="Warehouse">
<attribute name="Имя" optional="false" type="string" specialize="__label"/>
<attribute name="Адрес" type="address"/>
<!-- В данном случае мы специлазацией достигаем не другого способа вычисления
субордината, а уточнение типа реляции: теперь она указывает не на интерфейс Wares
а на его наследника - "Товары"
-->
<relation name="Товары" kind="embedding" list="true" to="Товары"
specialize="Warehouse.entries" readonly="true"/>
<presentation>
<l10n>
много=Заказчики кого=заказчика
</l10n>
</presentation>
</entity>
<entity name="Номенклатура" parent="WareClass">
<attribute name="Категория" type="Категория" optional="false"/>
<attribute name="Наименование" type="string" optional="false" specialize="__label"
filterable="true"/>
<attribute name="Отделы" type="Отдел" list="true" optional="false"/>
<attribute name="Стоимость упаковки" type="integer" optional="false"/>
<attribute pname="Вес упаковки, кг" name="Вес упаковки" type="float"/>
<attribute name="В наличии" type="float" optional="false" specialize="WareClass.in_stock"
default="0" readonly="true"/>
<attribute name="На складах" type="float" optional="false"
specialize="WareClass.local_count" default="0" readonly="true"/>
<attribute name="Цена" type="integer" optional="false"/>
<attribute name="Норма смен" type="integer"/>
<attribute name="Цена за смену" type="integer" value="it.цена / it.норма_смен"/>
<attribute name="Срок амортизации" type="integer"/>
<attribute name="Цена за месяц" type="integer" value="it.цена / it.срок_амортизации"/>
<presentation tableColumns="категория, отделы, наименование, на_складах, в_наличии, цена, цена_за_смену, цена_за_месяц">
<l10n>
кого=номенклатуру
</l10n>
<table>
<row>
<subordinate name="наименование"/>
<subordinate name="в_наличии"/>
</row>
<row>
<subordinate name="категория"/>
<subordinate name="на_складах"/>
</row>
<row>
<subordinate name="отделы"/>
</row>
<row>
<subordinate name="стоимость_упаковки"/>
<subordinate name="вес_упаковки" label="Вес упаковки, кг"/>
</row>
<row>
<subordinate name="цена"/>
</row>
<row>
<subordinate name="норма_смен"/>
<subordinate name="цена_за_смену"/>
</row>
<row>
<subordinate name="срок_амортизации"/>
<subordinate name="цена_за_месяц"/>
</row>
</table>
<query title="Операции с этой номенклатурой" ofType="Операция">
<parameter subordinate="entries.clazz" operator="in" value="it"/>
</query>
</presentation>
</entity>
<entity name="Товары" parent="Wares">
<attribute name="Количество" type="float" specialize="Wares.amount" optional="false"/>
<relation name="Наименование" kind="reference" to="Номенклатура" specialize="Wares.clazz"
optional="false"/>
</entity>
<entity name="Склад" parent="Warehouse">
<attribute name="Название" type="string" optional="false" specialize="__label"/>
<attribute name="Адрес" type="address"/>
<relation name="Товары" kind="embedding" list="true" to="Товары"
specialize="Warehouse.entries" readonly="true"/>
<presentation>
<l10n>
много=Склады
</l10n>
</presentation>
</entity>
<action name="it.approved ? 'Провести': 'Отменить'"
class="com.meta4.cms.warehouse.ApproveActionLogic" accept="Операция"
logMessage="Операция успешна проведена"/>
<entity name="Операция" parent="Transfer" abstract="true">
<attribute name="Тип" type="ТипОперации"
expressionClass="com.meta4.cms.warehouse.OperationTypeExpression"/>
<attribute name="Проведена" type="boolean" specialize="Transfer.approved" readonly="true"/>
<attribute name="Дата проведения" type="date" readonly="true"/>
<relation name="Заявитель" kind="reference" to="Сотрудник" optional="false"/>
<relation name="Откуда" kind="reference" to="Warehouse" specialize="Transfer.from"/>
<relation name="Куда" kind="reference" to="Warehouse" specialize="Transfer.to"/>
</entity>
<entity name="Перемещение" parent="Операция">
<attribute name="Проведено" type="boolean" specialize="Операция.проведена"/>
<relation name="Откуда" kind="reference" to="Warehouse" specialize="Операция.откуда"
optional="false"/>
<relation name="Куда" kind="reference" to="Warehouse" specialize="Операция.куда"
optional="false"/>
<relation name="Товары" list="true" kind="embedding" to="Товары"
specialize="Transfer.entries"/>
<relation pname="№ заявки" name="заявка" kind="reference" to="Заявка"
oppositeTo="Заявка.перемещения" readonly="true"/>
<relation name="Ходки" kind="embedding" list="true">
<to>
<entity name="Ходка">
<presentation>
<l10n>кого=ходку</l10n>
</presentation>
<attribute name="Путь" type="integer" default="0"/>
<attribute name="Ставка за путь" type="integer" default="7"/>
<attribute name="Абсолютная ставка" type="integer" default="0"/>
<attribute name="Итого" type="integer"
value="it.путь * it.ставка_за_путь + it.абсолютная_ставка"/>
<relation name="Водитель" kind="reference" to="Сотрудник" optional="false"/>
</entity>
</to>
</relation>
<presentation tableColumns="проведено, дата_проведения, заявитель, куда, откуда, заявка">
<l10n>много=Перемещения</l10n>
<table>
<row>
<subordinate name="заявка"/>
<subordinate name="проведено"/>
</row>
<row>
<subordinate name="куда"/>
<subordinate name="дата_проведения"/>
</row>
<row>
<subordinate name="откуда"/>
<subordinate name="заявитель"/>
</row>
</table>
<single subordinate="товары"/>
<single subordinate="ходки"/>
</presentation>
</entity>
<entity name="Списание" parent="Операция">
<attribute name="Проведено" type="boolean" specialize="Операция.проведена"/>
<attribute name="Причина" type="string" optional="false"/>
<relation name="заявка" pname="№ заявки" kind="reference" to="Заявка"
oppositeTo="Заявка.списания" readonly="true"/>
<relation name="Товары" list="true" kind="embedding" to="Товары"
specialize="Transfer.entries"/>
<relation name="Откуда" kind="reference" to="Warehouse" specialize="Операция.откуда"
optional="false"/>
<presentation tableColumns="проведено, дата_проведения, заявитель, причина, откуда, заявка">
<l10n>много=Списания</l10n>
<table>
<row>
<subordinate name="заявка"/>
<subordinate name="проведено"/>
</row>
<row>
<subordinate name="причина"/>
<subordinate name="дата_проведения"/>
</row>
<row>
<subordinate name="откуда"/>
<subordinate name="заявитель"/>
</row>
</table>
<single subordinate="товары"/>
</presentation>
</entity>
<entity name="Закупка" parent="Операция">
<attribute name="Дата поступления" type="date" optional="false" default="now"/>
<attribute name="Дата оплаты" type="date"/>
<attribute name="Продавец" type="string"/>
<!-- Пример агрегационного выражения и зависимости от связанных объектов -->
<attribute name="Стоимость закупки" type="float" value="sum(товары.стоимость_закупки)"/>
<relation name="Куда" kind="reference" to="Склад" specialize="Операция.куда"
optional="false"/>
<relation name="Товары" list="true" kind="embedding" specialize="Transfer.entries">
<to>
<entity name="Товар" parent="Товары">
<relation name="Наименование" kind="reference" to="Номенклатура"
specialize="Wares.clazz" optional="false"/>
<attribute name="Цена" type="float"/>
<attribute name="Количество" type="float" specialize="Wares.amount"
optional="false" default="1"/>
<attribute name="Стоимость закупки" type="float"
value="it.цена * it.количество"/>
</entity>
</to>
</relation>
<presentation tableColumns="проведена, дата_проведения, заявитель, дата_поступления, дата_оплаты, стоимость_закупки">
<l10n>
много=Закупки кого=закупку
</l10n>
<table>
<row>
<subordinate name="дата_поступления"/>
<subordinate name="проведена"/>
</row>
<row>
<subordinate name="дата_оплаты"/>
<subordinate name="дата_проведения"/>
</row>
<row>
<subordinate name="куда"/>
<subordinate name="заявитель"/>
</row>
<row>
<subordinate name="стоимость_закупки"/>
</row>
</table>
<single subordinate="товары"/>
</presentation>
</entity>
<entity name="Заявка">
<attribute name="Когда отгрузить" type="datetime"/>
<attribute name="Отдел" type="Отдел" optional="false" default="УРР"/>
<attribute name="__label" type="string" template="$номер_заявки" specialize="__label"/>
<attribute name="Номер заявки" type="string" optional="false"/>
<attribute name="Адрес" type="string"/>
<attribute name="Ответственный" type="string"/>
<attribute name="Статус" type="СтатусЗаявки" optional="false" default="Не обработана"/>
<attribute name="Заявка оставлена" type="date" default="now"/>
<relation name="Заказчик" kind="reference" to="Заказчик" optional="false"/>
<relation name="Заявитель" kind="reference" to="Сотрудник"/>
<relation name="Перемещения" kind="composition" to="Перемещение" list="true">
<constructor name='Перемещения из заявки' class="com.meta4.cms.warehouse.CreateTransfer"
primary="true">
<attribute pname="Добавить заказанные товары" name="auto_add" type="boolean"
default="true"/>
</constructor>
</relation>
<relation name="Списания" kind="composition" to="Списание" list="true">
<constructor name='Списания из заявки' class="com.meta4.cms.warehouse.CreateRetirement"
primary="true">
<attribute pname="Списать невернувшиеся товары" name="auto_add" type="boolean"
default="true"/>
</constructor>
</relation>
<relation name="Материалы" kind="embedding" list="true">
<to>
<entity name="Запись">
<attribute name="Заказано" type="float" default="0" optional="false"/>
<attribute name="Отгружено" type="float" readonly="true" default="0"/>
<attribute name="Возвращено" type="float" readonly="true" default="0"/>
<attribute name="Списано" type="float" readonly="true" default="0"/>
<relation name="Наименование" kind="reference" to="Номенклатура"
optional="false"/>
</entity>
</to>
</relation>
<constructor name='По типу отдела' class="com.meta4.cms.warehouse.RequestConstructorLogic"
primary="true">
<attribute name="Отделы" type="Отдел" list="true" optional="false"
defaultExpression="user.отдел"/>
<relation name="Заказчик" kind="reference" to="Заказчик" optional="false"/>
</constructor>
<presentation
tableColumns="когда_отгрузить, статус, номер_заявки, отдел, заказчик, ответственный">
<l10n>
много=Заявки
кого=заявку
</l10n>
</presentation>
</entity>
</projectDescription>
Мы создавали систему, чтобы она поддерживала режим SaaS, так что один сервер может иметь много «проектов», у каждого из которых свои классы оперируемых объектов. То есть у одной группы пользователей может быть CRM, у второй складской учет, а у третей тоже складской учет, но специализированный для ресторанного бизнеса, и все это на одном сервере.
В результате нашей работы мы сформировали 3 «артефакта»:
Безусловно, мы далеки от полной реализации наших идей. Например, не реализовано такое понятие как «регистр накопления», сейчас оно вручную эмулируется триггерами. Тем не менее, за этот год мы сформировали видение языка и архитектуру реализующей его системы.
Автор: meta4
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/arhitektura-prilozhenij/14577
Ссылки в тексте:
[1] ER: http://en.wikipedia.org/wiki/Entity-relationship_model
[2] demo.meta4.info: http://demo.meta4.info/
Нажмите здесь для печати.