推荐图书: 疯狂Java讲义精粹(附光盘)
你可曾听过:“我真的很喜欢你的产品,除了一些小的细节点”?然后CIO(首席信息官)推出一个必须有的需求清单,这清单中的数百个需求点需要添加到你的了不起的产品中。你可曾听过或者甚至说过:“同志们,我们将签署一个高利润的合同,但是。。。”?然后客户期望的额外的功能变成了开发工程师的头痛点。
所以,尽管现在你的产品能满足顾客的需求,但怎样才能让你的产品远离顾客潜在的有风险的想法?怎样才能在已有无数加载项的情况下,维系一个产品技术设计的最高水平,来让其在特定的方式下起作用?这些为成熟的解决方案提供可靠且突出支持的基础性需求,将面临多少挑战?
在商业世界,产品定制是一种越来越可取的要求和一些共通的做法,这些做法已经演变为响应这一客户需求。下面你将找到一个大致的方法,如果你已经对它们比较了解了,你可以直接拖到到“方法五:扩展法”章节,来了解我们怎样以一种我们自认为有效的方式克服这些挑战。
方法一:在一个产品中包含所有定制功能(All in One)
最直接最容易想到的定制化的办法就是在一个产品核心中实现所有的需求,然后通过“ 属性开关 ”的技术来匹配不同客户的需求。
这种方法的主要优点是始终保持一个单一产品,对于不需要太多扩展就能覆盖大多数业务需求的场景是比较管用的。
但这种方法其实隐藏了一个假设,即“没有太多了定制需求”。通常,产品的开发都以这个信念开发,但在几伦迭代交付之后,你会看到客户真正的定制需求量。陷入一个左右为难的困境并不罕见:拒绝定制或者潜在地丢失客户,或者把代码变成了一个“垃圾桶”,因为为一个客户定制太多的个性化需求对其他主要客户都是无用的。
你会选择哪种?很显然,从一个岩石和一个坚硬的东西中选择一个都不会成功。
总结:在一个产品中包含所有定制功能,只在定制需求很少或者有限时才算是合理的选择。否则,你将在可维护性和客户满意度之间做出选择。在此引用Jerry Garcia的话:“从两个不太坏的恶魔中选择一个,不还是恶魔吗”。
方法二:分支开发(Branching)
如果重要的定制是交付中必须要有的部分,那么 All in One 的技术方案就行不通了。有另外的更直接了当的方法—— 分支开发 。你可以很容易的创建一个独立的产品分支,随意修改。
与All in One相比,分支开发最大的优势是没有定制范围的限制,通过分支的方式来分隔不同客户的特定需求,避免了在同一套代码中混杂所有的功能。
但是,这个优势的另一面可能让产品演化走向死路。显而易见的,产品分支是主要的开发领域:大部分的bugfix、改进、新功能首先添加到产品中,那么,要保持定制的分支和主产品保持同步,就需要频繁的合并代码,合并代码时要是和主干产品没有冲突,那合并代码是很容易的操作;要是有冲突,合并操作将非常耗时,且不可避免的需要回归测试。
如果定制分支很少时,这种分支开发的方式是可行的,但是当交付的产品数量增加以后,面临痛苦合并的可能性将迫在眉睫。
总结:分支开发无疑是非常灵活和直接的方法——产品的任意部分都可以修改。但是在交付的后期将很费力,随着时间的推移,将变得更困难,也不太可能有太多的定制分支。
方法三: 实体-属性-值模型 (EAV)
实体-属性-值 模型(Entity-Attribute-Value model又名:Object-Attribute-Value model,垂直数据库模型和开放schema)是一种熟知的广泛使用的数据模型。EAV可以支持动态的实体属性,它适合用于平行的标准关系模型。
从产品角度来看EAV的优势是:你可以交付你的产品“as is”,然后通过在运行时添加需要的属性来调整数据模型,而不需要修改源码。
还是一样的有点不足:
- 适用性有限:该模型仅适用于这样的场景,基于已经编写好的逻辑,使用添加属性的方式,使其自动的嵌入到界面中
- 额外的DB服务器负担:垂直数据库的设计通常会变成企业级应用的瓶颈,因为它通常操作大量的与应用相关的实体和属性。
- 最后,企业级系统不可能没有复杂的报表引擎。EAV模型将因为它的“垂直”数据库结构带来许多潜在的并发症。
总结: EAV 模型在某些场景下很有有用,比如:提供一种附加信息数据即可获得灵活性的需求,这种附加的信息数据并不显示地在业务逻辑中使用。 换句话说,EAV适度是好的,除了标准的关系模型和 插件架构 。
方法四:插件架构(The Plugin Architecture)
插件架构是最常用最强大的一种方式之一:功能逻辑分别放在名为插件的东西里面。要想覆盖现有的盒子外的行为,并运行插件,就必须在产品的源码中定义“定制点”(又名扩展点)。一个“定制点”是某些源码中的片段,应用会扫描所有的插件,来检查是否有插件重写了指定的实现,如果实现了就执行该插件;另一种插件架构方式是外部脚本,功能实现被实现和存储在外部的脚本中,脚本的调用被预定义的“定制点”控制。
使用插件方式可以保持产品代码的纯净,可以不做修改的交付核心产品,而把定制的行为放在插件或者脚本中。另一个优点是可以很好的管理更新的过程。产品和插件的完全分离使他们可以相互独立的升级。
当然,不足之处在于:原则上,你不可能知道客户在未来提出的需求。因此,只可能猜测“定制点”要嵌在哪里。当然,你可以把“定制点”分散在所有可能的地方以缓解“万一需要的场景”,但这将导致代码可读性很差,代码很难调试,增添了复杂性。
总结:插件模式确实在“定制点”可预测的场景下工作,但要注意的是:在“定制点”间的定制是不可能的。
方法五:扩展法(The Extensions Approach)
在我们的企业级软件开发平台 CUBA 中,我们已经实现了一个独特的方法。正如 我们之前的文章 ,CUBA是非常实用的,在一套开发驱动的演化中创造出来的活的有机体。所以,基于我们丰富的已有产品的经验,我们提出了两个终极需求:
- 为客户定制的代码必须和核心产品代码完全分离
- 每个产品的代码部分必须可以被修改
我们设法满足这些需求,甚至从这种扩展机制中获得了更多好处。
CUBA的扩展
一个扩展就是一个独立的CUBA工程,它把底层工程当作依赖库来使用,继承了所有底层工程的特性(比如:你的核心产品),这很显然允许开发者实现新功能,而不影响父工程,但得益于 开放继承 模式和专门的CUBA设施,你也可以重写任何父工程的部分功能。总之,一个扩展就是你可以实现成千上万个文章开头提到的“小细节点”的地方。
事实上,每个CUBA工程是一个CUBA平台自身的扩展,所以它能重写任意的平台功能。我们自己采用这种方式来区分出核心平台之外的功能。所以如果在你的工程中需要他们,你只需要把他们当作父工程添加到你的工程中即可,类似多重继承!
用相同的方法,你可以构建一个可继承的自定义模型。这可能听起来比较复杂,但它很管用。让我给个真实的例子: Sherlock 是 Haulmont’s 的完备的出租车管理方案,支持每个运行出租车业务的各个方面,从预定和分派到应用和结算。这个方案涵盖了需要不同的客户业务,并且这些业务中很多是和本地化相关的。比如:所有英国的出租车公司有相同的法规,但有的法规在美国却行不通,反之亦然。很显然,我们不想在核心产品中实现所有这些法规,因为:
- 这是一个“操作层面的具体的”功能
- 在不同的国家,本地化的法规可能对出租车队管理的影响完全不同
- 有的客户根本不需要法规
所以,我们组织一下多层扩展:
- 核心产品包含通用出租车业务功能
- 第一层定制实现区域特殊化
- 第二层定制覆盖客户的需求清单(如果有的话!)
CUBA的扩展架构图
非常干净。
如你所见,使用扩展的方式,保持了代码的干净和可维护性,你既不需要以分支方式开发你的产品,也不需要迁移所有的需求到你的核心产品中。看起来真的很好,所以让我们看看它在代码实践中如何工作!
给已有的实体添加新的属性
让我们假设我们有一个User实体定义,它由两个属性组成:login和password:
User entity
Java
@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 }
现在我们的客户有个额外的需求,要添加一个“home address”属性到User实体中,要做到这个,我们在扩展中继承User实体:
Java
@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; } }
你可能已经发现,所有的注解,除了@Extend,都是JPA的注解。@Extend注解是CUBA引擎的一部分,它会用ExtUser全局替换User实体,甚至跨产品功能。
使用@Extend注解,我们必须让平台:
- 总是创建继承结构中叶子类型的实体
Java
User user = metadata.create(User.class); //ExtUser entity will be created
- 在执行前,转换所有JPQL的查询语句,以便我们总是返回叶子类型的集合
Java
select u from product$User u where u.name = :name //returns a list of ExtUsers
- 在关联的实体中总是使用叶子类型
Java
userSession.getUser(); //returns an instance of ExtUser type
换句话说,如果声明了一个继承的实体,父实体就在整个产品和扩展中被遗弃,并且被继承的实体全局替换。
界面定制
我们已经通过添加address属性来继承了User实体,现在我们想要让这个继承效果作用到用户界面中。首先,让我们看看原始界面是什么样子:
Java
<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> <fieldGroup id="fieldGroup" datasource="userDs"> <column> <field id="login"/> <field id="password"/> </column> </fieldGroup> <iframe id="windowActions" screen="editWindowActions"/> </layout> </window>
可以看到,一个CUBA的界面描述是以XML的方式呈现。显然,我们可以简单的在扩展中重新声明整个界面描述,但是这意味着复制-粘贴大部分的描述代码,它导致如果以后产品界面有点变化,我们就需要手动地把这些变化点复制到扩展的产品界面中。为了避免这个,CUBA引入了界面继承机制,你所需要做的仅是描述不同的界面部分:
Java
<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属性来定义祖先界面描述,然后只需要描述扩展需求中不同的部分即可。
走你,让我们最后看看界面效果
CUBA扩展后的界面效果
业务逻辑修改
为了修改CUBA平台的业务逻辑,CUBA平台使用了Spring框架作为其平台基础的核心部分。
比如:你有一个中间件来执行价格计算:
Java
@ManagedBean("product_PriceCalculator") public class PriceCalculator { public void BigDecimal calculatePrice() { //price calculation } }
要重写价格计算的实现,我们仅需要两个步骤:
第一步:继承产品类,并重写相应的方法:
Java
public class ExtPriceCalculator extends PriceCalcuator { @Override public void BigDecimal calculatePrice() { //modified logic goes here } }
第二部:在spring中配置这个产品bean:
Java
<bean id="product_PriceCalculator" class="com.sample.extension.core.ExtPriceCalculator"/>
现在PriceCalculator注入将总是返回继承的类的实例,这样,继承的实现将作用到整个产品中。
在扩展中升级基础产品的版本
随着核心产品的演进和新版本的发布,你终究会决定把你的扩展升级到最新的产品版本。这个过程非常简单:
- 在扩展中指定底层产品的新版本
- 重新构建扩展:
- 如果扩展扩展是构建在稳定的产品API基础上,那么它应该可以跑起来了
- 如果产品API有些重要的修改发生,且这些修改与定制的实现有冲突,那就要在扩展中支持新的API
大多数时候,产品API不会再更新中有太多的变更,尤其是小版本的发布。但是就算API的大改发生了,一个产品通常也会在未来的几个版本中保持向下兼容,并在老的实现中标记“deprecated”,允许所有的扩展迁移到新的API上。
总结
简单的汇总一下,我比较喜欢用对比分析的表格形式来阐述:
/ | All in One | Branching | EAV | Plugins | CUBA Extensions |
Architecture independent | + | + | – | – | – |
Dynamic customization | – | – | + | +/- | – |
Business logic customization | + | + | – | +/- | + |
Data model customization | + | + | + | +/- | + |
UI customization | + | + | +/- | +/- | + |
Code quality and readability | – | +/- | +/- | +/- | + |
Doesn’t affect performance | + | + | – | + | + |
Risk of software regression | High | High | Low | Medium | Medium |
The complexity of long term support | Extremal | Extremal | Low | Medium | Medium |
Scalability | Low | Low | High | High | High |
如你所见,扩展方式是很强大的,但它有一条不足,就是在运行中动态微调的能力不够。为了克服这个问题,CUBA也提供了全面的EAV模型和插件/脚本方式的支持。
翻译自:http://www.javacodegeeks.com/2015/07/how-to-develop-a-highly-customizable-product.html
相关推荐
- **案例介绍:** 李维斯公司通过其PersonalPairTM品牌牛仔裤,实现了对牛仔裤的高度定制化。客户可以根据自己的腰围、臀围、裤长和内接缝进行量身定做,只需多支付10美元。公司采用基于PC的系统,从库存数据中筛选...
Shopware是一款基于PHP开发的高度可定制的电子商务软件,旨在为商家提供灵活且功能丰富的在线购物平台。这款开源的电商解决方案以其强大的灵活性、可扩展性和创新性在业界受到广泛关注。 1. **PHP开发**:Shopware...
在React Native领域,开发人员经常寻求创建具有独特用户体验的应用程序,而`react-native-deck-swiper`正是这样一个组件,它提供了类似Tinder的卡片滑动功能,并且具有高度的定制性。这个组件允许用户在应用中实现...
【标题】可定制高度画廊jquery插件是一个用于创建灵活且可自定义高度的图片展示画廊的jQuery插件。这种插件在网页设计中非常常见,尤其适用于那些需要展示大量图片并希望保持布局整洁、视觉效果吸引人的场景。通过...
Apache ECharts 是一个免费、功能强大的图表和可视化库,提供了一种简单的方法,可以为您的商业产品添加直观、交互式和高度可定制的图表。它是用纯JavaScript编写的,基于zrender,这是一个全新的轻量级画布库。
随着SaaS模式的普及,一个常见的疑问是:SaaS是否可以进行定制开发?答案是肯定的,尽管SaaS通常以标准化服务为主,但为了满足不同企业的独特需求,定制化开发是必要的。 1. **配置**:在SaaS环境中,配置是指调整...
随着人们对个性生活追求的提高,个性化印刷产品越来越多,纪念册、照片书、相册、台历、挂历、扑克、明信片、LOMO卡、名片、T恤、杯子、海报等等均可实现在线定制,为了推动个性化定制市场的快速发展,我们开发了Q...
AI Agent作为一种高度智能化的软件系统,已经在多个领域展现出巨大的潜力和价值。通过结合先进的自动化技术、智能决策支持、自然语言处理、机器学习以及个性化服务等功能,AI Agent不仅能够极大地提升企业的运营效率...
其中,ArcGIS Explorer作为一款功能强大的GIS客户端软件,不仅提供了丰富的地图浏览、分析功能,还支持高度定制化开发,满足不同行业用户的特定需求。本文将深入探讨ArcGIS Explorer的基础知识、外观定制方法以及...
在这个“Ajax+Spring+Hibernate开发的个性化定制系统”项目中,这三个技术被巧妙地整合在一起,以提供一个高度用户定制化的体验。 Ajax(Asynchronous JavaScript and XML)是一种在无需重新加载整个网页的情况下,...
总的来说,大规模定制模式下的敏捷产品开发要求企业具备高度的灵活性和创新能力,能够在保持低成本的同时,快速推出符合消费者个性化需求的产品。通过有效的产品多样化管理、标准化实施以及优化定制流程,企业能够在...
嵌入式产品开发流程是将计算机硬件和软件技术与特定应用相结合的过程,旨在创建高度定制化和专用的系统。在IT行业中,嵌入式产品开发是至关重要的领域,广泛应用于消费电子、医疗设备、汽车系统、工业自动化等诸多...
2. 竞争目标:大批量生产的目标是低成本地开发、生产、销售标准产品,而大批量定制的目标是为几乎每个客户提供他们真正想要的定制产品。 3. 市场需求:大批量生产适用于需求稳定的大市场,而大批量定制则适用于需求...
【Android开发-NumberPicker作为仪表库】 ...如果你的项目需要一个既美观又可定制的数字选择器,那么这个库可能是一个值得考虑的选择。通过深入学习和使用,开发者可以充分利用它的各种功能,提升应用的用户体验。
随着人们对个性生活追求的提高,个性化印刷产品越来越多,纪念册、照片书、相册、台历、挂历、扑克、明信片、LOMO卡、名片、T恤、杯子、海报等等均可实现在线定制,为了推动个性化定制市场的快速发展,我们开发了Q...
Java开发的一款快捷高效的私有云和在线文档管理系统,为个人网站、企业私有云部署、网络存储、在线文档管理、在线办公等提供安全可控,简便易用、可高度定制的私有云产品。采用windows风格界面、操作习惯,无需适应...
定制型产品开发是指新产品是现有配置的稍许变异项目的相似性将允许实施流畅和高度结构化的开发过程的类型。该类型的产品开发需要具备快速响应客户需求的能力。 (五)高风险型 高风险型产品开发是指技术和市场不...
标题中的“非常牛的可定制可扩展的CMS”指的是一个高度灵活、功能强大的内容管理系统(Content Management System,简称CMS),这种系统允许用户根据自身需求进行定制和扩展,以适应不同的业务场景。描述中提到该CMS...
随着人们对个性生活追求的提高,个性化印刷产品越来越多,纪念册、照片书、相册、台历、挂历、扑客、明信片、LOMO卡、名片、T恤、杯子、海报等等均可实现在线定制,为了推动个性化定制市场的快速发展,我们开发了Q...