业务链就是设计模式里提到的责任链,也有叫职责链,名字不关键,重要的是理解它能用来解决什么问题。
我们用一个例子来说明。现在每个企业基本都用视频会议,输入一个会议密码(也有叫会议号)就可以直接入会,在入到会中之前是需要做一些能否入会的校验,例如下面流程图示意:
流程看起来是不是挺简单?
但这么清爽的入会流程估计只会出现在产品的第一个版本,很快就来了几条需求:
看起来只是需要一些逻辑判断,加几个if-else就能搞定,伪代码示例如下:
…… // 前面的产品权限校验完
if <没有主持人权限> { // 主持人权限校验,区分测试帐号和付费帐号
if <测试帐号> {
return <引导购买帐号错误码>
} else {
return <联系管理员开通主持人权限的错误码>
}
}
if <会议锁定> and <参会人身份入会> { // 会议锁定校验,只针对参会人
return <会议被锁定不能入会的错误码>
}
if <设置了不允许参会人提前入会> and <参会人身份入会> and <会议未开始> {
return <不允许参会人提前入会的错误码>
}
…… // 继续其它逻辑
过了一段时间,需求又来了:
你估计已经感觉到有些不好的苗头,但一时间可能也没想好怎么改善,并且产品要的又急,就只能顺着前面的路继续加逻辑。
但问题是,如果再来一波需求呢,比方说:
我们是否还沿着之前的路继续追加?
代码开发如同赶路,在适当的时候需要停下脚步,分析下需求演变特点,看看前面的方向。
如果只看单个需求本身,加个if-else确实是最快的做法,但如果持续不断的加,味道就渐渐变了。
像上面这种情况,估计你已经不愿意再加if-else了,原因倒不是新加这一个if-else能让软件一下子复杂到什么程度,而是我们相信了一个事实:这块儿业务后续还会持续增加,就像下面时间轴呈现的一样,需求会随着时间的车轮不断涌现出来。
事实也确实是这样,大家虽然不一定都开发过视频会议,但基本都用过视频会议软件,入会校验这个业务只要稍微发散下思维就还能再抛出一大批需求:
编号 | 需求点 |
---|---|
1 | 免费帐号要有会议时长限制,超过60分钟自动结会 |
2 | 免费帐号不能连续主持会议,要间隔N分钟后才能主持下一场 |
3 | 软件需要淘汰老版本,要求在入会时新版本检测并强制升级 |
4 | 周期会议的结会规则与单场不同,需要特殊处理 |
5 | 大型会议需要扩展支持助理身份入会来辅助管理会议 |
6 | 专业会议需要支持隐身入会来检查会议的合规性 |
7 | 新支持了语音加密会议,对入会终端有最低版本要求 |
8 | 新支持了等候室,对入会终端也有最低版本要求 |
…… | …… |
当加的业务越来越多时,整个入会的业务分布就会逐渐超出人脑能一下子理解的程度,如下图所示
是不是感觉有点乱?
图都是概括归纳过的,实际的代码逻辑可能远比图要混乱。研发习惯的是顺序思维,来一个需求就往接口里加几行代码,如果没有专门治理,长期累积的业务代码就会像一个泥团一样染在一起,分都分不开。
加上需求还在持续不断的扩张中,如果没有一个模型来梳理和管理这些业务逻辑,后续将会面临越来越棘手的维护工作:
这些问题在业务简单的时候,可能都不会是问题。但是,当业务代码变得复杂时,每次评估代码改动带来的影响会变得越来越困难,因评估不完整而意外引入的问题频率也会越来越高,入会开始变得不稳定,研发不再敢改动这部分代码。
基于对未来的判断团队可能会痛定思痛,下决心对这块儿业务的代码重新设计,找一种模型来支撑这种不断变化的需求。
重新设计说起来容易,真正要下手时可并不简单,一团乱麻的情况下理解都有困难,又谈何设计?
所以重新设计之前,一个非常必要的工作是梳理业务,那具体梳理什么呢?个人认为以下几点是必要的:
梳理业务并不容易,尤其是对于中途接手的人,需要沉下心来、刨根问底才能对业务有比较透的理解。梳理完后可以得到一个业务分组归类图,部分示意如下:
理清楚业务后,就可以设计一个模型来适配这些业务,同时要留有一定的扩展空间。
细心观察会发现,这些业务是有一定共同特征的:
基于这些业务都是在处理入会相关数据,可以把入会需要的数据组合为一个结构体来共用:
有了上面的共同特征后,就可以抽象一个用于入会校验的业务处理接口,而每个具体业务则实现这个接口,如下示意:
实现了此接口的具体类型我们称之为业务处理器,每个业务处理器在接口方法内封装自己职责范围内的业务逻辑即可,这里以主持人帐号状态检测为例示意如下:
// 主持人权限校验器
type HostStatusValidator struct{}
func NewHostStatusValidator() join.IHandler {
……
}
func (h *HostStatusValidator) Handle(jcdt *def.JoinData) error {
// 校验主持人的产品状态
if err := h.validateProductStatus(jcdt); err != nil {
return err
}
// 校验帐号的主持权限
if err := h.validateIdentity(jcdt); err != nil {
return err
}
return nil
}
// 校验主持人产品状态
func (h *HostStatusValidator) validateProductStatus(jcdt *def.JoinData) error {
……
}
// 校验主持人权限
func (h *HostStatusValidator) validateHostPermission(jcdt *def.JoinData) error {
……
}
用类似的方式,按照业务职责逐一实现每个业务处理器后,接下来便是构造业务链。
构造并执行业务链:
而这些业务处理器的代码,则可以集中化管理存放:
用业务链来重新设计后,不论是业务的运行过程,还是静态的代码结构,是不是都看着清爽多了?
其实好处还不止于此。
上面的入会校验是比较复杂的,比较适用于通用客户端。但实际产品开发时,要面对的入会场景往往不止这一种,例如:
这些场景往往需要快速入会,不需要走那么多复杂的校验。为此,我们可能需要按场景来定制不同的入会校验链。
这时候,业务链设计的另一个好处就体现出来了。按照业务拆分后的每个处理器都是独立、可复用的,我们直接根据需要自由重组不同场景的入会校验链即可,如下图所示:
我们只构造了两个业务链数组,对已有的业务逻辑完全没有改动,就轻松实现了不同场景的入会校验。这要是放在之前一片泥团的代码里,几乎不可想象。
上面我们仅仅是讨论了入会中校验子流程的处理链构造过程,而入会中的其它一些子流程并没有涉及,比如:
其实这些子流程也往往不只一个业务逻辑,像数据加载子流程,就需要加载帐号数据、会议数据、会议设置数据、参会人信息数据等,会后通知也类似。所以其它子流程也可以用业务链模型来拆分和治理代码,最后再用一个调度器把这几个子流程串起来就构成了整个入会接口的业务流程。
这样,我们就能组合多个子功能的业务链来完成一个复杂性更高的大功能。
在这篇文章里,我们以视频会议的入会业务为案例,讨论了如何用业务链思想来拆分和重组复杂的业务代码,通过明确职责来实现业务单元的独立复用,同时达到代码的长期可维护和可扩展性目标,这是一种治理代码复杂性、应对需求频繁变化的利器。
除核心流程外,真实使用时,还可以加入一些辅助性的细节设计,例如:
我们今天讨论的是代码已经腐化后,不得已而为之的一次重构优化,其实相比于优化,如何防止代码腐化更应该得到重视和警觉。
代码腐化的过程就像温水煮青蛙,它是在日常需求迭代中不知不觉发生的。在腐化的过程中,每个经手代码的人都只是沿着之前的老路让它变坏了一点点而已,但日积月累下来,软件最终就是变得逻辑混乱、问题频出、无法维护下去。在这期间,每个人都有一点责任,但又没有人是罪魁祸首。
所以靠一次重新设计并不能彻底解决代码腐化,因为后续维护过程中坏味道的种子还是会一点一点埋下。所以我们需要做的不仅仅是设计一个框架来支撑现在的业务,还需要在日后迭代中不断的审视和微调这个框架,让它能够始终与最新业务方向相匹配,与需求俱进。
最后,用一句话与大家共勉:每次离开时,确保比你来的时候更干净,哪怕只是一点点。