为什么要提高代码扩展性

我们写的代码都是为了一定的需求服务的,但是这些需求并不是一成不变的,当需求变更了,如果我们代码的扩展性很好,我们可能只需要简单的添加或者删除模块就行了,如果扩展性不好,可能所有代码都需要重写,那就是一场灾难了,所以提高代码的扩展性是势在必行的。怎样才算有好的扩展性呢?好的扩展性应该具备以下特征:

需求变更时,代码不需要重写。局部代码的修改不会引起大规模的改动。有时候我们去重构一小块代码,但是发现他跟其他代码都是杂糅在一起的,里面各种耦合,一件事情拆在几个地方做,要想改这一小块必须要改很多其他代码。那说明这些代码的耦合太高,扩展性不强。可以很方便的引入新功能和新模块。

怎么提高代码扩展性?

当然是从优秀的代码身上学习了,本文会深入AxiosNode.jsVue等优秀框架,从他们源码总结几种设计模式出来,然后再用这些设计模式尝试解决下工作中遇到的问题。本文主要会讲职责链模式观察者模式适配器模式装饰器模式。下面一起来看下吧:

职责链模式

职责链模式顾名思义就是一个链条,这个链条上串联了很多的职责,一个事件过来,可以被链条上的职责依次处理。他的好处是链条上的各个职责,只需要关心自己的事情就行了,不需要知道自己的上一步是什么,下一步是什么,跟上下的职责都不耦合,这样当上下职责变化了,自己也不受影响,往链条上添加或者减少职责也非常方便。

实例:Axios 拦截器

用过 Axios 的朋友应该知道,Axios 的拦截器有请求拦截器响应拦截器,执行的顺序是请求拦截器 -> 发起请求 -> 响应拦截器,这其实就是一个链条上串起了三个职责。下面我们来看看这个链条怎么实现:

