两万字盘点9种被玩烂了的设计模式以及在开源项目中的运用(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 大家好,我是三友~~之前有小伙伴私信我说看源码的时候感觉源码很难,不知道该怎么看,其实这有部分原因是因为没有弄懂一些源码实现的套路,也就是设计模式,所以本文我就总结了9种在源码中非常常见的设计模式,并列举了很多源码的实现例子,希望对你看源码和日常工作中有所帮助。

大家好,我是三友~~

之前有小伙伴私信我说看源码的时候感觉源码很难,不知道该怎么看,其实这有部分原因是因为没有弄懂一些源码实现的套路,也就是设计模式,所以本文我就总结了9种在源码中非常常见的设计模式,并列举了很多源码的实现例子,希望对你看源码和日常工作中有所帮助。

公众号:三友的java日记

模板方法模式

模板方法模式是指,在父类中定义一个操作中的框架,而操作步骤的具体实现交由子类做。其核心思想就是,对于功能实现的顺序步骤是一定的,但是具体每一步如何实现交由子类决定。

比如说,对于旅游来说,一般有以下几个步骤:

  • 做攻略,选择目的地
  • 收拾行李
  • 乘坐交通工具去目的地
  • 玩耍、拍照
  • 乘坐交通工具去返回

但是对于去哪,收拾什么东西都,乘坐什么交通工具,都是由具体某个旅行来决定。

那么对于旅游这个过程使用模板方法模式翻译成代码如下:

public abstract class Travel {
   
   

    public void travel() {
   
   
        //做攻略
        makePlan();

        //收拾行李
        packUp();

        //去目的地
        toDestination();

        //玩耍、拍照
        play();

        //乘坐交通工具去返回
        backHome();
    }

    protected abstract void makePlan();

    protected abstract void packUp();

    protected abstract void toDestination();

    protected abstract void play();

    protected abstract void backHome();

}

对于某次旅行来说,只需要重写每个步骤该做的事就行,比如说这次可以选择去杭州西湖,下次可以去长城,但是对于旅行过程来说是不变了,对于调用者来说,只需要调用暴露的travel方法就行。

可能这说的还是比较抽象,我再举两个模板方法模式在源码中实现的例子。

模板方法模式在源码中的使用

1、模板方法模式在HashMap中的使用

HashMap我们都很熟悉,可以通过put方法存元素,并且在元素添加成功之后,会调用一下afterNodeInsertion方法。

而afterNodeInsertion其实是在HashMap中是空实现,什么事都没干。

afterNodeInsertion

这其实就是模板方法模式。HashMap定义了一个流程,那就是当元素成功添加之后会调用afterNodeInsertion,子类如果需要在元素添加之后做什么事,那么重写afterNodeInsertion就行。

正巧,JDK中的LinkedHashMap重写了这个方法。

而这段代码主要干的一件事就是可能会移除最老的元素,至于到底会不会移除,得看if是否成立。

添加元素移除最老的元素,基于这种特性其实可以实现LRU算法,比如Mybatis的LruCache就是基于LinkedHashMap实现的,有兴趣的可以扒扒源码,这里就不再展开讲了。

2、模板方法模式在Spring中的运用

我们都知道,在Spring中,ApplicationContext在使用之前需要调用一下refresh方法,而refresh方法就定义了整个容器刷新的执行流程代码。

refresh方法部分截图

在整个刷新过程有一个onRefresh方法

onRefresh方法

而onRefresh方法默认是没有做任何事,并且在注释上有清楚两个单词Template method,翻译过来就是模板方法的意思,所以onRefresh就是一个模板方法,并且方法内部的注释也表明了,这个方法是为了子类提供的。

在Web环境下,子类会重写这个方法,然后创建一个Web服务器。

3、模板方法模式在Mybatis中的使用

在Mybatis中,是使用Executor执行Sql的。

Executor

而Mybatis一级缓存就在Executor的抽象实现中BaseExecutor实现的。如图所示,红圈就是一级缓存

BaseExecutor

比如在查询的时候,如果一级缓存有,那么就处理缓存的数据,没有的话就调用queryFromDatabase从数据库查

queryFromDatabase会调用doQuery方法从数据库查数据,然后放入一级缓存中。

而doQuery是个抽象方法

所以doQuery其实就是一个模板方法,需要子类真正实现从数据库中查询数据,所以这里就使用了模板方法模式。

责任链模式

在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,由该链上的某一个对象或者某几个对象决定处理此请求,每个对象在整个处理过程中值扮演一个小小的角色。

举个例子,现在有个请假的审批流程,根据请假的人的级别审批到的领导不同,比如有有组长、主管、HR、分管经理等等。

先需要定义一个处理抽象类,抽象类有个下一个处理对象的引用,提供了抽象处理方法,还有一个对下一个处理对象的调用方法。

public abstract class ApprovalHandler {
   
   

    /**
     * 责任链中的下一个处理对象
     */
    protected ApprovalHandler next;

    /**
     * 设置下一个处理对象
     *
     * @param approvalHandler
     */
    public void nextHandler(ApprovalHandler approvalHandler) {
   
   
        this.next = approvalHandler;
    }

    /**
     * 处理
     *
     * @param approvalContext
     */
    public abstract void approval(ApprovalContext approvalContext);

    /**
     * 调用下一个处理对象
     *
     * @param approvalContext
     */
    protected void invokeNext(ApprovalContext approvalContext) {
   
   
        if (next != null) {
   
   
            next.approval(approvalContext);
        }
    }

}

几种审批人的实现

//组长审批实现
public class GroupLeaderApprovalHandler extends ApprovalHandler {
   
   
    @Override
    public void approval(ApprovalContext approvalContext) {
   
   
        System.out.println("组长审批");
        //调用下一个处理对象进行处理
        invokeNext(approvalContext);
    }
}

//主管审批实现
public class DirectorApprovalHandler extends ApprovalHandler {
   
   
    @Override
    public void approval(ApprovalContext approvalContext) {
   
   
        System.out.println("主管审批");
        //调用下一个处理对象进行处理
        invokeNext(approvalContext);
    }
}

//hr审批实现
public class HrApprovalHandler extends ApprovalHandler {
   
   
    @Override
    public void approval(ApprovalContext approvalContext) {
   
   
        System.out.println("hr审批");
        //调用下一个处理对象进行处理
        invokeNext(approvalContext);
    }
}

有了这几个实现之后,接下来就需要对对象进行组装,组成一个链条,比如在Spring中就可以这么玩。

@Component
public class ApprovalHandlerChain {
   
   

    @Autowired
    private GroupLeaderApprovalHandler groupLeaderApprovalHandler;
    @Autowired
    private DirectorApprovalHandler directorApprovalHandler;
    @Autowired
    private HrApprovalHandler hrApprovalHandler;

    public ApprovalHandler getChain() {
   
   
        //组长处理完下一个处理对象是主管
        groupLeaderApprovalHandler.nextHandler(directorApprovalHandler);
        //主管处理完下一个处理对象是hr
        directorApprovalHandler.nextHandler(hrApprovalHandler);

        //返回组长,这样就从组长开始审批,一条链就完成了
        return groupLeaderApprovalHandler;
    }

}

之后对于调用方而言,只需要获取到链条,开始处理就行。

一旦后面出现需要增加或者减少审批人,只需要调整链条中的节点就行,对于调用者来说是无感知的。

责任链模式在开源项目中的使用

1、在SpringMVC中的使用

在SpringMVC中,可以通过使用HandlerInterceptor对每个请求进行拦截。

HandlerInterceptor

而HandlerInterceptor其实就使用到了责任链模式,但是这种责任链模式的写法跟上面举的例子写法不太一样。

对于HandlerInterceptor的调用是在HandlerExecutionChain中完成的。

HandlerExecutionChain

比如说,对于请求处理前的拦截,就在是这样调用的。

其实就是循环遍历每个HandlerInterceptor,调用preHandle方法。

2、在Sentinel中的使用

Sentinel是阿里开源的一个流量治理组件,而Sentinel核心逻辑的执行其实就是一条责任链。

在Sentinel中,有个核心抽象类AbstractLinkedProcessorSlot

AbstractLinkedProcessorSlot

这个组件内部也维护了下一个节点对象,这个类扮演的角色跟例子中的ApprovalHandler类是一样的,写法也比较相似。这个组件有很多实现

比如有比较核心的几个实现

  • DegradeSlot:熔断降级的实现
  • FlowSlot:流量控制的实现
  • StatisticSlot:统计的实现,比如统计请求成功的次数、异常次数,为限流提供数据来源
  • SystemSlot:根据系统规则来进行流量控制

整个链条的组装的实现是由DefaultSlotChainBuilder实现的

DefaultSlotChainBuilder

并且内部是使用了SPI机制来加载每个处理节点

所以,如果你想自定一些处理逻辑,就可以基于SPI机制来扩展。

除了上面的例子,比如Gateway网关、Dubbo、MyBatis等等框架中都有责任链模式的身影,所以责任链模式使用的还是比较多的。

代理模式

代理模式也是开源项目中很常见的使用的一种设计模式,这种模式可以在不改变原有代码的情况下增加功能。

举个例子,比如现在有个PersonService接口和它的实现类PersonServiceImpl

//接口
public interface PersonService {
   
   

    void savePerson(PersonDTO person);

}

//实现
public class PersonServiceImpl implements PersonService{
   
   
    @Override
    public void savePerson(PersonDTO person) {
   
   
        //保存人员信息
    }
}

这个类刚开始运行的好好的,但是突然之前不知道咋回事了,有报错,需要追寻入参,所以此时就可以这么写。

public class PersonServiceImpl implements PersonService {
   
   
    @Override
    public void savePerson(PersonDTO person) {
   
   
        log.info("savePerson接口入参:{}", JSON.toJSONString(person));
        //保存人员信息
    }
}

这么写,就修改了代码,万一以后不需要打印日志了呢,岂不是又要修改代码,不符和之前说的开闭原则,那么怎么写呢?可以这么玩。

public class PersonServiceProxy implements PersonService {
   
   

    private final PersonService personService = new PersonServiceImpl();

    @Override
    public void savePerson(PersonDTO person) {
   
   
        log.info("savePerson接口入参:{}", JSON.toJSONString(person));
        personService.savePerson(person);
    }
}

可以实现一个代理类PersonServiceProxy,对PersonServiceImpl进行代理,这个代理类干的事就是打印日志,最后调用PersonServiceImpl进行人员信息的保存,这就是代理模式。

当需要打印日志就使用PersonServiceProxy,不需要打印日志就使用PersonServiceImpl,这样就行了,不需要改原有代码的实现。

讲到了代理模式,就不得不提一下Spring AOP,Spring AOP其实跟静态代理很像,最终其实也是调用目标对象的方法,只不过是动态生成的,这里就不展开讲解了。

代理模式在Mybtais中的使用

前面在说模板方法模式的时候,举了一个BaseExecutor使用到了模板方法模式的例子,并且在BaseExecutor这里面还完成了一级缓存的操作。

其实不光是一级缓存是通过Executor实现的,二级缓存其实也是,只不过不在BaseExecutor里面实现,而是在CachingExecutor中实现的。

CachingExecutor

CachingExecutor中内部有一个Executor类型的属性delegate,delegate单词的意思就是代理的意思,所以CachingExecutor显然就是一个代理类,这里就使用到了代理模式。

CachingExecutor的实现原理其实很简单,先从二级缓存查,查不到就通过被代理的对象查找数据,而被代理的Executor在Mybatis中默认使用的是SimpleExecutor实现,SimpleExecutor继承自BaseExecutor。

这里思考一下二级缓存为什么不像一级缓存一样直接写到BaseExecutor中?

这里我猜测一下是为了减少耦合。

我们知道Mybatis的一级缓存默认是开启的,一级缓存写在BaseExecutor中的话,那么只要是继承了BaseExecutor,就拥有了一级缓存的能力。

但二级缓存默认是不开启的,如果写在BaseExecutor中,讲道理也是可以的,但不符和单一职责的原则,类的功能过多,同时会耦合很多判断代码,比如开启二级缓存走什么逻辑,不开启二级缓存走什么逻辑。而使用代理模式很好的解决了这一问题,只需要在创建的Executor的时候判断是否开启二级缓存,开启的话就用CachingExecutor代理一下,不开启的话老老实实返回未被代理的对象就行,默认是SimpleExecutor。

如图所示,是构建Executor对象的源码,一旦开启了二级缓存,就会将前面创建的Executor进行代理,构建一个CachingExecutor返回。

适配器模式

适配器模式使得原本由于接口不兼容而不能一起工作的哪些类可以一起工作,将一个类的接口转换成客户希望的另一个接口。

举个生活中的例子,比如手机充电器接口类型有USB TypeC接口和Micro USB接口等。现在需要给一个Micro USB接口的手机充电,但是现在只有USB TypeC接口的充电器,这怎么办呢?

其实一般可以弄个一个USB TypeC转Micro USB接口的转接头,这样就可以给Micro USB接口手机充电了,代码如下

USBTypeC接口充电

public class USBTypeC {
   
   

    public void chargeTypeC() {
   
   
        System.out.println("开启充电了");
    }

}

MicroUSB接口

public interface MicroUSB {
   
   

    void charge();

}

适配实现,最后是调用USBTypeC接口来充电

public class MicroUSBAdapter implements MicroUSB {
   
   

    private final USBTypeC usbTypeC = new USBTypeC();

    @Override
    public void charge() {
   
   
        //使用usb来充电
        usbTypeC.chargeTypeC();
    }

}

方然除了上面这种写法,还有一种继承的写法。

public class MicroUSBAdapter extends USBTypeC implements MicroUSB {
   
   

    @Override
    public void charge() {
   
   
        //使用usb来充电
        this.chargeTypeC();
    }

}

这两种写法主要是继承和组合(聚合)的区别。

这样就可以通过适配器(转接头)就可以实现USBTypeC给MicroUSB接口充电。

适配器模式在日志中的使用

在日常开发中,日志是必不可少的,可以帮助我们快速快速定位问题,但是日志框架比较多,比如Slf4j、Log4j等等,一般同一系统都使用一种日志框架。

但是像Mybatis这种框架来说,它本身在运行的过程中也需要产生日志,但是Mybatis框架在设计的时候,无法知道项目中具体使用的是什么日志框架,所以只能适配各种日志框架,项目中使用什么框架,Mybatis就使用什么框架。

为此Mybatis提供一个Log接口

而不同的日志框架,只需要适配这个接口就可以了

Slf4jLoggerImpl

就拿Slf4j的实现来看,内部依赖了一个Slf4j框架中的Logger对象,最后所有日志的打印都是通过Slf4j框架中的Logger对象来实现的。

此外,Mybatis还提供了如下的一些实现

这样,Mybatis在需要打印日志的时候,只需要从Mybatis自己的LogFactory中获取到Log对象就行,至于最终获取到的是什么Log实现,由最终项目中使用日志框架来决定。

观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。

这是什么意思呢,举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于观察者模式来实现,打119、救人的操作只需要观察火灾的发生,一旦发生,就触发相应的逻辑。

观察者的核心优点就是观察者和被观察者是解耦合的。就拿上面的例子来说,火灾事件(被观察者)根本不关系有几个监听器(观察者),当以后需要有变动,只需要扩展监听器就行,对于事件的发布者和其它监听器是无需做任何改变的。

观察者模式实现起来比较复杂,这里我举一下Spring事件的例子来说明一下。

观察者模式在Spring事件中的运用

Spring事件,就是Spring基于观察者模式实现的一套API,如果有不知道不知道Spring事件的小伙伴,可以看看《三万字盘点Spring/Boot的那些常用扩展点》这篇文章,里面有对Spring事件的详细介绍,这里就不对使用进行介绍了。

Spring事件的实现比较简单,其实就是当Bean在生成完成之后,会将所有的ApplicationListener接口实现(监听器)添加到ApplicationEventMulticaster中。

ApplicationEventMulticaster可以理解为一个调度中心的作用,可以将事件通知给监听器,触发监听器的执行。

ApplicationEventMulticaster可以理解为一个总线

retrieverCache中存储了事件类型和对应监听器的缓存。当发布事件的时候,会通过事件的类型找到对应的监听器,然后循环调用监听器。

所以,Spring的观察者模式实现的其实也不复杂。

总结

本文通过对设计模式的讲解加源码举例的方式介绍了9种在代码设计中常用的设计模式:

  • 单例模式
  • 建造者模式
  • 工厂模式
  • 策略模式
  • 模板方法模式
  • 责任链模式
  • 代理模式
  • 适配器模式
  • 观察者模式

其实这些设计模式不仅在源码中常见在平时工作中也是可以经常使用到的。

设计模式其实还是一种思想,或者是套路性的东西,至于设计模式具体怎么用、如何用、代码如何写还得依靠具体的场景来进行灵活的判断。

最后,本文又是前前后后花了一周多的时间完成,如果对你有点帮助,还请帮忙点赞、在看、转发、非常感谢。

往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

两万字盘点那些被玩烂了的设计模式

搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
设计模式
110.【十万字带你深入学习23种设计模式】(二十二)
110.【十万字带你深入学习23种设计模式】
52 1
110.【十万字带你深入学习23种设计模式】(二十二)
|
设计模式 算法 Java
110.【十万字带你深入学习23种设计模式】(十九)
110.【十万字带你深入学习23种设计模式】
63 1
|
设计模式 Java
110.【十万字带你深入学习23种设计模式】(二十六)
110.【十万字带你深入学习23种设计模式】
76 1
|
设计模式 Java
110.【十万字带你深入学习23种设计模式】(二十一)
110.【十万字带你深入学习23种设计模式】
60 1
|
设计模式
110.【十万字带你深入学习23种设计模式】(十)
110.【十万字带你深入学习23种设计模式】
50 0
|
设计模式 安全 Java
110.【十万字带你深入学习23种设计模式】(五)
110.【十万字带你深入学习23种设计模式】
60 0
|
设计模式 数据可视化 程序员
110.【十万字带你深入学习23种设计模式】(一)
110.【十万字带你深入学习23种设计模式】
99 0
|
设计模式 Java
110.【十万字带你深入学习23种设计模式】(九)
110.【十万字带你深入学习23种设计模式】
51 0
|
设计模式 安全 Java
110.【十万字带你深入学习23种设计模式】(六)
110.【十万字带你深入学习23种设计模式】
49 0
|
11月前
|
设计模式 缓存 Dubbo
两万字盘点9种被玩烂了的设计模式以及在开源项目中的运用(上)
大家好,我是三友~~ 之前有小伙伴私信我说看源码的时候感觉源码很难,不知道该怎么看,其实这有部分原因是因为没有弄懂一些源码实现的套路,也就是设计模式,所以本文我就总结了9种在源码中非常常见的设计模式,并列举了很多源码的实现例子,希望对你看源码和日常工作中有所帮助。
两万字盘点9种被玩烂了的设计模式以及在开源项目中的运用(上)