概述
- 本文首先会解释什么是责任链模式,以及其想要解决的问题。
- 然后举 1 个小故事,使之代码化,并逐步应用责任链模式迭代代码。
- 最后举 1 个日常前端业务开发中遇到的需求,从需求背景、需求分析、代码设计等流程来实现。
话不多说,Let’s go!
什么是责任链模式?
在经典的 Gof 所著的 Design Patterns: Elements of Reusable Object-Oriented Software (中文译本 《设计模式——可复用面向对象软件的基础》)是这样描述责任链模式的:
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
粗看一看,有点过于定义了。那下面我们从一则小故事开始逐步认识它。
三国之温酒斩华雄
「温酒斩华雄」的故事背景相信大多数都有所了解。下面我们以图简述下「十八路诸侯的大将 VS 华雄」的过程。
前面的 pk 都是华雄赢了,唯有最后败北于关羽。如果在某一轮中,华雄败了的话,后面的大将都不需要出马了。因为华雄只有一个。 我们将之代码化的话,我们可能写出如下 v1 的代码:
// 注:命名以拼音是为了 demo,项目中切勿以拼音或者拼音首字母简写命名
function killHuaXiong() {
let win = false;
// round 1: 大将甲 VS 华雄
win = APKHuaXiong();
if (!win) {
// round 2: 大将乙 VS 华雄
win = BPKHuaXiong();
if (!win) {
// round 3: 大将丙 VS 华雄
win = CPKHuaXiong();
if (!win) {
// round 4: 关羽 VS 华雄
win = DPKHuaXiong();
}
}
}
return win;
}
上述的代码浓浓的面向过程的味道,太多的 if - else 嵌套。可能有的读者看上第一眼就已经有些不适了。针对上面的代码,我们提出以下的问题:
- Q1: 如果关羽也输了,后续其他大将再上场 PK,我们就得修改
killHuaXiong这个函数,代码可扩展性太差 - Q2: 如果中间轮次的大将出场顺序换一换的话,
killHuaXiong这个函数还是得改动很大
我们尝试应用责任链模式重构下这段代码。根据责任链的定义,我们将「斩华雄」看做一个请求,然后像诸多其他大将和关羽看做多个对象。不难写出如下的 v2 版本的代码:
abstract class IHandler {
protected nextHandler: IHandler | null;
setNextHandler(handler: IHandler): void {
this.nextHandler = handler;
}
abstract handle(): boolean;
}
class AHandler extends IHandler {
override handle() {
let canHandle = false;
console.log('A 不敌');
if (!canHandle && this.nextHandler) {
return this.nextHandler.handle();
}
return canHandle;
}
}
class GuanYuHandler extends IHandler {
override handle() {
let canHandle = false;
console.log('关羽赢了');
// 关羽可以处理华雄
canHandle = true;
if (!canHandle && this.nextHandler) {
return this.nextHandler.handle();
}
return canHandle;
}
}
const aHandlerIns = new AHandler();
// 类似的,B、C、GuanYu
// 责任链绑定
aHandlerIns.setNextHandler(bHandlerIns);
bHandlerIns.setNextHandler(cHandlerIns);
cHandlerIns.setNextHandler(guanYuHandlerIns);
// 第一个对象开始执行
aHandlerIns.handle();
对着 v1 版本提出的 2 个问题来看 v2 的代码:
- Q1: 如果关羽也输了,后续其他大将再上场 PK
- 我们需要改动的是,在添加 XXXHandler 的类,然后将其实例化,并绑在关羽的后面
- Q2: 如果中间轮次的出场的大将换一换的话
- 我们要修改的只是绑定顺序
看上去,v2 的版本一切很美好,貌似完美契合了开闭原则(对扩展开放,对修改关闭)。但是我们仔细看代码的时候,会发现在 handle 方法中都存在了相似的逻辑。「如果自身对付不了的话,显式的让继任者去做」,我们可以将公共的代码提升到抽象父类中,v3 代码如下:
abstract class IHandler {
// 将公共的代码提升到抽象父类
handle(): boolean {
const canHandle = this.doHandle();
if (!canHandle && this.nextHandler) {
return this.nextHandler.handle();
}
return canHandle;
}
abstract doHandle(): boolean;
}
class AHandler extends IHandler {
override doHandle(): boolean {}
}
好了,迭代到 v3 版本,代码可扩展,可维护性进一步增强。如果进一步优化的话,可以通过一个管理类来设置各个具体 Handler 的执行顺序,从而避免各个 Handler 之间的耦合。读者有兴趣的话可以自行实现。
再看定义
通过上面的一个历史故事,我们再重新理解下责任链模式的定义:此模式更关注的是能够处理问题,至于是谁最终解决的,不是那么重要。有没有《让子弹飞》里的刘嘉玲的台词 「反正呢,我就想当县长夫人,谁是县长,我无所谓」那个味。
知识迁移、类比链表
其实我们可以观察到,责任链模式和数据结构链表在思想方面是高度相像的:
Handler可以类比链表的节点NodenextHandler可以类比于链表的next指针
计算机在很多领域所涉及的思想具有很高的同一性。接下来我们看看日常的业务开发当中,前端哪些场景可以使用这种模式,能让代码更模块化。
前端应用场景
在笔者负责业务开发中,地理位置信息(经纬度)是业务所必要的信息,用于后续的风控策略等。由于 HTML5 页面是投放到多个平台,比如:app,小程序等,获取地理位置信息的方式也是不尽相同:
- url 的参数,
/path?lat=110&lon=123(app 环境) - localStorage 之前存储的在有效期内的经纬度
- 调用 app 的能力来获取的经纬度 (app 环境)
- 微信 SDK 的
wx.getLocation方法获取的经纬度 (微信环境) - 借助百度的 SDK 获取的经纬度
- ...
我们从业务需求出发,只要其中有一种方式能获取地理位置信息,就算任务完成。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
abstract class ILocationHandler {
async handle(): Promise<Partial<TResult>> {}
// 有的方法只能在特定的环境中使用,比如 app 的 position action,只能在公司内部 app 里使用
abstract canIUse(): boolean;
abstract doHandle(): Promise<Partial<TResult>>;
}
class UrlParamsLocationHandler extends ILocationHandler {
override canIUse() {
return true;
}
override doHandle() {
return mockUrlParamsResolver(); // 具体获取经纬度的方法
}
}
// LocationChain 是一个链表
// 将业务所需的各个 ILocationHandler 串联起来
class LocationChain {
// ...
append(handler: ILocationHandler) {}
// 返回地址信息
async execute() {}
}
最后的话
平时的业务开发中发现,一些同学对于 OOP 的理解和应用是稍微有所欠缺的。在合适的业务、工程场景中,我们可以对业务需求抽象,适当地应用一些优秀的设计模式,写出更具有扩展性、可维护性的代码。