Как мы дорабатываем продукт под конкретного клиента

в 7:05, , рубрики: ERP-системы, lsFusion, Блог компании lsFusion, модульность, Программирование, продуктовая разработка, Управление продуктом, управление проектами, управление разработкой
image

Итак, мы продали клиенту программный B2B продукт.

На презентации ему все нравилось, но в ходе внедрения выяснилось, что кое-что все-таки не подходит. Можно конечно сказать что нужно следовать “best practice”, и изменить себя под продукт, а не наоборот. Это может сработать, если у вас есть сильный бренд (например, из трех больших букв, и вы можете послать всех на три маленькие буквы). В противном случае, вам быстро объяснят, что заказчик добился всего благодаря своим уникальным бизнес-процессам, и давайте-ка, лучше меняйте свой продукт, или ничего не получится. Есть вариант отказаться и сослаться на то, что лицензии уже куплены, и с подводной лодки деваться уже некуда. Но на относительно узких рынках такая стратегия долго работать не будет.

Приходится дорабатывать.

Подходы

Существует несколько основных подходов к адаптации продуктов.

Монолит

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

Копия

Клиенту передается весь исходный код продукта с лицензией на его изменение. Часто такие вендоры говорят заказчику, что они не будут сами адаптировать продукт, так как это будет слишком дорого (вендору гораздо выгоднее продавать лицензии, чем связываться с услугами). Но у них есть знакомые аутсорсеры, которые нанимают относительно недорогих и качественных сотрудников где-нибудь в третьих странах, готовые им помочь. Также бывают ситуации, когда доработки будут проводиться непосредственно специалистами заказчика (если у них есть штатные единицы). В таких случаях исходный код берется как отправная точка, а измененный код не будет иметь никакой связи с тем, что было изначально, и будет жить своей жизнью. В этом случае можно спокойно удалить хоть половину исходного продукта и заменить на полностью свою логику.

Слияние

Это смесь первых двух подходов. Но в нем разработчику, который правит код, всегда нужно помнить: “merge is coming”. Когда выйдет новая версия исходного продукта, то ему придется в большинстве случаев вручную сливать изменения в исходном и измененном коде. Проблема в том, что при любом конфликте нужно будет вспоминать, зачем были внесены определенные изменения, а это могло быть очень давно. А если в исходном продукте был проведен рефакторинг по коду (например, просто переставили местами блоки кода), то слияние будет очень трудоемким.

Модульность

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

Описание

Дальше на примерах я покажу, как мы расширяем продукты, разработанные на базе открытой и бесплатной платформы lsFusion.

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

Модули имеют зависимость между собой. Один модуль зависит от другого, если он использует его логику (например, обращается к свойствам или формам).

Когда появляется новый клиент, то под него заводится отдельный репозиторий (Git или Subversion), в котором будут создаваться модули с необходимыми ему доработками. В этом репозитории задается так называемый верхний модуль. При запуске сервера будут подключены только те модули, от которых он зависит напрямую или транзитивно через другие модули. Это позволяет использовать у клиента не весь функционал продукта, а только нужную ему часть.
На Jenkins создается задание, которое объединяет модули продукта и клиента в единый jar-файл, который затем устанавливается на рабочий или тестовый сервер.

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

Предположим, у нас в продукте есть модуль Order, в котором описана стандартная логика заказов:

Модуль Order
MODULE Order;

CLASS Book 'Книга';
name 'Наименование' = DATA ISTRING[100] (Book) IN id;

CLASS Order 'Заказ';
date 'Дата' = DATA DATE (Order) IN id;
number 'Номер' = DATA STRING[10] (Order) IN id;

CLASS OrderDetail 'Строка заказа';
order 'Заказ' = DATA Order (OrderDetail) NONULL DELETE;

book 'Книга' = DATA Book (OrderDetail) NONULL;
nameBook 'Книга' (OrderDetail d) = name(book(d));

quantity 'Количество' = DATA INTEGER (OrderDetail);
price 'Цена' = DATA NUMERIC[14,2] (OrderDetail);
sum 'Сумма' (OrderDetail d) = quantity(d) * price(d);

FORM order 'Заказ'
    OBJECTS o = Order PANEL
    PROPERTIES(o) date, number

    OBJECTS d = OrderDetail
    PROPERTIES(d) nameBook, quantity, price, NEWDELETE
    FILTERS order(d) = o

    EDIT Order OBJECT o
;

FORM orders 'Заказы'
    OBJECTS o = Order
    PROPERTIES(o) READONLY date, number
    PROPERTIES(o) NEWSESSION NEWEDITDELETE
