由用户挑选自己所需要的产品,动态装配出符合用户需要的软件功能。用户的需求表单可能是单参数的,例如通过不同按钮下的订单,orderType 就不同。 用户的需求也可能是分阶段,多参数的,例如用户可能先选了专车,然后又表达了自己需要儿童座椅。有很多种装配多个 Git 仓库成为产品族的方式,但是 Autonomy 有很大差异。
产品族问题也可以理解为“最终用户编程”问题,它本质上是用户来动态装配组件,而不是每一种组合后的订单,每一种组合后的商品,完全由程序员预编程好。
最简单的实现办法是添加一个代表类型的字段,例如 orderType。这种做法就是“预组合”,把所有的可能性都提前叉乘出来,变成单个变量。 这种做法有两个节点:
- 写入 orderType:如果和用户的按钮没有一一对应关系,在写入 orderType 的时候还需要有一个汇总多变量为单变量的逻辑
- 读取 orderType:所有需要差异性的地方,都要对 orderType 进行解读
也就是 orderType 成为把多个 Git 仓库粘合起来的一个东西,大家都认这个字段的含义。这种做法会有如下问题
- 难以支持分阶段表达的需求:比如用户下单的时候是一种类型,中途经过了机场,业务含义发生了变化怎么办?更新 orderType 吗?
- 难以添加新的 orderType:加了新的值,所有读取 orderType 的地方都要更新自己的判断逻辑
- 组合爆炸:如果变化的维度很多,叉乘出来的 orderType 可能是指数增长的
因为单字段的缺陷,所以实践中往往是一开始以单字段开局,然后很快地滑落为多字段的实现方案。然后又在两种方案之间反复横跳
- 添加新的 orderType:可以加一个值同时代表很多含义,不用修改表结构
- 添加新的字段:不关心这个字段的Git仓库可以不改。然而如果很多Git仓库都关心这个字段,不仅仅都要修改,而且要改接口定义
为了避免频繁加字段,就出现了用一个 bit 来代表某个 feature 是否激活的搞法。搞一个 64bit 的 int64 字段可以代表 64 个 feature flag。
公司内会有复杂的组织架构,他们都会对什么是他们负责的有自己的看法。类型字段怎么设置并不影响给不同部门出的报表,通过离线计算总是可以算出业务线所需要口径的数据的。 要避免按照公司的组织架构来设计字段,因为组织架构不是一成不变的,会拆分也会重组。
除了多字段之外,还可以通过多表来实现组合。比如对于 Order,我们可以定义另外一个 OrderShipment 表,代表快递。定义 OrderSelfPickup 表,代表自提。 通过查找这个 order 是否在 OrderShipment 中,得知这个订单是否走快递配送,以及配送的目的地。 这种做法,相当于加 feature flag,每个新的变种,都是一个新的独立 bit。 也就是压根没有 xxxType 一说,只有一堆 isXXX 这样的 feature flag。 加字段,或者加表就是麻烦,单纯从 Autonomy 的角度来说,肯定是一堆独立的 Feature Flag 更独立自主。 实际工程实践中,占主导性的考虑往往是从IO效率和稳定性出发。或者说是为了迁就落后的基础架构。
多表可以理解为每个 Git 仓库定义了自己的“合同”,自己的业务记录在自己的合同内部,而不是都由“order”来夹带。
实践中,order经常变成 Map<string, any>
这样的玩意,就是所有新的业务,都要往 order 上夹带新私货。
我们完全可以把具体类型的订单,拆分成一个独立的合同,比如常规购买,团购,周期购。
这样新加一类需求,可以用加表来实现。订单本身只提供销售额,销售时间这样的最大公约数,用来支持 GMV 统计等需求。
要不然实现统计 GMV 需求的时候又麻烦了,得一对一做网状集成,而不是都汇总到 order 上,然后做星型集成。
面向对象编程发展多年,凝结出来的一句智慧是“用组合代替继承”。用多个表来组合,就是用多个对象来组合。 本质上可以理解为一个内存中的组合对象,持久化到多个数据库表中。
我们要让一个对象,比如说,订单有如下接口
class Order {
get totalAmount(): number {
// 多态实现
}
get merchandisedAt(): Date {
// 多态实现
}
}
我们也可以把订单定义两个冗余字段
class Order {
// 销售额
totalAmount: number;
// 销售额计入哪天
merchandisedAt: Date;
}
如果我们有不同的合同来代表多样化的业务流程,统计销售 GMV 的工作就会很有挑战。在离散型 UI 里,我们看到了订单列表的例子,其选择就是虚方法的搞法。 如果下游是离线统计,BI 分析,冗余字段的搞法会更方便对接一些。这样就要求每种具体的合同,如果要把自己归纳为某种订单,都需要有一个对应的Order,同时冗余两个字段。 从会计的工作方式里我们也可以看到这样的做法。无论业务的原始凭证多么多样,会计都要誊写成会计账目。 某种程度上,会计的账目不也是冗余字段吗?
当我们拆分出了多个合同之后,每个合同可能分布在不同的 Git 仓库里。 从依赖关系的角度来说,要尽可能避免网状的依赖关系。 比如说分佣,需要知道订单是团购单或者周期购吗?可能是无关的,只需要是一个订单就可以了。 比如说退款,需要知道团购的订单和直接购买的订单退货流程是不同的吗?可能是相同,退货申请可能和订单关联就可以了。 这就是订单做为“业务泛型指针”的作用,把订单放在依赖的最底层,所有的上层具体合同通过订单进行彼此互相引用。 类似商品等泛化的概念也是如此。 与离散型UI的拆分不太一样的地方在于,这种倒置的依赖关系是怎么工作的。 离散型UI中,底层的集成组件是利用虚函数的方式来引用上层的具体实现。 而在产品族中,底层的订单有两个和上层具体实现的互动方式:
- 强制类型转换:订单做为一个
void*
或者interface{}
一样的无类型指针,让具体的业务代码去 downcast(强制类型转换)成自己含义下的订单。 - 数据同步:也就是每个具体的业务把数据冗余一份给订单。
如果判断变化不会很多,作用域控制在单个 Git 仓库内部,可以选择单字段的模式。如果预判将来会有各种幺蛾子,需要用来集成多个Git仓库,还是选择多字段,或者多表更不容易埋坑。