背景:设计模式是码神/码圣等为软件开发过程中相同表征的问题,抽象出可重复利用的解决方案。在某种程度上,使用设计模式代表了在某些特定需求情况下的最佳实践。同时掌握设计模式,也是一个开发者进阶的必修课程,可以起到和同行之间沟通的“行话”(
zhuang bi) 的作用;
但是单纯的学习设计模式,总是有一种高高再上,触不可及的感觉。如果生搬硬套去使用,反而会限制了我们后续需求的开发,最终成了四不像。小编将结合日常开发中遇到的需求和配套的设计模式来浅谈自己对设计模式的理解。
1、啥是模板模式——从思考制作奶茶开始
模板模式(Template Pattern),也叫模板方法模式(Template Method Pattern),它是行为型设计模式中最常用也是最易于理解的。 初听这个名字,就感觉已经懂了80%。
奶茶制作简图"这不就是和制作奶茶似的,制作步骤,茶底等大部分配料都确认好了,如果你想喝“多肉芒果奶茶”,那就在制作的时候添加一点芒果汁,如果你想喝“多肉葡萄奶茶”,那就换成添加葡萄汁 "
没错,简单的来说模板模式就是定义一个操作中的算法骨架(制作奶茶的步骤),而将一些步骤延迟到具体子类(具体的奶茶口味)中去实现,使得子类可以不改变整个算法的结构,就可以重新定义该算法的某些特定步骤。
这样做的好处是既统一算法,也提供了很大的灵活性。
2、业界大佬是如何实践的
在对模板模式有了初步认识后,我们来看一下业界主流开源框架是如何在API设计中使用的。模板模式几乎在每个优秀的开源框架中都能看到它的身影,如:
- Java SE 中的
InputStream
,OutputStream
等java.io
包中的都很多抽象类都使用了模板模式进行设计。 - Spring框架中的
Ioc容器
初始化过程中,JdbcTemplate
组件等也都应用了模板模式。 - Dubbo 注册中心的逻辑部分也使用了模板模式。
由于篇幅原因,我们就挑选了两个业界大佬的源码来观摩学习一下。
以下源码案例解读,读者可根据自身情况选择查看,我们这里只是学习其如何设计,无须深究具体代码含义。
2.1 Spring Ioc容器初始化中的模板模式
设计
首先,我们就从Spring Ioc容器初始化来分析Spring是如何使用模板模式的来设计的。
spring framework的版本是5.2.1
了解SpringBoot的同学都知道,我们用SpringApplication#run(class, args)
就可以创建Spring Ioc容器,其中主要有3种类别:
- Servlet Web 环境:
AnnotationConfigServletWebServerApplicationContext
- Reactive Web 环境:
AnnotationConfigReactiveWebServerApplicationContext
- (默认)非Web 环境:
AnnotationConfigApplicationContext
单名称上可以看出这几个XXXApplicationContext
是为了不同的应用场合设计的,而且后缀都是ApplicationContext
,说明它们肯定就如同我们的“多肉芒果奶茶”,“多肉葡萄奶茶”一样,都是用“奶茶”结尾,它们的制作步骤也肯定有相同,也必然有不同的地方。
没错,在Spring Ioc容器中,ApplicationContext
是一个非常重要的核心接口,它定义了Ioc容器中所需要的方法,但是它只是一个接口,并没有真正的实现。根据类关系图可以看出:
在ApplicationContent
的基础上,又新增了一个ConfigurableApplicationContext
接口类,在这个接口类中,它定义了如何配置和管理Ioc容器的生命周期方法。
接下来AbstractApplicationContext
是第一个真正的实现类,因为这里面方法众多,我们就以其中refresh()
为例,在该方法中它编排了一系列的逻辑
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory)
省略...
}
catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
其中在obtainFreshBeanFactory()
方法中,它调用了两个抽象方法
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}
而这两个抽象方法refreshBeanFactory()
,getBeanFactory()
的具体实现,则交由具体子类去实现具体逻辑。
在GenericApplicationContext#refreshBeanFactory()
的实现逻辑是:什么都不做
@Override
protected final void refreshBeanFactory() throws IllegalStateException {
if (!this.refreshed.compareAndSet(false, true)) {
throw new IllegalStateException(
"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
}
this.beanFactory.setSerializationId(getId());
}
而在AbstractRefreshableApplicationContext#refreshBeanFactory()
方法中,则实现了context的真实刷新逻辑。
是不是和我们制作奶茶的逻辑非常相似,整体步骤都已经大致被编排好,我们只需要通过具体子类去实现部分代码,就可以获得“不同口味”的Ioc容器。
2.2 Dubbo 注册中心的模板模式
设计
用过Dubbo的同学都知道,Dubbo注册中心拥有良好的拓展性,支持基于Zookeeper
,Nacos
,Redis
等非常多的实现,同时用户也可以在框架基础上,快速开发出符合自己业务需求的注册中心。而它的这种拓展性和使用模板设计模式也是密不可分。
从上图我们看出,AbstractRegistry
实现了Registry接口中的register()
服务注册,subscribe()
订阅,lookup()
查询,notify()
通知等方法,还实现了doSaveProperties
磁盘文件持久化注册信息这一通用方法。但是注册,订阅,通知等方法只是简单把URL加入对应的集合,没有具体的逻辑。
我们已服务subscribe()
订阅源码为例子:
@Override
public void subscribe(URL url, NotifyListener listener) {
if (url == null) {
throw new IllegalArgumentException("subscribe url == null");
}
if (listener == null) {
throw new IllegalArgumentException("subscribe listener == null");
}
if (logger.isInfoEnabled()) {
logger.info("Subscribe: " + url);
}
// subscribed 是一个Map结构,保存已订阅的服务
// ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<>();
Set<NotifyListener> listeners = subscribed.computeIfAbsent(url, n -> new ConcurrentHashSet<>());
listeners.add(listener);
}
FailbackRegistry
又继承了AbstractRegistry
,重写了父类的注册,订阅,和通知等方法,并且添加了重试机制。并且增加了四个模板方法:
// ==== Template method ====
public abstract void doRegister(URL url);
public abstract void doUnregister(URL url);
public abstract void doSubscribe(URL url, NotifyListener listener);
public abstract void doUnsubscribe(URL url, NotifyListener listener);
我们继续观察其subscribe()
订阅方法:
@Override
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener);
try {
// Sending a subscription request to the server side
// 此处调用模板方法,真实逻辑交由子类去实现
doSubscribe(url, listener);
} catch (Exception e) {
//异常处理逻辑
...
}
}
我们发现它重写了AbstractRegistry#subscribe()
,实现了订阅的大体逻辑及异常处理等通用性的东西。但是真正具体如何订阅的逻辑则是调用doSubscribe()
交由具体子类去实现,如ZookeeperRegistry#doSubscribe()
才真正调用zkClient
去创建znode或者watch某些节点。这就是模板模式的具体实现。
3、自己动手实践
我们的标题是《模板模式在财务账单导入的实践》的实践,那我们肯定免不了我们要自己结合需求设计一套处理流程。
业务需求如下:
用户需要将支付宝,微信,Paypal,POS机等大约5,6种不同种类型的交易账单上传至系统,并根据用户所预设配置规则(主要配置如币种,汇率,交易主体等信息)进行计算,汇总。其中账单文件格式包含且不限于excel,csv, pdf并保留原始上传文件
在看到这个需求后,我们大致分析出以下几点需求特性
- 交易账单类型多样,需具有可拓展性,如后续有可能还会增加如Amex,WroldPay等其他平台
- 文件类型多样,涉及csv,excel,pdf等,且具体解析方法需按具体平台规定的格式解析
- 具有统一的操作流程,上传 → 保存原始文件 → 加载预设配置 → 分析汇总 → 结果入库
相信大家也已经感觉到我们的需求和Spring Ioc或Dubbo 注册中心在需求设计上有相似的地方,那我们是不是也可以用模板模式来解决这个问题呢?毕竟模板模式的优点就是:
1. 统一算法骨架
2. 提取公共代码,便于维护
3. 具体行为交由子类控制,具有良好的灵活性
整体设计类图如下:
类关系图
我们首先定义了一个BillSummaryBizFlow
的接口类,在这里定义了解析账单所需的相关接口,如getTemplateCode()
获取模板code,isMatch()
文件类型是否匹配,getMetaData()
业务元数据等。其中核心处理方法为 parse()
解析方法。
紧接着我们用AbstractBillSummaryBizFlow
实现了BillSummaryBizFlow
,我们着重观察 parse()
解析方法:
@Override
public void parse(Properties properties, File file) {
try {
if (isMatch(properties, file)) {
saveOriginFile(file);
List<?> datas = doParse(properties, file);
doPrecess(properties, datas);
}
} catch (Exception e) {
省略...
}
}
这里面我们新增1个公共方法及两个模板方法
- public FileInfo saveOriginFile(File file) 公共方法,将文件保持至磁盘
- protected abstract List<?> doParse(Properties properties, File file); 解析文件模板方法,交由具体子类实现
- protected abstract void doPrecess(Properties properties, List<?> originDatas); 处理数据模板方法,交由具体子类实现。
这样我们就定义好了整个账单解析的骨架,如果需要开发支付宝账单的解析逻辑,只需要继承AbstractBillSummaryBizFlow
,并实现doParse()
和doPrecess()
方法即可。
而无需改变整个账单处理流程的逻辑。
在具体使用时,我们可以结合工厂设计模式,直接通过前端传递过来的账单code拿到具体的BillBizFlow子类进行解析处理即可,简单代码如下:
@PostMapping
public Result<Void> handle(@RequestParam String platformCode,
@RequestParam String templateCode,
@RequestParam String accountNo,
@RequestParam MultipartFile file) {
if (StringUtils.isBlank(platformCode)) {
throw new RuntimeException("请选择第三方平台!");
}
BillHandleDTO dto = new BillHandleDTO();
dto.setPlatformCode(platformCode)
.setTemplateCode(templateCode)
.setAccountNo(accountNo)
.setFile(file);
//平台权限校验
Properties properties = mapping2Properties(dto);
BillSummaryBizFlow bizFlow = billBizFlowFactory.getBizFlow(templateCode);
bizFlow.parse(properties, file);
return Result.success();
}
4、总结
在本文中通过一个制作奶茶的案例简单学习了一下什么是模板模式,并通过Spring Ioc及Dubbor 注册中心的源码学习了一下大神们如和使用模板模式进行API设计的,最后根据我们实际遇到的业务需求+模板模式也简单设计了一套处理流程。
当然,模板模式也不是万能的,它也有缺点,就是每一个不同的实现都需要一个子类去实现,导致类的个数增加,使得整个系统更加庞大。所以它更适用的场景是当要完成某个过程中,该过程包含了一系列的步骤,并且有很多步骤都相同,但其个别的步骤行为可能存在差异,这时候不妨您考虑一下使用模板模式来设计处理您的业务。
如果您觉得这篇文章有用,请留下您的小💗💗,我是一枚Java小学生,欢迎大家吐槽留言。
网友评论