一、背景
之前接手了一个 springboot 项目。在我负责的模块中,有一块用户注册的功能,但是比较特别的是这个注册并不是重新注册,而是从以前的旧系统的数据库中同步旧数据到新系统的数据库中。由于这些用户角色来自于不同的系统,所以我需要在注册的时候先判断类型(这个类型由一个专门的枚举类提供),再去调用已经写好的同步方法同步数据。
伪代码大概是这样的:
1 | public void register(String type, String userId, String projectId, String declareId){ |
由于用户的类型比较多,所以当我接手的时候已经有8个 if-esle 了,由于这个项目会逐步的跟其他平台对接,要同步的用户类型会越来越多,而且也不能排除什么时候不新增,反而要取消一部分类型的同步情况。
就这个情况来说,一方面每一次新增或删除类型都需要修改 if-else 上逻辑分支,如果需要新增一些同步前的处理的步骤(根据经验这种情况几乎一定会出现的),大概率代码会直接被加在 if-else 方法里头;另一方面,这个业务的需求也有相对稳定的地方:同步方法会不一样,但是一定会根据类型来判断。出于以上考虑,我决定趁现在牵扯范围不大的时候重构一下。
二、思路
1.抽取策略接口和策略类
首先,由于每种用户类型的同步方法是由各模块自己提供的,其实已经算是抽出了策略,只是没有实现一个统一的策略接口。
但是我在这一步遇上了问题:
- 各模块的同步方法的名称不全部一样;
- 由于年代久远,旧代码是不允许改的。
代码不让改,就没法通过为旧实现类新增接口实现多态,方法名不一样,那么反射这条路子也走不通。我想到了装饰器,为每个实现类新增一个装饰器类,注册的时候通过装饰器去调用同步方法,但是这样缺点很明显,会引入一个装饰器接口+n多个装饰器类,为了优化这一个方法,反而要引入十几个类,这样反而违背初衷。
但是好在天无绝人之路,他们并不是完全没有相同点:
- 虽然参数名不一样,但是每个同步方法都需要的参数数量和类型都是一样的;
- 他们都返回一个布尔值
这让我想起了 JDK8 的函数式接口,将策略接口改造为函数式接口,由于同步方法的参数和返回值类型都是一样的,就可以直接以 Lambda 表达式的形式将各个模块的同步方法放进去,这样就不需要改动模块的代码了。
新增的接口如下:
1 |
|
2.策略池的实现
接着,为了实现原本 if-else 的逻辑,我需要一个策略池,能够建立起一个用户类型跟对应的同步策略的映射关系,一开始,我打算直接写在 register()
方法所在的类中加入以下代码:
1 |
|
但是这样在添加新的用户类型时,需要先去枚举类添加新枚举,然后再回到register()
所在的类为策略池添加策略,这个两个逻辑上相连的过程被分散到了两个地方,而且仍然要修改register()
所在类的代码。所以决定不用上述的代码,而是去对枚举类下手。
原本的枚举类是这样的:
1 | /** |
为了保证逻辑能够集中,我决定将添加策略这一过程一起放到到枚举类里,在添加枚举的时候就把策略一起放进去:
注:下文的 SpringUtils 实现了 BeanFactoryPostProcessor 接口,是一个用于从 ConfigurableListableBeanFactory 获取对象的工具类。
1 | /** |
由于由于枚举类已经相当于之前策略池的 Map 集合了,所以我们直接在里面添加一个 getSynchronizeService()
方法,用于直接获取同步方法:
1 | /** |
到目前为止,策略池已经基本完成了,我们对原本的 if-else 中的同步方法进行在次封装,现在如果需要在同步前做些其他的处理也可以直接卸载 Lambda 里头,同步策略的具体实现不再与 register()
方法有关,它变回了纯粹的用户同步/注册的方法。
但是我们不难发现,现在为策略接口添加实现的地方也变成了枚举类中,策略接口 IUserSynchronizeService
一般也不会被用在其他地方,因此不妨把策略接口也一并引入枚举类中,让他成为一个枚举类的内部接口。
现在,枚举类是这样的:
枚举类堆外只暴露根据类型获取方法的IUserSynchronizeService()
方法,以及 A 和 B 两个枚举。
完整的 UserSynchronizeTyeEnum
枚举类代码如下:
1 | /** |
三、使用
现在,改造完毕,可以开始使用了,对于原先的 register()
方法,现在改为:
1 | public void register(String type, String userId, String projectId, String declareId){ |
当我们需要再添加一个 C 类用户的同步注册的时候,只需要前往枚举类添加:
1 | /** |
即可,register()
方法就不需要再做修改了。