“对新增开放,对修改关闭。”——开闭原则。

这里分享一个我在业务系统设计过程中常用的一个“复合模式”,用作一个在业务系统设计中运用“开闭原则”的例子。

背景

这是一个账务系统,负责处理各类业务流程中发生的若干个账户之间的转账相关逻辑,包括账户余额的变更、以及各账户的流水记录。

这个系统的复杂度在于:不同的业务流程,所需要操作的账户、金额的计算公式、以及流水的类型,都有很大的差异;即使是同一个业务,里面也会细分为多个子业务,账户、金额、流水类型又各不相同。而且,业务、子业务还会不断的新增和变更。这就要求我们在设计时,必须充分考虑扩展性。

思路

首先,业务流程虽然种类繁多,但是抽象、概括之后,其实核心逻辑非常简单。第一步当然是校验,然后是账户查找、计算金额等数据准备工作,准备好必要的数据之后就可以记账了,记账完成后做一些收尾的操作。

这样一来,所有的相关业务都可以放到这同一个“抽象”层次内来做处理了。如下图所示。 分发模式-业务模型

扩展性问题则是这样解决的。这个“抽象”层次内,最顶层的“服务提供者”只提供一个分发服务。它会根据外部传入的参数,选择对应的业务服务提供者,并将参数交给它来处理。

而“业务服务提供者”的最顶层,同样只提供分发服务。它会根据一些更细致的规则,选择对应的子业务服务提供者,然后将参数交给子业务服务提供者去处理。

这样的服务分发层级,在现实生活中可以找到很直观的例子。在一些大医院的门诊部大门口,会有一个分诊台。分诊台的护士会告诉你应该挂哪个科室的号。而在一些大科室的门口,又会有一名护士负责叫号,并指点被叫到的病人去找哪位医生。实际上,这也是我做这个“分发”服务的一个灵感来源。

在这个“分发”的框架下,新增一套业务是非常简单的。并且,由于真正处理业务功能的服务提供者只需要专注于自己的“一亩三分地”,与其它的业务之间很少发生耦合,因此,在必须对某些功能进行修改时,影响范围也会非常的小。

理论上,“分发”这个动作可以无限嵌套,因此无论多复杂的业务逻辑,都可以通过不断的分发处理来进行简化。只不过这样做会使得线程处理栈变得非常深,给理解系统和debug和trouble shooting增加不必要的麻烦。我们的系统做到二级分发,已经有“类爆炸”的困扰了。

类图

分发模式-类图

顶层的BizAccountEventService就是我们匹配“业务抽象”所建立起来的接口。所有的记账相关业务功能,无论是一级分发、二级分发,还是实际的业务功能处理,都是这个接口下的一个实现类。

BaeServiceAsDispatcher

只有两个类直接实现了BizAccountEventService接口,其中之一就是BaeServiceAsDispatcher,即业务抽象内最顶层的“服务提供者”——一级分发器。一级分发器需要一个服务工厂,以根据不同的参数提供不同的接口实现类。这个工厂既可以是一个简单的Map,也可以做得更复杂一些。我们单独创建了一个工厂类,这样可以方便地将一级分发扩展为二级分发。

在一级分发器中,服务工厂提供的类实际上都是二级分发器。二级分发器与一级分发器的逻辑实际上是一样的,也是利用工厂来根据不同的参数获得不同的接口实现。因而,二级分发器都继承自一级分发器,实例之间的差别基本上只是一个工厂。

BaeServiceAsSekeleton

直接实现BizAccountEventService接口的另一个类是BaeServiceAsSekeleton。这个类是一个模板类,它定义了真正的业务操作的核心步骤:校验、预处理、转账、后处理。所有的业务处理类,都必须是它、或者它的子类的实例。

但是,与教科书上的“模板模式”不同的是,BaeServiceAsSekeleton中的模板模式,并不是通过继承、而是通过组合来实现的。这个模板中的四个步骤,被分别委托给了三个接口(BaeValidatorBaeEditorBaeTransfer)。这样做可以增加不同子业务之间的代码复用性。例如,业务甲的逻辑是ABCD,业务乙的逻辑是EFGH,而业务丙的则是ABGH。这种情况下,由于Java单继承的限制,业务丙无法通过继承甲或乙的类来完全复用代码,但是组合的方式却可以轻松做到。

关于这个接口,项目组内曾有过争论。有同事认为,应当为BaeServiceAsSkeleton类提供另一个接口,以便于保持BizAccountEventService接口的层次简单。这样做当然可以,不过我们没有把精力放在这件事上。

开闭

那么,回过头来看“开闭”。“对新增开放”很好理解,那么什么是“闭”呢?

实事求是的说,我们不可能完全地把“修改”关在门外。在这个“分发”方案里,我们也仍然需要通过修改分发相关代码,来增加新的业务、或变更原有业务。我们能够关闭的、也许也是我们真正想关闭的,是大量的、大范围的修改代码、是这样做所带来的不可控的风险。

以这个“分发”方案为例。当一种业务逻辑发生变化时,我们需要修改的仅仅是分发相关代码、以及这一种子业务所涉及的部分子类。这种修改的影响范围被牢牢锁定在业务抽象之下、甚至是单个子业务的相关抽象之下,因而,其中的风险是清晰明了的,因而也是可以有效控制住的。甚至于,我们可以保持原有业务代码完全变,而通过继承、多态等方式扩展出新的业务逻辑,从而用“废除+新增”的方式完全替代“修改”——当然,这种方式会带来很多冗余代码,值得商榷。

但是,把不可控的风险关在门外——我们做到了。