前端设计模式之责任链模式

前言

设计模式系列

前端设计模式之工厂模式

前端设计模式之代理模式

前端设计模式之策略模式

前端设计模式之装饰模式

责任链模式

什么是责任链模式

责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。(此处引自 gof 设计模式)

在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,所以责任链将请求的发送者和请求的处理者解耦了。

责任链模式是一种对象行为型模式,其主要优点如下:

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

其主要缺点如下。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

其他说明

责任链模式,总的一个核心就是请求者不必知道是谁哪个节点对象处理的请求,由于处理请求的可以在不同对象下处理,所以请求者跟接受者是解耦的。

纯的责任链:要求请求在这些对象链中必须被处理,而且一个节点处理对象,要么只处理请求,要么把请求转发给下个节点对象处理;

不纯的责任链:要求在责任链里不一定会有处理结构,而且一个节点对象,即可以处理部分请求,并把请求再转发下个节点处理;

javascript 中介者模式

责任链模式对前端开发来说可能有点陌生,但是看了前面的描述又感觉似曾相识

实际上 express、redux 里的 middleware 都可以简单理解为责任链模式的运用

要实现中间件模式,最重要的实现细节是:

  1. 可以通过调用 use() 函数来注册新的中间件
  2. 当接收到需要处理的新数据时,注册的中间件在执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值
  3. 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的回调函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件

项目实战

通用中间件开发

class Middleware {
  constructor() {
    this.$cache = []
    this.$middlewares = []
  }

  // 注册中间件
  use() {
    [...arguments].forEach(item => {
      if (typeof item === 'function') {
        this.$cache.push(item)
      }
    })
    return this
  }

  /**
   * 每个中间件只有两个形参 第一是传进来的参数 第二个是调用下一个中间件的函数
   * 中间件的执行顺序是根据你注册中间件的顺序来去调用的 
   */
  next(params) {
    while (this.$middlewares.length) {
      const ware = this.$middlewares.shift()
      ware.call(this, params, this.next.bind(this))
    }
  }
  
  execute(params) {
    this.$middlewares = this.$cache.map(fn => {  // 复制一份
      return fn;
    });
    this.next(params)
  }

}

export default Middleware

通用中间件使用 ajax

const middleware = new Middleware()

function transform(options, next) {
  console.log('before', options.data);
  options.data.age = Number(options.data.age)
  next(options); // 通过验证
}

function validate(options, next) {
  console.log('validate', options.data);
  next(options); // 通过验证
}

function send(options, next) {
  setTimeout(function () { // 模拟异步
    console.log('send', options.data);
    next();
  }, 100);
}

middleware.use(transform).use(validate).use(send)
middleware.execute({ data: { name: 'cookie', age: '20' } });

如上我们在发送请求之前加入了类型转换、数据校验,将数据的业务处理使用中间件模式剥离,使得处理过程模块化,维护性提升。

中间件升级-事件回调

/**
* 注册事件
* @param {String} name 事件名称 
* @param {Function (params)} callback 回调函数 
*/
on(name, callback) {
if (typeof callback === 'function') {
  this.$events[name] = callback
} else {
  throw '事件回调必须为函数'
}
}

/**
* 发射(触发)事件
* @param {String} name 事件名称 
* @param {Any} params 回调参数 
*/
emit(name, params) {
if (this.$events[name]) {
  let callback = this.$events[name]
  callback.call(this, params)
} else {
  throw '没有注册这个事件'
}
}

每个中间件的过程都是不可控制的,全部都交由中间类去统一调用,我们可以加入事件回调,方便我们在中间件处理过程中拥有额外的逻辑能力

将上述的使用方法再改造一下,方便实际业务中使用

function send(options, next) {
  this.emit('request', options)
  setTimeout(() => { // 模拟异步
    console.log('send', options.data);
    this.emit('response', options)
    options.promise.resolve({ data: options.data })
  }, 100);
}

// 请求之前的回调函数
middleware.on('request', params => {
  // 在这里可以做请求之前的一些处理,比如添加全局参数等
  console.log(params, '再多做一些处理')
})

// 请求成功的回调函数
middleware.on('response', params => {
  // 在这里可以做下请求成功的一些处理,比如全局loading什么的
  console.log(params, '请求成功')
})

middleware.use(transform).use(validate).use(send)

middleware.executeFc({ data: { name: 'cookie', age: '20' } }).then(({ data }) => {
  console.log('finally', data)
});

上述的项目实例是采用 ajax 来演示,实际通用的中间件类,可以在业务中将一些流程化执行的任务拆分出来,例如表单验证、多重条件判断等等

多种条件判断

const middleware = new Middleware()

function judge1(options, next) { // 空数校验
  if (!options.data) {
    options.promise.reject({ data: false, msg: '数据为空' })
    return
  }
  next(options); // 通过验证
}

function judge2(options, next) { // 判断小于10
  if (options.data < 10) {
    options.promise.reject({ data: false, msg: '数据小于10' })
    return
  }
  next(options); // 通过验证
}

function judge3(options, next) { // 判断大于30
  if (options.data < 30) {
    options.promise.reject({ data: false, msg: '数据小于30' })
    return
  }
  options.promise.resolve({ data: true, msg: '数据小于30' })
}

middleware.use(judge1).use(judge2).use(judge3)

middleware.executeFc({ data: 40 }).then(({ data }) => {
  console.log('finally', data)
}).catch(({ msg }) => {
  console.log(msg)
})

将流程化执行的多种条件判断通过中间件解耦,可以使得条件判断方法更加清晰。一般当你需要使用中介者来改造业务逻辑的时候,前端的项目确实有点复杂了。

尾声

完整的 demo 地址:项目实战 demo,喜欢的朋友可以 star 一下,后续会根据设计模式博文的推出,逐步的将此项目继续拓展出来。

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

推荐阅读更多精彩内容