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

в 7:12, , рубрики: Анализ и проектирование систем, архитектура приложений, информационные системы, метки: ,

Декларативный подход и MDA ахритектура имеют целый ряд преимуществ, позволяющих существенно сократить издержки на разработку и поддержку информационных систем (ИС: CRM, WMS, Project Management, etc). Этот подход уже используется в ряде продуктов (таких как 1С, например). Тем не менее, декларативный подход в них используется для решения слишком узкого круга задач. В этой статье мы рассмотрим преимущества декларативного подхода, покажем как можно значительно расширить область его применения в построении ИС, проверим построенную модель на реальных задачах и продемонстрируем работу прототипа.

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

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

С чего все началось

Все началось с того, что совершенно случайно возникла задача по созданию небольшой CRM заточенной под воркфлоу конторы. Я стал смотреть на разные решения, от MS Access, до веб-фреймворков (Django, RoR). Но все они оказались слишком низкоуровневыми. Мне хотелось работать в терминах «пользователей», «событий», «задач» и т.п., а не объектов БД, и иметь соответствующие визуальные представления (календари, списки с фильтрами и т.д.).

Что мы предлагаем

Для разработки информационных систем чаще всего используется объектно-реляционная нотация (OORDBMS, ORM), в рамках которой бизнес-правила описываются императивно на одном из ЯП. Мы предлагаем расширить объектно-реляционную нотацию новыми понятиями, что позволит декларативно описывать бизнес-правила. Мы стремимся максимально приблизить язык «реализации» к языку формализации требований.

Почему декларативное описание бизнес-правил  важно?

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

  • Частичная автоматизация обновления базы данных для новых версий
  • Обновления системы «на лету»
  • Кастомизация системы в визуальном конструкторе бизнес-аналитиком или конечным пользователем
  • В рамках одного SaaS сервиса можно поддерживать динамически расширяемый набор конфигураций

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

Примеры решаемых задач

Рассмотрим набор характерных задач, которые встают при проектировании ИС и от которых мы отталкивались при построении языка:

  1. Отображать стоимость (цена * количество), цену с учетом НДС на основе цены и ставки НДС и т.д. В пользовательском интерфейсе эти величины должны автоматически перевычисляться, а сервер должен проверять корректность полученных данных (или вычислять их сам)
  2. Более сложный пример: суммарная стоимость закупки по всем товарам расцененная по заданному прайс-листу. Поддержка должна быть и со стороны клиента, и сервера.
  3. Отображать на календаре события, порожденные разными источниками: днем рождения пользователя, началом митинга, завершением майлстоуна и т.д.
  4. Специализировать структуру «пользователя» системы
  5. Разный набор параметров для различных категорий пользователей, например для клиентов и сотрудников компании
  6. Повторное использование кода на объектах с разной структурой.
    Например списания, перемещения и оприходования товаров имеют различную структуру но идентичные действия
  7. Бизнес правила:
    • Начисление штрафа за каждый просроченный день задачи ее исполнителю
    • Вычисление арендной платы исходя из длительности использования и посуточной стоимости
  8. Уведомления:
    • постановщика задачи о ее завершении
    • директора при списании дорогой техники
  9. Фиксация прайс-листа после его согласования
  10. Менеджеры могут видеть только клиентов, которые им назначены

Новые понятия

Рассмотрим основные понятия, которые мы добавляем в объектно-реляционную модель:

Классы

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

  • Классы обладают только полями (методы отсутствуют)
  • Классы допускают наследование (и, как и в объектно-реляционном расширении SQL при запросе к базовому классу возвращаются объекты не только базового класса, но и классов-наследников)
  • Классы бывают абстрактными (т.е. у которых не может быть экземпляров). У абстрактных классов могут быть абстрактные поля, которые в не абстрактных дочерних классах необходимо «реализовать»: либо задать вычислимое выражение, либо объявить простым хранимым полем
    Типы полей можно уточнять при наследовании (если поле класса ссылается на класс A, то при наследовании можно уточнить ссылку на класс B, если B наследник A.)
  • Ссылочные поля классифицируются как
    • Ссылки
    • Композиции (аналог композиций в UML) — «композируемый» объект имеет время жизни ограниченное временем жизни «композирующего» объекта
    • Включения (я не встречал аналогов в известных языках программирования или моделирования), являются усилением композиции — вложенный объект не мыслится вне рамок своего родителя

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

Вычислимые поля

Возможность задавать выражение на XPath-похожем языке в качестве значения для поля объекта. Понятие вычислимых выражений и вычислимых полей используется во всех остальных расширениях нашего языка. Необходимо отметить, что пользователи ORM фреймворков или PL/SQL используют возможности своего языка при реализации get/set методов, но это совсем не тоже самое, т.к. информация о вычислении описывается в тьюринг-полном языке. Мы специально упрощаем язык выражений до не-тьюринг-полного, чтобы:

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