;

NAVIGATOR {
    NEW orders;
}

Клиент X хочет добавить для строки заказа также процент скидки и цену со скидкой.
Сначала в репозитории заказчика создается новый модуль OrderX. В его заголовке ставится зависимость на исходный модуль Order:

REQUIRE Order;

В этом модуле объявляем новые свойства, под которые будут созданы дополнительные поля в таблицах, и добавляем их на форму:

discount 'Скидка, %' = DATA NUMERIC[5,2] (OrderDetail);
discountPrice 'Цена со скидкой' = DATA NUMERIC[14,2] (OrderDetail);

EXTEND FORM order
    PROPERTIES(d) AFTER price(d) discount, discountPrice READONLY
;

Цену со скидкой делаем недоступной для записи. Она будет рассчитываться отдельным событием при изменении либо исходной цены, либо процента скидки:

WHEN LOCAL CHANGED(price(OrderDetail d)) OR CHANGED(discount(d)) DO
  discountPrice(d) <- price(d) * (100 (-) discount(d)) / 100;

Теперь нужно изменить расчет суммы по строке заказа (он должен учитывать нашу вновь созданную цену со скидкой). Для этого мы, как правило, создаем определенные “точки входа”, куда другие модули могут вставлять свое поведение. Вместо исходного объявления свойства sum в модуле Order используем следующее:

sum 'Сумма' = ABSTRACT CASE NUMERIC[16,2] (OrderDetail);
sum (OrderDetail d) += WHEN price(d) THEN quantity(d) * price(d); 

В этом случае, значение свойства sum будет собрано в один CASE, где WHEN могут быть разбросаны по разным модулям. Гарантируется, что если модуль A зависит от модуля B, то все WHEN модуля B сработают позже, чем WHEN модуля A. Для правильного подсчета суммы со скидкой в модуле OrderX добавляется следующее объявление:

sum(OrderDetail d) += WHEN discount(d) THEN quantity(d) * discountPrice(d);

В итоге, если будет задана скидка, то сумма будет с ее учетом, а иначе — исходное выражение.

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

orderLimit 'Лимит заказа' = DATA NUMERIC[16,2] ();
EXTEND FORM options 
    PROPERTIES() orderLimit
;

Затем, в том же модуле, объявляем сумму по заказу, показываем ее на форме и добавляем ограничение на ее превышение:

sum 'Сумма' (Order o) = GROUP SUM sum(OrderDetail d) IF order(d) = o;
EXTEND FORM order
   PROPERTIES(o) sum
;
CONSTRAINT sum(Order o) > orderLimit() MESSAGE 'Превышен лимит заказов';

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

DESIGN order {
    OBJECTS {
        NEW pane {
            fill = 1;
            type = SPLITH;
            MOVE BOX(o);
            MOVE BOX(d) {
                PROPERTY(price(d)) { pattern = '#,##0.00'; }
                PROPERTY(discountPrice(d)) { pattern = '#,##0.00'; }
            }
        }
    }
}

В итоге мы получаем два модуля Order (в продукте), в котором реализована базовая логика заказа, и OrderX (у заказчика), в котором реализована нужная ему логика скидок:

Order

MODULE Order;

CLASS Book 'Книга';
name 'Наименование' = DATA ISTRING[100] (Book) IN id;

CLASS Order 'Заказ';
date 'Дата' = DATA DATE (Order) IN id;
number 'Номер' = DATA STRING[10] (Order) IN id;

CLASS OrderDetail 'Строка заказа';
order 'Заказ' = DATA Order (OrderDetail) NONULL DELETE;

book 'Книга' = DATA Book (OrderDetail) NONULL;
nameBook 'Книга' (OrderDetail d) = name(book(d));

quantity 'Количество' = DATA INTEGER (OrderDetail);
price 'Цена' = DATA NUMERIC[14,2] (OrderDetail);
sum 'Сумма' = ABSTRACT CASE NUMERIC[16,2] (OrderDetail);
sum (OrderDetail d) += WHEN price(d) THEN quantity(d) * price(d); 

FORM order 'Заказ'
    OBJECTS o = Order PANEL
    PROPERTIES(o) date, number

    OBJECTS d = OrderDetail
    PROPERTIES(d) nameBook, quantity, price, NEWDELETE
    FILTERS order(d) = o

    EDIT Order OBJECT o
;

FORM orders 'Заказы'
    OBJECTS o = Order
    PROPERTIES(o) READONLY date, number
    PROPERTIES(o) NEWSESSION NEWEDITDELETE
;

NAVIGATOR {
    NEW orders;
}

OrderX

