设计模式实践—模板模式在财务账单导入的实践

背景:设计模式是码神/码圣等为软件开发过程中相同表征的问题,抽象出可重复利用的解决方案。在某种程度上,使用设计模式代表了在某些特定需求情况下的最佳实践。同时掌握设计模式,也是一个开发者进阶的必修课程,可以起到和同行之间沟通的“行话”(zhuang bi) 的作用;
但是单纯的学习设计模式,总是有一种高高再上,触不可及的感觉。如果生搬硬套去使用,反而会限制了我们后续需求的开发,最终成了四不像。小编将结合日常开发中遇到的需求和配套的设计模式来浅谈自己对设计模式的理解。

1、啥是模板模式——从思考制作奶茶开始

模板模式(Template Pattern),也叫模板方法模式(Template Method Pattern),它是行为型设计模式中最常用也是最易于理解的。 初听这个名字,就感觉已经懂了80%。

"这不就是和制作奶茶似的,制作步骤,茶底等大部分配料都确认好了,如果你想喝“多肉芒果奶茶”,那就在制作的时候添加一点芒果汁,如果你想喝“多肉葡萄奶茶”,那就换成添加葡萄汁 "

奶茶制作简图

没错,简单的来说模板模式就是定义一个操作中的算法骨架(制作奶茶的步骤),而将一些步骤延迟到具体子类(具体的奶茶口味)中去实现,使得子类可以不改变整个算法的结构,就可以重新定义该算法的某些特定步骤。

这样做的好处是既统一算法,也提供了很大的灵活性

2、业界大佬是如何实践的

在对模板模式有了初步认识后,我们来看一下业界主流开源框架是如何在API设计中使用的。模板模式几乎在每个优秀的开源框架中都能看到它的身影,如:

  • Java SE 中的InputStream,OutputStreamjava.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容器中所需要的方法,但是它只是一个接口,并没有真正的实现。根据类关系图可以看出:

类关系图.png

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注册中心拥有良好的拓展性,支持基于ZookeeperNacosRedis等非常多的实现,同时用户也可以在框架基础上,快速开发出符合自己业务需求的注册中心。而它的这种拓展性和使用模板设计模式也是密不可分。

类的关系图

从上图我们看出,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小学生,欢迎大家吐槽留言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350