Таким образом, информация о способе вычислении поля содержится в описании структуры объекта, что позволяет по этой структуре автоматически

  • Валидировать объект на сервере
  • Автоматически обновлять вычисляемые поля в пользовательском интерфейсе

Интерфейсы

Интерфейсы хорошо известны в ООП, однако несправедливо забыты при описании баз данных. Введем интерфейсы следующим образом:

  • Интерфейс представляет собой абстрктный класс, у которого все поля абстрактны
  • Реализация интерфейса тождественна наследованию
  • Интерфейсы полностью взаимозаменяемы с классами: везде где можно использовать классы, можно использовать интерфейсы (по аналогии с представлениями в реляционной теории).
    То есть можно

    • Делать выборки по интерфейсу (результат выборки — все объекты, реализующие заданный интерфейс)
    • Изменять объект через интерфейс (если поле интерфейса реализовано через привязку к полю класса)
    • Создавать триггеры на интерфейсы (при этом триггер будет срабатывать для всех классов-реализаций)
  • Класс может «предоставлять» интерфейс. Для этого описывается «связанный» класс, который реализует указанный интерфейс используя поля класса, который «предоставляет» интерфейс.
    • Класс может предоставлять один и тот же интерфейс несколько раз («предоставления» именуются)
    • В отличие от наследования, «предоставления» можно описывать вне класса (можно провести аналогию с паттерном «Адаптер»)
    • Для структуры «связанного» класса, как и при любом наследовании,  допустимо уточнения типов полей

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

Проверка выразительности языка

Рассмотрим как приведенные выше задачи реализуются с помощью введенных нами конструкций:

  1. Отображать стоимость  как произведение цены на количество, цену с учетом НДС

    Это полностью ложится на понятие «вычислимые поля», а язык описания выражений, безусловно, позволяет описывать сумму и произведение :)

  2. Более сложный пример: суммарная стоимость закупки по всем товарам расцененная по заданному прайс-листу.

    Пусть закупка описывается как объект с полями «суммарная стоимость» и полем «товары», содержащим список объектов «товар-количество-стоимость».
    Зададим поле «стоимость» в каждом объекте «товар-количество-стоимость» выражением:

    • взять прайс-лист из закупки
    • отфильтровать список цен по товару, который должен совпадать с моим
    • взять у получившегося списка первый и единственный элемент
    • взять у него поле «цена».

    В классе «Закупка» полю «суммарная стоимость» задать выражение «сумма всех записей товара-количество по вычислимому полю „стоимость“».
    Задача системы — построить транзитивное замыкание зависимостей, чтобы изменение «количество» в одной из записей «товар-количество-стоимость» приводило к пересчету суммарной стоимости

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

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

    • «название» — выражение «День рождение $имя $фамилия»,
    • «дата» — пользователь.день_рождения
    • «период»  - константное выражение «1 год»
    • «длительность» — константное выражение «весь день»
  4. Специализировать структуру «пользователя» системы

    Для этого понятие «пользователь» опишем интерфейсом (например, с полями «логин»  и «пароль»). Интерфейсы неотличимы от классов с точки зрения манипулирования, и интерфейс можно реализовать классом с произвольной структурой

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

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

  6. Повторное использование кода на объектах с разной структурой.

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

Остальные пункты, связанные с описанием бизнес-процессов и управлением прав доступа, требуют понятий которые мы рассмотрим в следующей статье, если эта тема вам интересна.

Что мы реализовали

Мы достигли уровня «first-playable» и создали конфигурацию для клининговой компании нашего знакомого, которая реализует учет инвентаря. Посмотреть ее можно здесь demo.meta4.info (логин/пароль root/root). Весь вид форм, диалоги, логика работы определяются следующей конфигурацией (в нее, правда, не вошло описание ограничений прав доступа):

XML конфигурация

<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 «артефакта»:

  • Язык описания информационных систем
    • Описание структуры
    • Описание представления
    • Описание триггеров
  • Сервер: Java + Jetty + MongoDB. MongoDB используется из-за безсхемности, что облегчило нам работу по хранению данных с разной структурой в одной коллекции. Однако, из-за существенных недостатков (отсутствие транзакций и JOIN-ов, медленная агрегация) мы планируем перейти на RDBMS когда станем «большими и взрослыми»
  • Клиент: Javascript + MooTools. Представление целиком формируется на клиенте, доступ к данным через REST API сервера. На клиенте повторяется реализация работы с моделями языка, но уже в упрощенном виде, специально для формирования представления, с учетом того, что клиент «тонкий». В отличие от традиционной «текстовой» шаблонизации мы используем шаблонизацию на уровне DOM дерева (этот подход очень мало распространен, и я напишу о нем, если интересно)

Безусловно, мы далеки от полной реализации наших идей. Например, не реализовано такое понятие как «регистр накопления», сейчас оно вручную эмулируется триггерами. Тем не менее, за этот год мы сформировали видение языка и архитектуру реализующей его системы.

Автор: meta4

Поделиться

* - обязательные к заполнению поля