MODULE OrderX;

REQUIRE Order;

discount 'Скидка, %' = DATA NUMERIC[5,2] (OrderDetail);
discountPrice 'Цена со скидкой' = DATA NUMERIC[14,2] (OrderDetail);

EXTEND FORM order
    PROPERTIES(d) AFTER price(d) discount, discountPrice READONLY
;

WHEN LOCAL CHANGED(price(OrderDetail d)) OR CHANGED(discount(d)) DO
    discountPrice(d) <- price(d) * (100 (-) discount(d)) / 100;

sum(OrderDetail d) += WHEN discount(d) THEN quantity(d) * discountPrice(d);

orderLimit 'Лимит заказа' = DATA NUMERIC[16,2] ();
EXTEND FORM options 
    PROPERTIES() orderLimit
;

sum 'Сумма' (Order o) = GROUP SUM sum(OrderDetail d) IF order(d) = o;
EXTEND FORM order
   PROPERTIES(o) sum
;
CONSTRAINT sum(Order o) > orderLimit() MESSAGE 'Превышен лимит заказов';

DESIGN order {
    OBJECTS {
        NEW pane {
            fill = 1;
            type = SPLITH;
            MOVE BOX(o);
            MOVE BOX(d) {
                PROPERTY(price(d)) { pattern = '#,##0.00'; }
                PROPERTY(discountPrice(d)) { pattern = '#,##0.00'; }
            }
        }
    }
}

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

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

Если в исходном коде продукта произойдут какие-либо изменения, которые будут противоречить коду в зависимом модуле, то при запуске сервера будет выдана ошибка. Например, если в модуле Order удалят форму order, то при старте будет ошибка, что в модуле OrderX не найдена форма order. Также ошибка будет подсвечена в IDE. Кроме того, в IDE есть функция поиска всех ошибок в проекте, которая позволяет определить все проблемы возникшие из-за обновления версии продукта.

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

Заключение

Такая микромодульная архитектура обеспечивает следующие преимущества:

  • Каждому заказчику подключается только нужный ему функционал. Структура его базы данных содержит только те поля, которые он использует. Интерфейс конечного решения не содержит лишних элементов. Сервер и клиент не выполняют ненужные события и проверки.
  • Гибкость в изменениях базового функционала. Непосредственно в проекте клиента можно вносить изменения в абсолютно любые формы продукта, добавлять события, новые объекты и свойства, действия, менять дизайн и многое другое.
  • Значительно ускоряется поставка новых доработок, требуемых заказчику. При каждом запросе на изменение не требуется продумывать, каким образом она отразится на других клиентах. За счет этого многие доработки могут быть выполнены и введены в эксплуатацию в кратчайшие сроки (часто в течение нескольких часов).
  • Более удобная схема расширения функционала продукта. Сначала любой функционал можно включить конкретному заказчику, который готов его попробовать, а затем, в случае успешного внедрения, модули целиком переносятся в репозиторий продукта.
  • Независимость кодовой базы. Так как многие доработки оказываются по договорам услуг клиенту, то формально весь код, разработанный в рамках этих договоров, принадлежит заказчику. При такой схеме обеспечивается полное разделение кода продукта, который принадлежит вендору от кода, собственником которого является клиент. По запросу мы переносим репозиторий на сервер клиента, где он может силами собственных разработчиков дорабатывать нужный ему функционал. Кроме того, если поставщик осуществляет лицензирование по отдельным модулям продукта, то у заказчика нет исходного кода модулей, на которые нет лицензии. Таким образом, у него отсутствует техническая возможность подключить их самостоятельно в нарушение условий лицензирования.

Описанная выше схема модульности при помощи расширений в программировании чаще всего называется mix in. Например, в Microsoft Dynamics относительно недавно появилась концепция extension, которая также позволяет расширять базовые модули. Однако, там требуется гораздо более низкоуровневое программирование, что в свою очередь требует более высокой квалификации разработчиков. Кроме того, в отличие от lsFusion, расширение событий и ограничений требует изначально заложенных «точек входа» в продукт, чтобы можно было этим воспользоваться.

На данный момент, по описанной выше схеме мы поддерживаем и внедряем ERP-систему для розничной торговли у более чем 30 относительно крупных клиентов, которая состоит из более чем 1000 модулей. Среди заказчиков присутствуют как FMCG-сети, так и аптеки, магазины одежды, сети магазинов дрогери, оптовики и другие. В продукте, соответственно, есть отдельные категории модулей, которые подключаются в зависимости от индустрии и используемых бизнес-процессов.

Автор: LeshaLS

Источник


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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js