您是否听说过:“我们非常喜欢您的产品……除了一些小细节”。 然后,CIO推出了一系列其他“必备”要求的清单,其中有数百个要求添加到您的惊人产品中。 您是否听说过,甚至说过:“团队,我们即将签署一份利润丰厚的合同,但是……”? 然后,客户对附加功能的愿望清单使开发人员感到头疼。
那么,如何使产品远离客户的潜在危险想法,同时又使他们满意呢? 对于专门设计为以特定方式运行但现在具有大量附加组件的产品,如何保持最高性能水平呢? 为已开发的解决方案提供不间断且出色的支持的基本需求将带来多少挑战?
在商业世界中,产品定制已成为越来越令人期望的要求,并且响应于这种客户需求,已经发展了许多通用实践。 您可以在下面找到典型方法的概述。 如果您已经熟悉它们,那么欢迎您直接滚动至“ 扩展方法 ”部分,并了解我们如何以我们认为更有效的方式解决这些挑战。
一体
定制的最直接,最明显的解决方案是实施一个核心产品所需的所有内容,然后采用“ 功能切换 ”技术来满足每个特定客户的需求。
多合一方法的主要优点是保留了整体产品,这对于某些类型的产品似乎是一种很好的方式,这些产品通常可以满足业务需求,而无需进行广泛的定制。
这种方法的自然局限性隐藏在“不需要太多定制”的假设中。 通常,产品开发就是从这种信念开始的,但是经过多次交付,您才真正意识到需要多少特定于客户的功能。 陷入困境的情况并不少见。 拒绝定制开发并可能失去客户,或者将源代码转换为具有针对单个客户的功能的垃圾箱 ,这些功能对于大多数最终用户而言可能是无用的。
您会选择哪个选项? 显然,在艰难和艰难的地方之间进行选择并不是成功的方法。
简介:仅当您确定需要罕见且有限的定制时,“多合一”方法才是合适的选择。 否则,您将面临可管理和可支持产品与客户满意度之间的选择。 让我引用杰里·加西亚(Jerry Garcia)的话:“不断选择两种邪恶中的较小者仍然是选择邪恶”。
分枝
如果重大定制是交付的“必不可少”部分,则不能采用“多合一”技术。 还有另一种简单的方法- 分支 。 您只需分支产品代码库,然后单独进行更改即可。
将分支与“多合一”进行比较,最大的优点是,没有适用于自定义范围的限制。 您使用单独的分支来满足不同客户的特定要求,并避免在同一代码库中混合使用所有功能。
但是,它的另一面可能会在产品发展方面变成死胡同。 显然,产品分支是主要的开发空间:大多数错误修正,改进和新功能都首先被应用到产品中。 因此,需要频繁合并以使所有定制分支与核心产品保持同步。 只要原始产品源代码不受定制分支的影响,合并是一项简单的操作,否则,合并将变得非常耗时,并可能导致不可避免的回归错误。
如果您仅限于很少的自定义分支,则此方法仍然可以使用。 但是,随着交付实例数量的增加,面临“通过合并实施酷刑”的可能性迫在眉睫。
简介: 分支方法无疑是非常灵活和直接的-产品的任何部分都可以修改。 但是,交付后阶段可能非常费力,随着时间的推移变得更加困难,并且不太可能导致交付大量可管理的定制分支。
实体-属性-价值模型
实体-属性-价值模型 (又名对象-属性-价值模型,垂直数据库模型和开放式架构)是众所周知的且被广泛使用的数据模型。 EAV支持动态实体属性,通常与标准关系模型并行使用。
从产品化的角度来看,使用EAV的主要优点是您可以“按原样”交付产品,然后通过在运行时添加所需的属性来调整数据模型,从而保持源代码的整洁。
与以往一样,还有一个缺点:
- 有限的适用性–仅通过允许向实体添加属性来限制EAV模型,然后根据预编程的逻辑将其自动嵌入到UI中。
- 额外的数据库服务器负载–垂直数据库设计通常成为企业应用程序的瓶颈,企业应用程序通常使用大量与它们相关的实体和属性进行操作。
- 最后,如果没有复杂的报告引擎,就无法想象企业系统。 EAV模型具有“垂直”数据库结构,因此有可能带来许多麻烦。
简介: 实体-属性-价值模型在某些情况下具有很大的价值,例如当需要提供通过具有附加信息性数据而实现的灵活性时,该信息性数据未在业务逻辑中明确使用。 换句话说,EAV具有良好的适度性,例如,除了标准的关系模型和插件体系结构之外 。
插件架构
插件体系结构是最流行且功能最强大的方法之一,其中功能逻辑作为单独的工件(称为插件)保存。 要覆盖现有的开箱即用行为并运行插件,必须在产品源代码中定义“定制点”(即扩展点)。 “定制点”是源代码中的某个位置,应用程序在该位置上浏览附加的插件,以检查插件是否包含要在此处运行的替代实现。 插件体系结构的一种变化是外部脚本。 在实现功能实现并将其作为脚本存储在外部时。 脚本调用也由预定义的“定制点”控制。
使用这种插件方法,可以使产品“清洁”特定的客户要求,“按原样”交付核心产品,并根据插件或脚本的要求自定义行为。 这种方法的另一个优点是管理完善的更新过程。 产品和插件功能的完全分离使彼此之间可以独立更新。
当然存在限制:主要限制是不可能完全知道将来可能会提出哪些自定义要求。 因此,只能猜测应该在何处嵌入“定制点”。 当然,这些可以作为缓解“防万一”计划的零星散布在各处,但这将导致代码可读性差,硬调试以及复杂的支持。
简介:如果易于预测“定制点”,那么插件架构确实可以工作,但是请注意,“定制点”之间的定制是不可能的。
扩展方法
我们在企业软件开发平台CUBA中实施了独特的方法。 正如我们上一篇文章所述 ,CUBA是一种非常实用的活生物体,它是通过开发人员驱动的演变过程创建的。 因此,根据我们在现成产品上的丰富经验,我们提出了两个最终要求:
- 客户特定的代码应与核心产品代码完全分开
- 产品代码的每个部分都应该可以修改
我们设法满足了这些要求,并通过我们的“扩展”机制实现了更多目标。
CUBA扩展
扩展是一个单独的CUBA项目,它继承了基础项目(即您的核心产品)的所有功能,并将其用作库。 显然,这使开发人员可以实现全新的功能而不会影响父项目,但是由于使用了“ 开放继承”模式和特殊的CUBA工具,您还可以覆盖父项目的任何部分。 总之,扩展是实现本文开头讨论的数百个“少量次要细节”的地方。
实际上,每个CUBA项目都是CUBA平台本身的扩展-因此它可以覆盖任何平台功能。 我们自己采用了这种方法,以从核心平台中分离出一些现成的功能(全文搜索,报告,图表等)。 因此,如果您在项目中需要它们,则只需将它们添加为父项目-就是这样,是多重继承!
您可以用相同的方式构建分层的定制模型 。 这听上去很复杂,但是很合理。 让我举一个真实的例子: Sherlock –是Haulmont完整的出租车管理解决方案,支持从预定,派遣到应用程序和计费的出租车业务的方方面面。 该解决方案涵盖了客户业务的许多不同方面,其中许多是与位置相关的。 例如,所有英国出租车公司都具有相同的法律法规,但是其中许多法规不适用于美国,反之亦然。 显然,我们不想在核心产品中实施所有这些规定,因为:
- 这是“特定于操作区域”的功能
- 当地法规可能会对不同国家的出租车队运营产生完全不同的影响
- 一些客户根本不需要监管控制
因此,我们组织了多级扩展层次结构:
- 核心产品包含出租车业务的通用功能
- 定制的第一层实现了区域特性
- 第二层定制涵盖了客户的愿望清单(如果有的话!)
干净利落。
如您所见,通过使用扩展,您既不需要分支也不需要在核心产品中集成所有需求 ,因此代码保持简洁且易于管理。 听起来真是太好了,所以让我们看看它是如何工作的!
向现有实体添加新属性
假定我们具有用户实体的产品定义,该产品定义由两个字段组成:登录名和密码:
@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
}
现在,我们的一些客户提出了一项附加要求,即向用户添加“家庭住址”字段。 为此,我们在扩展中扩展User实体:
@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;
}
}
您可能已经注意到,除@Extends以外的所有注释都是通用的JPA注释。 @Extends属性是CUBA引擎的一部分,它甚至在整个产品功能上都将User实体全局替换为ExtUser 。
使用@Extends属性,我们强制平台执行以下操作:
- 始终创建“最新子级”类型的实体
User user = metadata.create(User.class); //ExtUser entity will be created
- 在执行之前转换所有JPQL查询,以便它们始终返回“最新子集”
select u from product$User u where u.name = :name //returns a list of ExtUsers
- 始终在关联实体中使用“最新子项”
userSession.getUser(); //returns an instance of ExtUser type
换句话说,如果声明了扩展实体,则基础实体将在整个解决方案(产品和扩展)中被放弃,并且被扩展实体全局覆盖。
屏幕定制
因此,我们通过添加地址属性扩展了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>
<fieldGroup id="fieldGroup" datasource="userDs">
<column>
<field id="login"/>
<field id="password"/>
</column>
</fieldGroup>
<iframe id="windowActions" screen="editWindowActions"/>
</layout>
</window>
如您所见,CUBA屏幕描述符表示为普通XML。 显然,我们可以简单地在扩展名中重新声明整个屏幕描述符,但这意味着要复制粘贴其中的大部分内容。 因此,如果将来产品屏幕中发生某些更改,我们将必须手动将这些更改复制到扩展屏幕中。 为避免这种情况,CUBA引入了屏幕继承机制,您所需要的只是描述对屏幕的更改:
<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平台使用Spring框架,该框架构成了平台基础结构的核心部分。
例如,您有一个中间件组件来执行价格计算过程:
@ManagedBean("product_PriceCalculator")
public class PriceCalculator {
public void BigDecimal calculatePrice() {
//price calculation
}
}
要覆盖价格计算实现,我们只需要执行两个简单的操作。
首先,扩展产品类并覆盖相应的过程:
public class ExtPriceCalculator extends PriceCalcuator {
@Override
public void BigDecimal calculatePrice() {
//modified logic goes here
}
}
最后,使用产品Bean标识符在Spring配置中注册新类:
<bean id="product_PriceCalculator" class="com.sample.extension.core.ExtPriceCalculator"/>
现在, PriceCalculator注入将始终返回扩展类实例。 因此,修改后的实现将在整个产品中使用。
扩展基础产品版本
随着核心产品的发展和新版本的发布,您最终将决定将扩展程序升级到最新的产品版本。 过程非常简单:
- 在扩展中指定基础产品的新版本。
- 重建扩展名:
- 如果扩展是基于产品API的稳定部分构建的,则可以运行它。
- 如果对产品API进行了一些重大修改,并且这些修改与扩展中实现的自定义重叠,则有必要在扩展中支持新产品API。
大多数情况下,产品API在每次更新之间都不会发生重大变化,尤其是在次要版本中。 但是,即使API发生了“大爆炸”,产品通常也至少保持几个未来版本的向下兼容性,并且旧的实现被标记为“已弃用”,从而允许将所有扩展迁移到最新的API。
结论
作为一个简短的摘要,我想以表格形式说明比较分析的结果:
一体 | 分枝 | 电动汽车 | 外挂程式 | CUBA扩展 | |
独立于架构 | + | + | – | – | – |
动态定制 | – | – | + | +/- | – |
业务逻辑定制 | + | + | – | +/- | + |
数据模型定制 | + | + | + | +/- | + |
用户界面定制 | + | + | +/- | +/- | + |
代码质量和可读性 | – | +/- | +/- | +/- | + |
不影响性能 | + | + | – | + | + |
软件回归的风险 | 高 | 高 | 低 | 介质 | 介质 |
长期支持的复杂性 | 极端的 | 极端的 | 低 | 介质 | 介质 |
可扩展性 | 低 | 低 | 高 | 高 | 高 |
如您所见,扩展方法功能强大,但是它缺少的一件事就是能够动态地微调系统(动态定制)。 为了克服这个问题,CUBA还提供了对实体属性值模型和插件/脚本方法的全面支持。
我希望您会发现此概述很有用,当然您的反馈意见也将受到赞赏。
翻译自: https://www.javacodegeeks.com/2015/07/how-to-develop-a-highly-customizable-product.html