治理需求复杂多变的利器——业务链

1. 引言

业务链就是设计模式里提到的责任链,也有叫职责链,名字不关键,重要的是理解它能用来解决什么问题。

我们用一个例子来说明。现在每个企业基本都用视频会议,输入一个会议密码(也有叫会议号)就可以直接入会,在入到会中之前是需要做一些能否入会的校验,例如下面流程图示意:

  • 需要校验会议密码的有效性
  • 需要校验主持人帐号的产品开通状态
    治理需求复杂多变的利器——业务链_第1张图片

流程看起来是不是挺简单?

但这么清爽的入会流程估计只会出现在产品的第一个版本,很快就来了几条需求:

  • 未开通主持人权限的帐号不能主持会议
  • 会议锁定时不允许入会
  • 主持人未入会时不允许参会人提前入会
  • 测试帐号试用到期引导付费购买

看起来只是需要一些逻辑判断,加几个if-else就能搞定,伪代码示例如下:

…… // 前面的产品权限校验完

if <没有主持人权限> {   // 主持人权限校验,区分测试帐号和付费帐号
	if <测试帐号> {  
	    return <引导购买帐号错误码>
	} else {
		return <联系管理员开通主持人权限的错误码>
	}
}

if <会议锁定> and <参会人身份入会> { // 会议锁定校验,只针对参会人
	return <会议被锁定不能入会的错误码>
}

if <设置了不允许参会人提前入会> and <参会人身份入会> and <会议未开始> {
	return <不允许参会人提前入会的错误码>
}

…… // 继续其它逻辑

过了一段时间,需求又来了:

  • 预约会议结束/取消/不存在的检测
  • 帐号欠费引导及时付费
  • 一个帐号不能并发主持两场会议
  • 一个用户不能同时参加两场会议
  • 用户不能重复入同一场会议

你估计已经感觉到有些不好的苗头,但一时间可能也没想好怎么改善,并且产品要的又急,就只能顺着前面的路继续加逻辑。

但问题是,如果再来一波需求呢,比方说:

  • 主持人必须登录才能主持会议
  • 参会人必须实名登录才能参加会议
  • 只允许企业内部用户参会
  • 只允许注册用户参会
  • 参会人最多可以提前多长时间入会
  • 登录用户误使用别人的主持人密码检测
  • 主持人误使用自己的参会人密码检测

我们是否还沿着之前的路继续追加?

代码开发如同赶路,在适当的时候需要停下脚步,分析下需求演变特点,看看前面的方向。

2. 问题分析

如果只看单个需求本身,加个if-else确实是最快的做法,但如果持续不断的加,味道就渐渐变了。

像上面这种情况,估计你已经不愿意再加if-else了,原因倒不是新加这一个if-else能让软件一下子复杂到什么程度,而是我们相信了一个事实:这块儿业务后续还会持续增加,就像下面时间轴呈现的一样,需求会随着时间的车轮不断涌现出来。
治理需求复杂多变的利器——业务链_第2张图片

事实也确实是这样,大家虽然不一定都开发过视频会议,但基本都用过视频会议软件,入会校验这个业务只要稍微发散下思维就还能再抛出一大批需求:

编号 需求点
1 免费帐号要有会议时长限制,超过60分钟自动结会
2 免费帐号不能连续主持会议,要间隔N分钟后才能主持下一场
3 软件需要淘汰老版本,要求在入会时新版本检测并强制升级
4 周期会议的结会规则与单场不同,需要特殊处理
5 大型会议需要扩展支持助理身份入会来辅助管理会议
6 专业会议需要支持隐身入会来检查会议的合规性
7 新支持了语音加密会议,对入会终端有最低版本要求
8 新支持了等候室,对入会终端也有最低版本要求
…… ……

当加的业务越来越多时,整个入会的业务分布就会逐渐超出人脑能一下子理解的程度,如下图所示

治理需求复杂多变的利器——业务链_第3张图片

是不是感觉有点乱?

图都是概括归纳过的,实际的代码逻辑可能远比图要混乱。研发习惯的是顺序思维,来一个需求就往接口里加几行代码,如果没有专门治理,长期累积的业务代码就会像一个泥团一样染在一起,分都分不开。

加上需求还在持续不断的扩张中,如果没有一个模型来梳理和管理这些业务逻辑,后续将会面临越来越棘手的维护工作:

  1. 排查一个问题,如何快速定位到相关业务的代码位置?
  2. 新来一个需求,如何清楚的确认对已有业务逻辑是否有影响?
  3. 当有多个业务操作同一个数据时,如何保证数据不被其它业务误修改?

这些问题在业务简单的时候,可能都不会是问题。但是,当业务代码变得复杂时,每次评估代码改动带来的影响会变得越来越困难,因评估不完整而意外引入的问题频率也会越来越高,入会开始变得不稳定,研发不再敢改动这部分代码。

基于对未来的判断团队可能会痛定思痛,下决心对这块儿业务的代码重新设计,找一种模型来支撑这种不断变化的需求。

3. 设计构思

3.1 梳理业务

重新设计说起来容易,真正要下手时可并不简单,一团乱麻的情况下理解都有困难,又谈何设计?

所以重新设计之前,一个非常必要的工作是梳理业务,那具体梳理什么呢?个人认为以下几点是必要的:

  • 理清关系:梳理业务与业务之间的关系,找出哪些业务之间有依赖关系;
  • 分组归类:将业务重新组织,相似的逻辑归到一处,避免一堆零碎又相似的业务逻辑满天飞;
  • 职责明确:重新定义业务职能,这个业务是做什么的?哪些需求应该加到这个业务下?

