欢迎转载, 转载请注明来源
。
本文探讨了几种
开发
高度可定制项目的
方案
,包括CUBA平台
。
做为开发
人员
肯定听客户说过
:“
你们的产品还不错,但是还
有些细节需要完善”
,
然后
收到一份有数百个需求的
“
待完善细节
”清单。
做为项目经理肯定也跟团队说过
:"
公司即将拿到一个大单,但是需要咱们先。。。。。。
" ,
结果往往变成
开发人员头疼不已
地去满足客户的各项愿望清单
。
那么,如何
既能满足客户的需求,又能使核心产品
远离客户潜在
的
危险想法? 以特定
技术
方式设计的产品,
如何
既支持更多功能插件又能持续保证
最高性能? 为解决方案提供可靠和出色的支持,需要面对多大的挑战
?
随着
商业世界
对
定制产品的需求越来越强烈,
软件开发行业
发展出了许多通用
方法
来满足客户的
定制
需求。下面
我们会介绍
一些
典型
的
方
案
,
如果您
对
这些方案
已经很熟悉
,那么欢迎您直接向下滚动到“扩展
方案
”段落,了解我们如
何以更有效的方式解决这些
具有挑战性的问题
。
"全家桶"
方案(
All In One
)
最直接,
最显而易见
的解决方案
就
是
在
一个核心产品里实现所有的功能,然后
为了满足不同客户的需求,再给各个功能加个“开关”
。
这个方案的
主要优点
就是“大”而“全”
,
对于
某些类型
的
产品
适用
,这
样的
产品通常涵盖
该行业所有
业务需求
所以无需
大量定制
。
这种方法的先天局限性隐藏在“无需大量
定制”的假设中。通常,产品开发始于这一设想,但经过多次交付后,
就
会意识到
客户定制化需求的
真实规模。
然后就是进退
两难的
局面
:拒绝定制开发
则
可能失去客户,继续在
核心
产品上添加
定制
功能
则
导致源码成为一个“垃圾箱”,
核心
产品代码包含了
多数
用户根本用不到
的功能
、
臃肿、难以维护。
这种情况下你会如
何选择?显然,
不管怎么选
都不是成功之
道
。
总结:只有在你确定产品不会出现很多的个性化需求时,“All in One”方
案
才是合适的选择。 否则,你
只能在
产品的可维护性
、可控性
与客户满意度之间
二选一
。
对此,我们引用
Jerry Garcia
说过的一句话做出评价
:“
Constantly choosing the lesser of two evils is still choosing evil(两害相权取其轻
也是害
)
”。
分支
方案
如果
确实
有重量级的定制
需求
“必须”交付
,
又
不
能
使用大而全的方案
,
另一种直截了当的方式
就是使用代码
分支
-
创建一个
新的代码
分支,然后在这个分支上独立修改。
分支
方案
与All in One比较,最大的优点是
可以随心所欲的做定制化开发
,不用
担心
一个分支
的
代码修改影响其它
客
户。 使用
不同
的分支来满足不同客户的特定
需
求,
避免了在
同一代码库中混合所有功能。
然而
随着
产品发展,
这种方案
将会面对另一种“死法”
。
因为
大多数
bug修复
,
产品
改进,新功能
都应用到核心产品分支上,而定制化开发在客户分支上,
因此,需要频繁地合并分支以使所有定制分支与核心产品保持同步。 原始产品代码没有受到客户分支的影响的话合并还算是一个简单的操作,否则的话代码合并会变得非常耗时并且无法避免地,回归测试时肯定有bug。
如果
只需要
少量
客户
分支,
这个
方
案还是有效的
。 然而,随着交付
的
增加,频繁地代码合并将会成为开发人员的噩梦。
总结:分支方
案
无疑非常灵活和直接 - 产品的任何部分都可以修改。 然而,
费力的部分发生在
交付
之后
,
并且
随着时间的推移
会越来越费力
,并且
客户分支太多的话就更加难以管理
。
EAV:
实体 - 属性 - 值模型 (
entity
–attribute–value model
)
EAV,即
实体 - 属性 - 值模型(又名对象 - 属性 - 值
(object-attribute-value)
模型,垂直数据库模型
或
开放模式)是众所周知且广泛使用的数据模型。 EAV支持动态实体属性,通常与标准关系模型并行使用。
从产品化的角度来看,使用EAV的主要优点是您可以“按原样”交付产品,然后通过在运行时添加所需的属性来调整数据模型,从而保持源代码的清洁。
但这个方案也有
缺陷
:
适用范围有限
- EAV模型受限于仅允许向实体添加属性,然后根据预先编写的逻辑将其自动嵌入到UI中。
额外的数据库服务器负载 - 垂直数据库设计经常成为企业应用程序的瓶颈,
因为
企业应用程序通常
有
大量实体
,实体也可能具有很多
属性。
最后,企业级应用往往需要成熟的
报表引擎
,EAV的
“垂直”数据库结构
会给报表引擎开发增加大量复杂度。
总结:实体 - 属性 - 值模型在某些情况下具有很大的价值,例如
通过增加
额外的
不会在业务逻辑中明确使用的
信息数据来实现灵活性。换句话说,EAV是对标准的关系模型和插件架构的一个很好的补充。
插件架构
方案
插件架构是最流行和最强大的方法之一 -
定制
功能逻辑被
发布
为单独的制件,称为插件。
这种方案需要在
产品源代码中定义“
定制
点”(也称为扩展点)
,应用程序在定制点检查是否有插件要覆盖开箱功能
。
插件的一个变体是外部脚本
:所需
功能
采用外部
脚本实现并在外部存储
,通过
预定义的“
定制
点”控制
调用外部脚本实现插件功能
。
使用插件
方案
,
产品代码保持一直“干净”, 不会被不同的客户化影响。研发团队
“按原样”交付核心产品,
定制化功能做在插件或者脚本中。
另一个优点是
软件升级更新更易于管理,
产品和插件
的
完全分离使
得它们
都可以彼此独立地更新
而互不影响
。
当然,
这个方案
也
有
限制:
主要
是
不
可能完全预测
客户
将来
会
提出哪些定制要求,只能
先
猜测应该嵌入“定制点”的位置。
也许可以在各处都添加
定制点
以防万一,但
结果就是
代码可读性差
、难以
调试
,技术支持复杂度提高
。
总结:如果“
定制
点”易于预测,插件
架构方案
确实有效,
需要注意的就是
“
定制
点”
以外无法添加定制功能
。
扩展方
案
我们
的
企业级应用开发平台CUBA
采取
了一种独特的方法。CUBA是一
个具有
实践
性
、开发
人员
驱动演进的平台。 基于我们对现有产品的丰富经验,
在定制化开发方面,
我们提出了两个
终极
要求:
客户定制化代码应与核心产品代码完全分离。
产品代码的每个部分都应该支持可修改定制。
最终,通过CUBA的“扩展”机制,
我们
不但满足了以上需求而且做到了更多。
CUBA扩展
扩展是一个
独立
的CUBA项目
(lib库)
,它继承了
父
项目(
比如说核心产品
)的所有功能。开发人员能够在
扩展里开发新的功能而且
不
会
影响
到
父项目,
更厉害的是
由于使用了
Open Inheritance
模式和
CUBA的特别机制
,
开发人员可以在扩展里
重写
父项目的任何部分。总之,
CUBA扩展就
是
开发团队
实现本文
开头提到的
数百个“
小
细节”的地方。
实际上,每个CUBA项目都是CUBA平台本身的扩展 - 因此任何平台功能
都可以被重写
。我们自己
也
采用这种方法从核心平台中分离出一些开箱即用的功能(全文搜索,报表,图表等)。如果您在项目中需要它们,
就把
它们添加为父项目 - 就
像
多重继承!
用
同样的方式
还
可以构建
多级扩展结构
。
听上去有点复杂但是很实用
。
举一个现成的例子
:
Sherlock
- 是
Haulmont
的出租车
全周期
管理解决方案,支持出租车业务从预订
、派单到
计费
的方方面面
。该解决方案
满足了客户许多不同的
业务,其中相当一部分与
地域
相关。例如
,
所有英国出租车公司都有相同的法律规定,但其中许多不适用于美国,反之亦然。显然,我们不希望在核心产品中实施所有这些规定,
原因是
:
这是“特定运营区域”的功能
不同国家当地的法规可能对出租车车队运营产生完全不同的影响
一些客户根本不需要类似的法律规定
因此,我们这样组织多级扩展结构:
核心产品包含出租车业务的通用功能
第一级定制实现区域定制化功能
第二级定制涵盖特定客户的定制需求(如果有的话)
显而易见,通过使用扩展,既不需要客户分支也不需要把客户需求都集成到核心产品中,代码还清晰可控。 看上去好得难以置信,接下来看看它是如何实现的:
向现有实体添加新属性(实体定制)
假设Product产品中有一个
User实体
,它
包含两个字段:login和password:
然后根据客户要求需要
向
上述实体
添加“家庭住址”字段
,我们就需要在
"扩展"中扩展User实体:
上述代码中
除了@Extends之外的所有注
解
都是常见的JPA注
解
。 @Extends
注解
是CUBA引擎的一部分,它全局地将
User
实体替换为ExtUser
,包括替换掉原Product产品里的User实体
。
使用@Extends属性,我们
在
平台
里强制
做以下事情:
始终创建“最新子类"的实体
User user = metadata.create(User.
class
);
//ExtUser 实体将创建
在JPQL执行之前做转换,以便它们始终返回“最新子类”的集合
select u from product$User u where u.name = :name //返回ExtUsers集合
始终在关联实体中使用“最新子类”实体
userSession.getUser(); //返回 ExtUser 类型
换句话说,如果声明了扩展实体,整个解决方案(核心
产品和扩展)中
的
基础实体
都会被
扩展实体
替换
。
界面定制
以上
我们通过添加地址属性扩展了用户实体,现在
要反映到
用户界面中。 原始产品
(Product)
界面声明
是这样的
:
如
图所示
,CUBA界面描述使用普通XML
语法
。
虽然
我们可以简单地在扩展中重
写
整个界面
的XML
描述
(先
复制粘贴
原XML的
大部分内容
),但是
如果将来
原
界面某些内容发生变化
就得
手动将这些更改复制到扩展界面。为了避免这种情况,CUBA引入了界面继承机制,
在扩展中
只需要描述对界面的更改
即可
:
如图所示,
使用extends属性定义要继承的界面,
然后描述要
更改的界面元素。
看看结果:
修改
业务逻辑
CUBA平台使用Spring Framework
实现业务逻辑
,Spring Framework构成了平台基础架构的核心部分。
举例,有一个bean用
来
做
价格计算:
要
重写
价格计算
逻辑
,
只需要简单的两步:
首先,
扩展
(继承)核心产品中的
类并
重写相应的方法
:
其次
,使用
原有的
bean标识符在Spring配置中注册新类:
然后
对PriceCalculator的注入将始终返回扩展类实例,
所以新逻辑
将
在
整个产品
范围内生效
。
在扩展中升级
核心
产品版本
随着核心产品的发展和新版本的发布,您
很可能会打算
将扩展升级到最新的
核心
产品版本。 这个过程
也
很简单:
在扩展中指定核心产品的新版本号
重新构建扩展:
如果扩展是基于产品API的稳定部分构建的,则可以直接运行扩展。
如果核心产品API进行了一些重大修改,并且这些修改与扩展中实现的定制化开发重复了,则有必要在扩展中支持新的核心产品API。
大多数情况下,产品API在更新时不会发生显著的变化,尤其是在
小
版本中。 但即使出现API“大爆炸”,产品通常会保持至少两个未来版本的向下兼容性,旧的实现标记为“已弃用”,允许将所有扩展迁移到最新的API。
总结
现在我们以
表格形式
对以上方案做一个简短的总结
:
显而易见,
扩展方
案很
强大,但它
不支持运行时定制
(动态定制)。为了
弥补这方面
,CUBA
也支持
Entity-Attribute-Value模型和Plugin/Scripting方
案
。