我们如何为特定客户修改产品

图片

因此,我们向客户出售了B2B软件产品。

在演讲中,他喜欢一切,但在实施过程中发现仍然有些不合适。 当然,您可以说您需要遵循“最佳实践”,并将自己转变为产品,而不是相反。 如果您有很强的品牌(例如,三个大字母,并且可以发送所有三个小字母),这可能会起作用。 否则,他们会迅速向您解释,客户由于其独特的业务流程而实现了所有目标,因此,我们最好更改您的产品,否则将无法正常工作。 可以选择拒绝并提及已经购买了许可证的事实,并且潜水艇无处可去。 但是在相对狭窄的市场中,这种策略在很长一段时间内都行不通。

我们必须修改。

方法


有几种基本的产品适应方法。

整体式


任何更改都直接对产品的源代码进行,但包含在某些选项中。 通常,在此类产品中,存在带有设置的怪异形式,为了不引起混淆,已为其分配了编号或代码。 这种方法的缺点是源代码会变成很大的意大利面,其中有很多不同的用例,以至于维护起来非常长且昂贵。 每个后续选项都需要越来越多的资源。 这种产品的性能也有很多不足之处。 而且,如果产品的编写语言不支持继承和多态性等现代实践,那么一切都会变得很可悲。

复制


将为客户提供产品的完整源代码,并具有对其进行修改的许可证。 通常,此类供应商会告诉客户他们不会自己修改产品,因为这种产品价格太昂贵(与销售许可证相比,出售许可证的利润要高得多)。 但是他们有熟悉的外包商,他们在第三国的某个地方雇用了相对廉价且高质量的员工,他们随时准备为他们提供帮助。 在某些情况下,客户的专家会直接进行改进(如果他们有人员配备的话)。 在这种情况下,将源代码作为起点,并且修改后的代码将与原始内容没有任何关系,并且将保持自己的生命。 在这种情况下,您可以安全地删除至少一半的原始产品,并用自己的逻辑替换它。

合并


这是前两种方法的混合。 但是在其中,更正代码的开发人员应始终记住:“合并即将到来”。 当发行新版本的源产品时,在大多数情况下,它将必须手动合并源代码和修改后的代码中的更改。 问题在于,在任何冲突中都必须记住为什么要进行某些更改,而这可能是很久以前的事情了。 而且,如果在原始产品中执行了代码重构(例如,代码块被简单地重新排列了),那么合并将非常耗时。

模块化


逻辑上是最正确的方法。 在这种情况下,产品的源代码不会更改,并且会添加其他模块来扩展功能。 但是,为了实现这种方案,产品必须具有允许以这种方式扩展的体系结构。

内容描述


进一步通过示例,我将展示我们如何扩展基于开放和免费的lsFusion平台开发的产品。

系统的关键要素是模块。 模块是带有lsf扩展名的文本文件,其中包含lsFusion代码 。 在每个模块中,都声明了域逻辑(函数,类,动作)和表示逻辑(表单,导航器)。 通常,模块位于目录中,由逻辑原理划分。 产品是实现其功能的模块的集合,并存储在单独的存储库中。

模块是相互依赖的。 如果一个模块使用其逻辑(例如,引用属性或形式),则依赖于另一个模块。

当出现新客户端时,将为其启动单独的存储库(Git或Subversion),其中将创建具有必要修改的模块。 该存储库定义了所谓的顶层模块。 服务器启动时,将仅连接那些模块,它直接或通过其他模块传递依赖于这些模块。 这样,客户不仅可以使用产品的所有功能,还可以使用他需要的部分。
Jenkins创建一个任务,将产品和客户端模块组合到一个jar文件中,然后将其安装在生产或测试服务器上。

考虑实践中出现的几种主要改进案例:

假设我们在产品中有一个订购模块,它描述了标准订购逻辑:

订购模块
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, NEW , DELETE
FILTERS order(d) = o

EDIT Order OBJECT o
;

FORM orders ''
OBJECTS o = Order
PROPERTIES (o) READONLY date, number
PROPERTIES (o) NEWSESSION NEW , EDIT , DELETE
;

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 ;