梳理业务并不容易,尤其是对于中途接手的人,需要沉下心来、刨根问底才能对业务有比较透的理解。梳理完后可以得到一个业务分组归类图,部分示意如下:

  • 可以看到,有依赖关系的业务往往也是比较相似的业务,我们把它归到了一起;
  • 外层红框标注的是一个个明确定义的业务单元,尽量独立,相互之间依赖越少越好;
    治理需求复杂多变的利器——业务链_第4张图片

理清楚业务后,就可以设计一个模型来适配这些业务,同时要留有一定的扩展空间。

3.2 业务处理模型设计

细心观察会发现,这些业务是有一定共同特征的:

  1. 都属于入会校验类业务,依赖的数据和要处理的数据基本是相同的;
  2. 业务模型都可以归纳为对某个数据作一定的逻辑处理,并返回处理结果是正常还是异常;

基于这些业务都是在处理入会相关数据,可以把入会需要的数据组合为一个结构体来共用:
治理需求复杂多变的利器——业务链_第5张图片

有了上面的共同特征后,就可以抽象一个用于入会校验的业务处理接口,而每个具体业务则实现这个接口,如下示意:
治理需求复杂多变的利器——业务链_第6张图片

实现了此接口的具体类型我们称之为业务处理器,每个业务处理器在接口方法内封装自己职责范围内的业务逻辑即可,这里以主持人帐号状态检测为例示意如下:

// 主持人权限校验器
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 {
	……
}

用类似的方式,按照业务职责逐一实现每个业务处理器后,接下来便是构造业务链。

3.3 业务链构造

构造并执行业务链:

  • 第一步:给这些业务排一个执行顺序,有依赖的按照依赖关系排,没有依赖的按照个人理念定。这样后续来了新业务就知道该往何处插入;
  • 第二步:将这些业务处理器按照顺序组成一个链,简单实现的话,用数组就可以表示;
  • 第三步:按照顺序依次执行链条中每个处理器,相应的入会数据会在每个处理器中流转一遍,业务链执行完,结果也就出来了;
    治理需求复杂多变的利器——业务链_第7张图片

而这些业务处理器的代码,则可以集中化管理存放:

  • 把所有业务处理器的代码实现集中到一个目录下管理;
  • 给每个处理器按照业务含义来命名,通过名称就能快速定位到代码文件:
    治理需求复杂多变的利器——业务链_第8张图片

用业务链来重新设计后,不论是业务的运行过程,还是静态的代码结构,是不是都看着清爽多了?

其实好处还不止于此。

3.4 分场景业务链

上面的入会校验是比较复杂的,比较适用于通用客户端。但实际产品开发时,要面对的入会场景往往不止这一种,例如:

  • SDK整合快捷入会
  • 管控角色入会

这些场景往往需要快速入会,不需要走那么多复杂的校验。为此,我们可能需要按场景来定制不同的入会校验链。

这时候,业务链设计的另一个好处就体现出来了。按照业务拆分后的每个处理器都是独立、可复用的,我们直接根据需要自由重组不同场景的入会校验链即可,如下图所示:

治理需求复杂多变的利器——业务链_第9张图片

我们只构造了两个业务链数组,对已有的业务逻辑完全没有改动,就轻松实现了不同场景的入会校验。这要是放在之前一片泥团的代码里,几乎不可想象。

3.5 分阶段业务链

上面我们仅仅是讨论了入会中校验子流程的处理链构造过程,而入会中的其它一些子流程并没有涉及,比如:

  • 数据加载:校验中用到的入会相关数据(JoinData)的加载;
  • 会后通知:会议数据、入会记录的更新保存以及状态通知;
  • 入会选择:不同场景的入会方式选择;

其实这些子流程也往往不只一个业务逻辑,像数据加载子流程,就需要加载帐号数据、会议数据、会议设置数据、参会人信息数据等,会后通知也类似。所以其它子流程也可以用业务链模型来拆分和治理代码,最后再用一个调度器把这几个子流程串起来就构成了整个入会接口的业务流程。

治理需求复杂多变的利器——业务链_第10张图片

这样,我们就能组合多个子功能的业务链来完成一个复杂性更高的大功能。

小结

在这篇文章里,我们以视频会议的入会业务为案例,讨论了如何用业务链思想来拆分和重组复杂的业务代码,通过明确职责来实现业务单元的独立复用,同时达到代码的长期可维护和可扩展性目标,这是一种治理代码复杂性、应对需求频繁变化的利器。

除核心流程外,真实使用时,还可以加入一些辅助性的细节设计,例如:

  • 业务处理器的可开关配置;
  • 业务处理器的共性封装,像日志、外部依赖、上下文信息等;
  • 业务处理器的准入条件提取;

我们今天讨论的是代码已经腐化后,不得已而为之的一次重构优化,其实相比于优化,如何防止代码腐化更应该得到重视和警觉。

代码腐化的过程就像温水煮青蛙,它是在日常需求迭代中不知不觉发生的。在腐化的过程中,每个经手代码的人都只是沿着之前的老路让它变坏了一点点而已,但日积月累下来,软件最终就是变得逻辑混乱、问题频出、无法维护下去。在这期间,每个人都有一点责任,但又没有人是罪魁祸首。

所以靠一次重新设计并不能彻底解决代码腐化,因为后续维护过程中坏味道的种子还是会一点一点埋下。所以我们需要做的不仅仅是设计一个框架来支撑现在的业务,还需要在日后迭代中不断的审视和微调这个框架,让它能够始终与最新业务方向相匹配,与需求俱进。

最后,用一句话与大家共勉:每次离开时,确保比你来的时候更干净,哪怕只是一点点。

你可能感兴趣的:(实践案例,设计模式,golang,后端)