设计模式之责任链模式案例解析

此文来源于源码阅读网的鲁班大叔,此文以设计一个缓存模块为引题,然后阐述责任链模式。

一、设计一个缓存模块

接下来我们在分析缓存模块的需求如下:

  1. 存储在内存中,想要的时候立⻢可取(速度快)
  2. 存储在硬盘,断电不会丢失缓存数据
  3. 存储在第三方应用中(Redis\MongoDb),扩展性好和第三方软件进行集程 4. 淘汰策略:先进先出(FiFo)
  4. 淘汰策略:最近最少使用(LRU)
  5. 缓存过期:
  6. 记录缓存命中率
  7. 保证线程安全,对缓存的操作必须是同步的
  8. 缓存阻塞,防止缓存击穿
  9. 序列化,与反序列化
    在这里插入图片描述

解决方案一:

使用一个类专⻔类实现缓存需求,每个方法对应一个功能。为降低使用的复杂性,只对外开放 必须的方法。
Propertites configs;
getObject() 获取缓存
putObject() 设置缓存 clear() 清除缓存 store() 存储
logging()
记录缓存命中率
方案优点:内聚性强,所有相关缓存相关的功能全部集中在一起,阅读代码方便,不用在各个 类文件中跳来跳去。
方案缺点:偶合性高、一些必须和不必须的全部偶合到了一起,扩展功能时只能在这个类中继 续添加,久而久之这个类就会很臃肿,扩展性不强。
在这里插入图片描述

解决方案二:

将一些相关性高的功能抽像成接口:

  1. 存储机制
  2. 过期淘汰策略
  3. 命中率统计
  4. 阻塞 然后将各个部分进行组合成一个缓存器。基于配置来决定使用哪个部件。
    方案优点:如果要增加新的存储机制,只要在存储接口中新添加实现类即可,所以相对于第一 种方案灵活性更高。 方案缺点:由于新增了多个接口和类,整个实现的复杂度是成陪上升。这种复杂度的上升对开 发者不会有太大的影响。但对于源码的维 护者却是一个灾难,而且是核灾,在梳理流程或调试问题时,需要在各个类和接口之间跳来跳 去。另外它的扩展性也并没有想像中的好。因为所有的扩展必须在已知的接口之上进行扩展, 当新增的需求不符合任何一个接口定义时,就傻眼了。比如说我现在要添加一个功能实现缓存 阻塞,以防止缓存穿透。这时要么新增接口定义,要么在缓存上下文中添中。但无论怎么做都 会使复杂度进一步一上升。最后无法控制。
    工作不满1年的人会用方案一,因为没有什么设计能力,只能是要啥功能就直接加。而工作1 到5年的喜欢用方案二,有一定设计能力,但设计不到点子上,反而会经常犯过度设计的毛 病。5年以上的又会使用回方案一 因为这时他更关注快速实现需求。
    在这里插入图片描述

解决方案三:

怎样设计才是最优方案呢?
来看 下 大神Clinton Begin 他是怎么做的。Clinton Begin 它就是mybatis作者,他20年前写 的代码现在依然值得我们去学习。
他是这么做的。首先没有缓存上下文组件。把所有功能都抽像成一个接口,所有功能都去实现 这个接口。存储、淘汰机制、缓存过期、缓存阻塞。各个功能实现,完全分开,互相之间没有 任何依赖。最大程度保证了低耦合。那各个功能实现怎么进行关联组合呢?前两个方案是通过 一个缓存上下文将各功能部件进行了组合。Clinton Begin 的设计是让各个部件之间串联成一 个链条。具体做法是A部件引用B部件实例、B部件引用C、C以在引用D 类推下去。那这种引 用,不就不成导致高度耦合了吗?其实并没有,因各部件在引用下个部件时,它并没有直接去 依赖下个部件,而是通过接口间接进行的引用。所以它即可以引用A,又可以引用B,只要是 这个接口的实现即可,至于实现是什么不用去关心。 有了这个链条,只要在链条的头部发起调用,整个链条中的逻辑都会被执行。
现在一起来看看MyBat is具体实现代码。
缓存的各个部件实现:
在这里插入图片描述
在这里插入图片描述

缓存配置:

public @interface CacheNamespace {
Class<? extends org.apache.ibatis.cache.Cache> implementation() default
PerpetualCache.class;
Class<? extends org.apache.ibatis.cache.Cache> eviction() default
LruCache.class;
long flushInterval() default 0; int size() default 1024;
boolean readWrite() default true; boolean blocking() default false; /**
* Property values for a implementation object. * @since 3.4.2
*/
Property[] properties() default {}b }

在这里插入图片描述


整个链条的执行过程

可以看到Cache实例结构就是一个个缓存零部件组成的链条。无论是调用实例中的哪个方法都 会依次往下传递。 当我们修改配置之后,链条又会重新进行组合。最大限度保证灵活性。在扩展性方面,只需实 现Cache接口,然后把实现器织入链条即可。不需要在额外定义接口。
在这里插入图片描述
如上面讲的缓存例子 整个链条就包括:缓存序列化->记录缓存命中率->处理过期缓存->淘汰 策略->存储。另外这种模式还有一个优点就是它的,业务表述清晰,把链条一展现,我们就 知道他要看干嘛了。所以各大框架都在大量的使用这种模式。比如Dubbo在它的各个场景当中 就大量的使用责任链,如客户端发起远程调用过程包含:Mock处理、负载均衡、集群容错、 初始上下文、监控统计、异步转同步等逻辑就是通过责任链模式组合而成。还有他的服务响应 也是大量使用责任链模式,所以如果不提前看学习责任链设计模式,这Dubbo源码是没法看 的。

经典责任链:

听到这有些同学可能会存在疑问,这和我们在书本上学的责任链模式好像不一样呀。书本 上的责任链讲的都是。请求只会被唯一的节点处理,如果当前节点处理不了,就会交给下个请 求。如果能处理,就不在向下传递。其实这两种设计都属于责任链模式,前者是采用多个节点 分散承担责任,后者由特定的节点来承担责任。 后者比前者出现的早,所以也叫经典责任链 模式。而后者我们可称作变种责任链。
接下来我们看一个经典责任链模式在Spring MVC当中的一个例子。
在一个Spring MVC应用当中,会存在很多的请求处理方法,即处理器。当请求到达时应该由哪 个处理器进行处理处理呢?Spring的设计是,将多个处理器映射组个一个列表,然后每次都遍 历这个列表直到有某个映射器返回处理器为止。这就是一个经典的责任链模式。
在这里插入图片描述

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	if (this.handlerMappings != null) {
		for (HandlerMapping mapping : this.handlerMappings) {
		HandlerExecutionChain handler = mapping.getHandler(request); 
		if (handler != null) {
			return handler; 
			}
		} 
	}
	return null; 
}

总结:

责任链由多个节点(处理器)组成。在行为模式上有两种:一种是遍历链条上的节点,直到找到 对应节点,然后处理为止,这种叫 模式。另一种是由各个节点依次处理,共享负担 责任的一部分,我们称它他 。两种模式在结构上也会不一样,前者基于数组进行遍 历,后者通过引用组成链表。

作业练习:

  1. 我们平常所看到的 (Filter)过滤器、(Interceptor)拦截器、(pipeline)管道这些都属于责任 链模式吗?如果是请说出你在哪些框架源码中有⻅过这些场景?它们属于经典责任链,还 是变种责任链?
  2. 基于以下场景设计一个经典责任链模式。 请假审批流程:1天以内由HR直接审批,2至3天由部⻔主管审批、3天以上由经理进行审批。

以上来源: 鲁班大叔

在这里插入图片描述

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页