现在,您需要在订单行上更改金额的计算(它必须考虑到我们新创建的折扣价)。 为此,我们通常创建某些其他模块可以插入其行为的“入口点”。 代替在Order模块中对sum属性的初始声明,我们使用以下代码:
sum '' = ABSTRACT CASE NUMERIC [ 16 , 2 ] (OrderDetail);
sum (OrderDetail d) += WHEN price(d) THEN quantity(d) * price(d);

在这种情况下, sum属性的值将在一个CASE中收集,其中WHEN可以分散在不同的模块中。 可以保证,如果模块A依赖于模块B,则模块B的所有WHEN都将晚于模块A的WHEN。为了正确计算折现金额 ,将以下声明添加到OrderX模块:
sum(OrderDetail d) += WHEN discount(d) THEN quantity(d) * discountPrice(d);

结果,如果设置了折扣,则该金额将受其限制,否则受原始表达的影响。

假设客户想要添加一个限制,即订单金额不得超过特定的指定金额。 在同一OrderX模块中, 我们声明一个将存储约束值的属性,并将其添加到标准选项表单中(如果需要,可以使用设置创建一个单独的表单):
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' ; }
}
}
}
}
结果,我们获得了两个Order模块(在产品中),在其中实现了订单的基本逻辑;在OrderX中 (在客户处),实现了必要的折扣逻辑:

订购
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, NEW , DELETE
FILTERS order(d) = o

EDIT Order OBJECT o
;

FORM orders ''
OBJECTS o = Order
PROPERTIES (o) READONLY date, number
PROPERTIES (o) NEWSESSION NEW , EDIT , DELETE
;

NAVIGATOR {
NEW orders;
}

订单
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模块中删除订单 ,则在启动时会出现一个错误,即在OrderX模块中找不到订单 。 同样,该错误将在IDE中突出显示。 此外,IDE还具有搜索项目中所有错误的功能,该功能使您可以识别由于更新产品版本而发生的所有问题。

实际上,我们将(产品和所有客户的)所有存储库都连接到同一个项目,因此我们可以冷静地重构产品,同时更改使用该产品的客户模块中的逻辑。

结论


这种微模块架构具有以下优点:

  • 每个客户仅连接他所需的功能 。 他的数据库的结构仅包含他使用的那些字段。 最终解决方案的接口不包含不必要的元素。 服务器和客户端不执行不必要的事件和检查。
  • 基本功能更改的灵活性 。 您可以直接在客户的项目中对绝对任何形式的产品进行更改,添加事件,新对象和属性,操作,更改设计等等。
  • 大大加快了客户要求的新改进的交付 。 每次更改请求时,您都无需考虑它将如何影响其他客户。 因此,可以进行许多改进,并尽快(通常在几个小时内)投入运行。
  • 一种更方便的方案,用于扩展产品的功能 。 首先,可以为准备试用的特定客户提供任何功能,然后,如果成功实施,则将模块完全转移到产品存储库中。
  • 代码库独立性 。 由于根据客户服务合同提供了许多改进,因此正式而言,根据这些合同开发的整个代码属于客户。 通过这种方案,可以确保将属于卖方的产品代码与客户拥有的代码完全分开。 根据要求,我们将存储库转移到客户的服务器,客户可以在该服务器上与自己的开发人员一起修改所需的功能。 此外,如果供应商许可了单个产品模块,则客户将没有没有许可证的模块的源代码。 因此,他没有违反许可条件的独立连接它们的技术能力。

上面在编程扩展的帮助下描述的模块化方案通常被称为mix in 。 例如,Microsoft Dynamics最近引入了扩展的概念,该概念还允许您扩展基本模块。 但是,在那里需要更低级别的编程,从而需要更高的开发人员资格。 另外,与lsFusion不同,事件和限制的扩展要求产品具有初始的“入口点”才能利用此优势。

目前,根据上述方案,我们支持并实施了一个零售系统ERP系统 ,该系统具有30多个相对较大的客户,其中包括1000多个模块。 在客户中有快速消费品网络,以及药店,服装店,百货连锁店,批发商等。 在产品中,根据所使用的行业和业务流程,分别连接了不同类别的模块。

Source: https://habr.com/ru/post/zh-CN461983/


All Articles