// 先从用法入手,一般我们添加拦截器是这样写的 
// instance.interceptors.request.use(fulfilled, rejected)
// 根据这个用法我们先写一个`Axios`类。
function Axios() {
  // 实例上有个interceptors对象,里面有request和response两个属性
  // 这两个属性都是InterceptorManager的实例
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 然后是实现InterceptorManager类
function InterceptorManager() {
  // 实例上有一个数组,存储拦截器方法
  this.handlers = [];
}

// InterceptorManager有一个实例方法use
InterceptorManager.prototype.use = function(fulfilled, rejected) {
  // 这个方法很简单,把传入的回调放到handlers里面就行
  this.handlers.push({
    fulfilled,
    rejected
  })
}

上面的代码其实就完成了拦截器创建和use的逻辑,并不复杂,那这些拦截器方法都是什么时候执行呢?当然是我们调用instance.request的时候,调用instance.request的时候真正执行的就是请求拦截器 -> 发起请求 -> 响应拦截器链条,所以我们还需要来实现下Axios.prototype.request:

Axios.prototype.request = function(config) {
  // chain里面存的就是我们要执行的方法链条
  // dispatchRequest是发起网络请求的方法,本文主要讲设计模式,这个方法就不实现了
  // chain里面先把发起网络请求的方法放进去,他的位置应该在chain的中间
  const chain = [dispatchRequest, undefined];
  
  // chain前面是请求拦截器的方法,从request.handlers里面取出来放进去
  this.interceptors.request.handlers.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  // chain后面是响应拦截器的方法,从response.handlers里面取出来放进去
  this.interceptors.response.handlers.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 经过上述代码的组织,chain这时候是这样的:
  // [request.fulfilled, request.rejected, dispatchRequest, undefined, response.fulfilled,  
  // response.rejected]
  // 这其实已经按照请求拦截器 -> 发起请求 -> 响应拦截器的顺序排好了,拿来执行就行
  
  let promise = Promise.resolve(config);   // 先来个空的promise,好开启then
  while (chain.length) {
    // 用promise.then进行链式调用
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}

上述代码是从 Axios 源码中精简出来的,可以看出他巧妙的运用了职责链模式,将需要做的任务组织成一个链条,这个链条上的任务相互不影响,拦截器可有可无,而且可以有多个,兼容性非常强。

实例:职责链组织表单验证

看了优秀框架对职责链模式的运用,我们再看看在我们平时工作中这个模式怎么运用起来。现在假设有这样一个需求是做一个表单验证,这个验证需要前端先对格式等内容进行校验,然后 API 发给后端进行合法性校验。我们先分析下这个需求,前端校验是同步的,后端验证是异步的,整个流程是同步异步交织的,为了能兼容这种情况,我们的每个验证方法的返回值都需要包装成 promise 才行

// 前端验证先写个方法
function frontEndValidator(inputValue) {
  return Promise.resolve(inputValue);      // 注意返回值是个promise
}

// 后端验证也写个方法
function backEndValidator(inputValue) {
  return Promise.resolve(inputValue);      
}

// 写一个验证器
function validator(inputValue) {
  // 仿照Axios,将各个步骤放入一个数组
  const validators = [frontEndValidator, backEndValidator];
  
  // 前面Axios是循环调用promise.then来执行的职责链,我们这里换个方式,用async来执行下
  async function runValidate() {
    let result = inputValue;
    while(validators.length) {
      result = await validators.shift()(result);
    }
    
    return result;
  }
  
  // 执行runValidate,注意返回值也是一个promise
  runValidate().then((res) => {console.log(res)});
}

// 上述代码已经可以执行了,只是我们没有具体的校验逻辑,输入值会原封不动的返回
validator(123);     // 输出: 123

上述代码我们用职责链模式组织了多个校验逻辑,这几个校验之间相互之间没有依赖,如果以后需要减少某个校验,只需要将它从validators数组中删除即可,如果要添加就往这个数组添加就行了。这几个校验器之间的耦合度就大大降低了,而且他们封装的是 promise,完全还可以用到其他模块去,其他模块根据需要组织自己的职责链就行了。

观察者模式

观察者模式还有个名字叫发布订阅模式,这在 JS 的世界里可是大名鼎鼎,大家或多或少都用到过,最常见的就是事件绑定了,有些面试还会要求面试者手写一个事件中心,其实就是一个观察者模式。观察者模式的优点是可以让事件的产生者和消费者相互不知道,只需要产生和消费相应的事件就行,特别适合事件的生产者和消费者不方便直接调用的情况,比如异步中。我们来手写一个看看:

class PubSub {
  constructor() {
    // 一个对象存放所有的消息订阅
    // 每个消息对应一个数组,数组结构如下
    // {
    //   "event1": [cb1, cb2]
    // }
    this.events = {}
  }

  subscribe(event, callback) {
    if(this.events[event]) {
      // 如果有人订阅过了,这个键已经存在,就往里面加就好了
      this.events[event].push(callback);
    } else {
      // 没人订阅过,就建一个数组,回调放进去
      this.events[event] = [callback]
    }
  }

  publish(event, ...args) {
    // 取出所有订阅者的回调执行
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      subscribedEvents.forEach(callback => {
        callback.call(this, ...args);
      });
    }
  }

  unsubscribe(event, callback) {
    // 删除某个订阅,保留其他订阅
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

// 使用的时候
const pubSub = new PubSub();
pubSub.subscribe('event1', () => {});    // 注册事件
pubSub.publish('event1');                // 发布事件

实例:Node.js 的 EventEmitter

观察者模式的一个典型应用就是 Node.js 的 EventEmitter,我有另一篇文章从发布订阅模式入手读懂 Node.js 的 EventEmitter 源码从异步应用的角度详细讲解了观察者模式的原理和 Node.js 的 EventEmitter 源码,我这里就不重复书写了,上面的手写代码也是来自这篇文章。

实例:转圈抽奖

一样的,看了优秀框架的源码,我们自己也要试着来用一下,这里的例子是转圈抽奖。想必很多朋友都在网上抽过奖,一个转盘,里面各种奖品,点一下抽奖,然后指针开始旋转,最后会停留到一个奖品那里。我们这个例子就是要实现这样一个 Demo,但是还有一个要求是每转一圈速度就加快一点。我们来分析下这个需求: