О кастомизации информационных систем



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

    Возможные подходы


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

    После разработки и внедрения информационной системы начинается самая долгая и часто самая болезненная фаза жизненного цикла — поддержка. В случае с проектом-расширением эта фаза может стать вдвойне неприятнее, ведь придется поставлять заказчику не только новые “фичи”, которые реализованы специально для него, но и новые версии продукта, на котором основано расширение. Для того, чтобы в проект попали изменения из новой версии продукта, видится один способ — merge изменений из основной ветки в бранч расширения. Но представьте, насколько это окажется трудоемко, и сколько потенциальных ошибок может проявиться, если один и тот же участок кода сильно изменялся в обеих ветках.

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

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

    Использование динамических атрибутов (модель Entity-Attribute-Value)
    Модель Entity-Attribute-Value (или Open Schema) может использоваться вместе со стандартной реляционной моделью для динамического определения и хранения значений новых атрибутов сущностей. При использовании модели EAV значения атрибутов обычно пишутся в одну таблицу из трех колонок. Как несложно догадаться, их имена Entity, Attribute и Value:
    • Entity — хранит ссылку на объект, поле которого мы описываем. Обычно это идентификатор сущности;
    • Attribute — ссылка на определение атрибута (об этом ниже);
    • Value — собственно значение атрибута.

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

    Для использования этой модели в продукте необходимо сделать 2 вещи:
    1. Реализовать механизм задания метаданных, с помощью которого мы сможем, например, указать, что к сущностям типа “Договор” добавится новый атрибут «Дата расторжения», тип поля — «Дата», компонент для отображения — DateField.
    2. Реализовать механизмы отображения и ввода значений динамических атрибутов на необходимых экранах продукта. Механизм должен находить возможный набор атрибутов для данной сущности в таблице с описанием метаданных, отображать компоненты для их редактирования, а затем в таблице с данными искать и отображать их значения, сохраняя их при закрытии экрана.

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

    Далее о недостатках. Во-первых, это ограниченность применения. Модель EAV позволит лишь добавить атрибуты в сущность и отобразить их в заранее определенном месте на экране. Не более того. Об изменении функциональности, хитрых UI-компонентах здесь речи не идет.

    Во-вторых, EAV модель создает большую дополнительную нагрузку на сервер БД. Для загрузки одного экземпляра сущности без связей потребуется чтение вместо одной нескольких строк таблицы. Для загрузки списка экземпляров, например в таблицу на UI, вообще потребуется N+1 запросов, либо джойны по числу колонок таблицы. Учитывая, что база данных в корпоративных системах и так чаще всего является самым медленным и плохо масштабируемым элементом, такая дополнительная нагрузка может просто убить систему.

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

    Плагинная архитектура
    Данная архитектура позволяет хранить дополнительную функциональность в отдельных артефактах — плагинах. Если ваш заказчик хочет какой-то новой специфики, то вы ставите ему базовый продукт, пишете плагин, подключаете его и готово. Для использования плагинов в продукте должны быть объявлены точки расширения. Что это такое? Если просто, то это определенные места в коде. В этих местах перебираются загруженные плагины, анализируется, есть ли в плагинах логика, предназначенная для данной точки расширения, и если такая логика находится, она выполняется. Примеры точек расширения: пункт меню, обрабочик команды, кнопка на тулбаре, новый экран.

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

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

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

    Как это делаем мы


    Мы выпустили на рынок два тиражируемых продукта: ECM (или в более привычных терминах, систему электронного документооборота, СЭД) ТЕЗИС и систему для автоматизации бизнеса такси Sherlock. С самого начала было очевидно: для того, чтобы поставить конкретному клиенту максимально удобную систему, потребуются доработки продукта, и следовательно в основе продукта должна лежать легко расширяемая архитектура.

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

    Разнообразие требований, их объем и непредсказуемость не позволяли использовать ни один из способов, описанных выше. Вдобавок ко всему, версии продуктов выходят довольно регулярно. Это делает обязательным требованием максимальную легкость перевода проекта-расширения на новую версию продукта.

    Как же мы решаем проблему создания и поддержки расширений?

    Наш проект-расширение


    Большинство проектов нашей компании созданы с помощью платформы CUBA. Мы уже писали о ней в одной из прошлых статей. Для того, чтобы понять, как организованы проекты-расширения, сначала нужно разобраться с устройством самой платформы.

    Если коротко, то CUBA — это набор модулей, каждый из которых предоставляет определенную функциональность:
    • cuba — ядро приложения, содержит в себе всю инфраструктуру, средства для организации бизнес-логики, библиотеку визуальных компонентов, подсистему безопасности и пр.
    • reports — подсистема генерации отчетов
    • fts — подсистема полнотекстового поиска
    • charts — подсистема вывода диаграмм
    • workflow — подсистема управления бизнес-процессами
    • ccpayments — подсистема работы с кредитными картами

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

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

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


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

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

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

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

    Добавление нового атрибута в сущность базового продукта


    Определим для себя задачу: в сущность User базового продукта необходимо добавить поле для хранения адреса. Подобные требования, пожалуй, самые распространенные среди наших заказчиков. Сразу скажем, что платформа поддерживает модель динамических атрибутов, о которых писалось выше, но на практике этот вариант используется редко — скорость выборки данных и легкость построения отчетов практически всегда оказываются важным требованием.
    Собственно об альтернативном способе добавления атрибута. В качестве ORM платформой используется OpenJPA. Объявление сущности в продукте выглядит следующим образом:

    @Entity(name = "product$User")
    @Table(name = "PRODUCT_USER")
    public class User extends StandardEntity {
    	@Column(name = "LOGIN")
    	protected String login;
    
    	@Column(name = "PASSWORD")
    	protected String password;
    
    	//getters and setters
    }
    

    Как видите, это стандартное для JPA описание сущности и маппинга на таблицу и колонки БД.

    Создаем наследника сущности в проекте-расширении:

    @Entity(name = "ext$User")
    @Extends(User.class)
    public class ExtUser extends User {
        
        @Column(name = "ADDRESS", length = 100)
        private String address;
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    }
    

    Механизм наследования стандартен для OpenJPA за исключением аннотации @Extends, которая и представляет наибольший интерес. Именно она объявляет, что класс ExtUser будет повсеместно использоваться вместо класса User.

    Теперь все операции создания сущности User будут создавать экземпляр расширенной сущности:

    User user = metadata.create(User.class); //создает класс ExtUser
    

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

    select u from product$User u where u.name = :name
    

    Без аннотации @Extends мы имели бы на выходе коллекцию объектов User, и для получения адреса из ExtUser пришлось бы повторно перечитать из базы результат предыдущего запроса. Но используя информацию о переопределении, которую предоставляет аннотация @Extends, механизмы платформы произведут предварительную трансформацию запроса и вернут нам коллекцию объектов расширенной сущности ExtUser. Более того, если какие-то другие сущности имели ссылки на User, то при подключении расширения эти ссылки будут возвращать объекты типа ExtUser, без какого-либо изменения исходного кода.

    Сущность переопределена. Теперь хорошо бы отобразить новое поле пользователю.

    Экран платформы представляет собой связку XML + Java. XML декларативно описывает UI, Java-контроллер определяет реакцию на события. Понятно, что с переопределением Java-контроллера особых проблем не возникнет, а вот с расширением XML чуть сложнее. Вернемся к предыдущему примеру с добавлением поля address в сущность User.
    Описание разметки простейшего экрана выглядит так:

    <window
            datasource="userDs"
            caption="msg://caption"
            class="com.haulmont.cuba.gui.app.security.user.edit.UserEditor"
            messagesPack="com.haulmont.cuba.gui.app.security.user.edit"
            >
    
        <dsContext>
            <datasource
                    id="userDs"
                    class="com.haulmont.cuba.security.entity.User"
                    view="user.edit">
               </datasource>
        </dsContext>
    
        <layout spacing="true">
            <fieldGroup id="fieldGroup" datasource="userDs">
                <column width="250px">
                    <field id="login"/>
                    <field id="password"/>
                </column>
            </fieldGroup>
          
            <iframe id="windowActions" screen="editWindowActions"/>
        </layout>
    </window>
    

    Видим ссылку на контроллер экрана UserEditor, объявление источника данных (datasource), компонента fieldGroup, отображающего поля сущности, и фрейм со стандартными действиями “ОК” и “Отмена” (windowActions).

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

    <window extends="/com/haulmont/cuba/gui/app/security/user/edit/user-edit.xml">
        <layout>
            <fieldGroup id="fieldGroup">
                <column>
                    <field id="address"/>
                </column>
            </fieldGroup>
        </layout>
    </window>
    

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

    <screen id="sec$User.edit" template="com/sample/sales/gui/extuser/extuser-edit.xml"/>
    

    Результат:



    Переопределение бизнес-логики


    Что касается переопределения функциональности, то здесь все достаточно тривиально. Инфраструктура платформы реализована на Spring. Соответственно и слой сервисов с бизнес логикой — это управляемые спрингом бины. Возможностей расширения, предоставляемых фреймворком, оказывается более чем достаточно для переопределения нужной бизнес-логики. Чтобы наглядно это продемонстрировать, снова небольшой пример. Допустим, на среднем слое в базовом продукте имеется компонент, выполняющий расчет цены:

    @ManagedBean("product_PriceCalculator")
    public class PriceCalculator {
    	public void BigDecimal calculatePrice() { 
    		//price calculation
    	}
    }
    

    Для того, чтобы в проекте-расширении заменить алгоритм расчета цены мы делаем 2 простых шага:

    Создаем наследника переопределяемого компонента:

    public class ExtPriceCalculator extends PriceCalcuator {
    	@Override
    	public void BigDecimal calculatePrice() { 
                   //modified logic goes here
    	}
    }
    

    Регистрируем класс в конфигурационном файле Spring с идентификатором бина из базового продукта:

    <bean id="product_PriceCalculator" class="com.sample.extension.core.ExtPriceCalculator"/>
    

    Теперь контейнер Spring будет всегда возвращать нам экземпляр ExtPriceCalculator.

    Переопределение темы


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

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

    Для реализации веб-UI нами был выбран популярный фреймворк Vaadin. Vaadin позволяет описывать темы на SCSS. Описание стилей для новой темы на SCSS само по себе в разы приятнее, чем на чистом CSS. Мы сделали процесс создания темы еще менее трудоемким, вынеся множество параметров в переменные.

    Заготовка для новой темы создается в 2 клика с помощью нашей студии разработки. Новая тема импортирует стили базовой темы платформы, разработчику остается переопределить лишь нужные стили и переменные. Многие клиенты желают видеть информационную систему в корпоративных цветах своей компании. Используемый нами подход к расширению тем позволяет им легко этого добиться.

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

    Примеры различных визуальных тем:





    Заключение


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

    P.S. Автор заглавного фото — Илья Варламов. Еще больше шикарных пакистанских грузовиков вы можете найти в его блоге.
    Метки:
    Haulmont 36,39
    Компания
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 3
    • 0
      Больше всего мне понравилось ваше решение для расширения сущностей. У нас на прошлой работе так и не получилось изящного решения. В результате с каждой сущностью оказалась связана ещё одна СущностьExt. И в расширениях просто замещается продуктовый jarник с пустыми Ext-сущностями на jarник где эти сущности содержат нужные поля.

      Правда поначалу я подумал, что @Extends это возможность OpenJPA которую я за два года работы с WebSphere 7.0 с FeaturePack for JPA 2.0 так и не заметил. Но потом понял что это ваше расширения. И насколько я понял из беглого просмотра исходников, вы генерируете свой orm.xml. Таким образом ваше вмешательство в OpenJPA для реализации расширений сущности ограничивается этапом сборки.
      • 0
        Да, вы правы. OpenJPA мы не допиливали — используем стандартный, и @Extends — это уже аннотация именно нашей платформы. orm.xml на основе информации о переопределении сущностей формируется при старте приложения.
      • 0
        Думаю более точное название статьи «CUBA. Механизм кастомизации продуктов» , так как с точки зрения бизнеса это релевантно именно для продуктов. В случае когда:
        — есть 100 заказчиков, использующих продукт и для которых сделаны доработки.
        — пришло время переводить их на новую версию.
        Возникают вопросы объединения новой версии и доработок, вот здесь то этот механизм и очень полезен, так как избавляет от необходимости делать объединение кода в ручную.

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

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