2

设计模式种类

模式分类模式说明模式例举
创建型设计模式创建型设计模式关注于对象创建的机制方法,通过该方法,对象以适应工作环境的方式被创建。构造器模式(Constructor)、工厂模式(Factory) 、抽象工厂模式 (Abstract) 、原型模式 (Prototype)、 单例模式 (Singleton) 、建造者模式(Builder)
结构设计模式结构模式关注于对象组成和通常识别的方式实现不同对象之间的关系。装饰模式、外观模式、享元模式、适配器模式、代理模式
行为设计模式行为模式关注改善或精简在系统中不同对象间通信。迭代模式、中介者模式、观察者模式、访问者模式
  1. 创建型设计模式

    基本的对象创建方法可能会给项目增加额外的复杂性,而这类模式的目的就是为了通过控制创建过程解决这个问题。

  2. 结构设计模式

    这类模式有助于在系统的某一部分发生改变的时候,整个系统结构不需要改变。

    这也同样有助于对系统中某部分没有达到某一目的的部分进行重组。

模式简述表

SN描述
Creational根据创建对象的概念分成下面几类。
Class
Factory Method(工厂方法)通过将数据和事件接口化来构建若干个子类。
Object
Abstract Factory(抽象工厂)建立若干族类的一个实例,这个实例不需要具体类的细节信息。(抽象类)
Builder (建造者)将对象的构建方法和其表现形式分离开来,总是构建相同类型的对象。
Prototype(原型)一个完全初始化的实例,用于拷贝或者克隆。
Singleton(单例)一个类只有唯一的一个实例,这个实例在整个程序中有一个全局的访问点。
Structural根据构建对象块的方法分成下面几类。
Class
Adapter(适配器)将不同类的接口进行匹配,调整,这样尽管内部接口不兼容但是不同的类还是可以协同工作的。
Bridge(桥接模式)将对象的接口从其实现中分离出来,这样对象的实现和接口可以独立的变化。
Composite(组合模式)通过将简单可组合的对象组合起来,构成一个完整的对象,这个对象的能力将会超过这些组成部分的能力的总和,即会有新的能力产生。
Decorator(装饰器)动态给对象增加一些可替换的处理流程。
Facada(外观模式)一个类隐藏了内部子系统的复杂度,只暴露出一些简单的接口。
Flyweight(享元模式)一个细粒度对象,用于将包含在其它地方的信息 在不同对象之间高效地共享。
Proxy(代理模式)一个充当占位符的对象用来代表一个真实的对象。
Behavioral基于对象间作用方式来分类。
Class
Interpreter(解释器)将语言元素包含在一个应用中的一种方式,用于匹配目标语言的语法。
Template Method(模板方法)在一个方法中为某个算法建立一层外壳,将算法的具体步骤交付给子类去做。
Object
Chain of Responsibility(响应链)一种将请求在一串对象中传递的方式,寻找可以处理这个请求的对象。
Command(命令)封装命令请求为一个对象,从而使记录日志,队列缓存请求,未处理请求进行错误处理 这些功能称为可能。
Iterator(迭代器)在不需要直到集合内部工作原理的情况下,顺序访问一个集合里面的元素。
Mediator(中介者模式)在类之间定义简化的通信方式,用于避免类之间显式的持有彼此的引用。
Observer(观察者模式)用于将变化通知给多个类的方式,可以保证类之间的一致性。
State(状态)当对象状态改变时,改变对象的行为。
Strategy(策略)将算法封装到类中,将选择和实现分离开来。
Visitor(访问者)为类增加新的操作而不改变类本身。

策略模式

概念

将一系列相关算法封装,并使得它们可相互替换。

简单来说:通过向封装的算法传递参数,在其封装的函数中,根据参数去执行对应的函数,达到想要的目的。

  • 可以将策略集中到一个 module 中,然后导出,再在需要的地方导入这些策略,这样就成功解耦。

示例

假如我们现在需要做 5 个判断,当判断成功时,就执行某段代码,很容易我们会想到这么做:

if (x = 1) { }
else if (x = 2) { }
else if (x = 3) { }
else if (x = 4) { }
else if (x = 5) { }

这种做法很是便捷,但是当逻辑和条件变得复杂时,这种做法就会导致难以阅读和维护,以及程序变得臃肿:

    if (x) {
        if (y) {
            if (z) {
                ...
            } else { }
        } else { }
    } else { }

这种”金字塔“编程风格属实让人难以理解。所以现在我们引入与一个最佳实践,也就是常说的设计模式:策略模式。

我们将以上代码通过策略模式,可以改成:

// Map 形式策略
// 你可以将 Map 换成对象,但是 Map 可以存储任意 key 类型。
let strategy:Map<number,()=>void> = new Map([
    [1,(value)=>{console.log(value)}],
    [2,(value)=>{console.log(value)}],
])
let implementStrategy = (number, value) => strategy?.get?.(number)?.(value);
implementStrategy(1,'yomua'); // yomua
implementStrategy(4,99); // 99
// 对象形式策略
let strategy = {
    a(value) { console.log(value) },
    b(value) { console.log(value) },
}
const implementStrategy = (number, value) => strategy[number](value)
implementStrategy('a', 5); // 5
implementStrategy('b', 'Yomua'); // Yomua

通过这个策略模式,我们不需要进行判断,只需要在合适的情况下传入指定的参数,那么就可以达到我们的目的。

至于你说如果你想在达到某个条件时,才执行策略方法,那么你可以:在策略方法中做判断,比如:

// 对象形式策略
let strategy = {
    a(value) { value === 5 ? console.log(value) : console.log('错误的数字') },
}
const implementStrategy = (number, value) => strategy[number](value)
implementStrategy('a', 5); // 5
implementStrategy('a', 1); // 错误的数字
// 我们可以让策略更加智能些
// 接收对象,善用解构赋值
let strategy:Map<number,()=>void> = new Map([
    [1,({value,id,...})=>{console.log(value,id,...)}],
    [2,({value,id,...})=>{console.log(value,id,...)}],
])
let implementStrategy = (number, data) => strategy?.get?.(number)?.(data);
implementStrategy(1,{value,id,....});

// 或使用剩余参数
let strategy:Map<number,()=>void> = new Map([
    [1,(...data)=>{console.log(data)}], // data 是一个数组
    [2,(...data)=>{console.log(data)}],
]);
let implementStrategy = (number, ...data) => strategy?.get?.(number)?.(...data);
implementStrategy(1,{name:'yomua'},"yhw",4,...); 

代理模式

什么是代理模式?

代理模式指的是为对象提供一种代理,以控制对该对象的访问。

代理模式属于结构型模式。

示例

简单实现代理模式

场景

小明打算向女神表白,又怕直接被拒绝而尴尬,决定找女神的同学帮忙转接鲜花给女神

var Flower = function() {};

var xiaoming = {
  sendFlower: function(target) {
    var flower = new Flower();
    target.receive(flower);
  }
}
var classmate = {
  receive: function(flower) {
    girl.receive(flower);
  }
}
var girl = {
  receive: function(flower) {
    console.log('女神收到了花');
  }
}
xiaoming.sendFlower(classmate); // 输出女神收到了花

代理模式实现图片懒加载

不用代理实现

通常实现图片懒加载有很多种方法,现在我介绍其中一种:

  • 在一个函数 A 中 ,通过先在页面创建一个空的 img 元素,和 1 个虚拟的 img 元素的实例(Image 实例),然后使得该函数暴露(返回)另一个 1 个函 B 数或对象 B ;

    这个返回的函数 B 或对象 B 会把你向这里面传递的图片地址赋值给一开始创建的空 img 元素

  • 最后在你想要加载图片的任意位置,调用该函数并传递想要显示的图片的地址,就可以实现图片懒加载。
// 不用代理实现图片懒加载
    // 不用代理实现图片懒加载
    var myImage = (function () {
        var img = document.createElement('img');
        document.body.appendChild(img); // 最后在页面显示的 img
        // 虚拟 Img:代码中存在 img 实例(元素),但页面不存在与之对应的 img 元素。
        /**
         * 拟 img,先赋给虚拟 img src,当虚拟 img 能加载成功传递过来的 src 时
         * 才使得真正的 img 渲染到页面;
         * 通过这么一层虚拟 img 可以防止浏览器渲染了无效的 src,因为即使 src 链接
         * 的地址是不存在的,浏览器也会默认渲染一个占位符,这会导致整个相关 DOM 元素
         * 更新,从而消耗浏览器性能。
         * 而如果通过虚拟 img 先进行判断你这个 src 能否成功加载,只有能成功加载,
         * 才让你渲染到页面,否则让真正的 img 永远无法加载~
         */
        var virtualImg = new Image(); 
        virtualImg.onload = () => { img.src = virtualImg.src; }
        return {setSrc(src) { virtualImg.src = src; }}
    })()
    setTimeout(() => {
        myImage.setSrc('https://pic.qqtn.com/up/2019-9/15690311636958128.jpg')
    }, 1000);

使用代理模式实现

var myImage = (function(){
  var image = document.createElement('img');
  document.body.appendChild(image);
  return {
    setSrc: function(src) {
      image.src = src;
    }
  }
})();

var proxyImage = (function(){
  var img = new Image();
  img.onload = function() {
    myImage.setSrc(this.src);
  }
  return {
    setSrc: function(src) {
      myImage.setSrc('file:///C:/Users/admin/Desktop/mask/img/7.jpg');
      img.src = src;
    }
  }
})()
proxyImage.setSrc('https://img1.sycdn.imooc.com/5c09123400014ba418720632.jpg');

Proxy 实现代理模式

观察者模式和发布订阅模式 这一节的观察者模式的示例中,我们有通过 Proxy 去代理了一个指定的对象,来控制对它们的访问和赋值,而这实际上也是一个代理模式的实现。

观察者模式和发布订阅模式

观察者模式

什么是观察者模式

一个称作观察者的对象,维护一组称作被观察者的对象,当被观察者发生变化时,会发送一条广播,告知所有观察它的观察者,它自身发生的任何变化,这会使得所有观察者都知道被观察者发生了什么变化。

在观察者模式中,如果观察者想要接收被观察对象的通知,则观察者必须到对应的被观察对象上注册该事件,

在以下的示例中,每一个观察者都存入到了 set 集合中,当被观察对象发生改变时(这是一个事件),将会广播通知 set 集合中所有的观察者,这样观察者们就接收到了通知。

下面让我们使用 ProxyReflect 来写一个简单的观察者模式的示例。

示例

<!-- 定义 observable 和 observe,前者使对象成为被观察者,后者使对象成为观察者 -->

TIP:这里的观察者是一个函数。

const set = new Set();
const observable = obj => new Proxy(obj, {
  set: function (target, key, value, receiver) {
    // 先执行默认行为得到最新数据,再通知观察者;否则观察者会观察到旧的数据,而非最新数据
    const result = Reflect.set(target, key, value, receiver);
    set.forEach(observer => observer()); 
    return result;
  },
})
const observe = func => set.add(func);
  • const set = new Set();

    这里是存放观察者的地方

  • observable

    观察某个对象,当该对象发生变化时,通知所有观察者(observe)

    • set.forEach(observer => observer());

      若观察对象发生了改变,将会发送一条广播,使得所有观察者都知道,

      并且你还可以向之传送参数,通知观察者被观察对象发生的变化或任何你想要向观察者传递的信息。

      即:使得每个在 set 中的观察者都被执行(这就相当于广播)

  • observe

    使用该函数来知道“谁”是观察。

    即:定义一个观察者,会将观察者存放到“观察者之家”: set.

    当观察者接收到被观察对象的广播时,将会执行。

<!-- 使用定义的 observable 和 observe -->

// 观察一个对象
const obj = observable({
  name: '张三',
  age: 20
});

// 定义观察者(所有的观察者都会在被观察对象改变时执行,因为被观察对象发生改变时将会发送广播通知所有观察者。
observe(() => {console.log(`${obj.name}`)})
observe(() => {console.log(obj.age)})

// 改变被观察对象
obj.name = 'yomua';

/**
 * yomua
 * 20
 */

优势

观察者和发布/订阅模式鼓励人们认真考虑应用不同部分之间的关系,同时帮助我们找出这样的层,该层中包含有直接的关系,这些关系可以通过一些列的观察者和被观察者来替换掉。这中方式可以有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。

使用观察者模式更深层次的动机是,当我们需要维护相关对象的一致性的时候,我们可以避免对象之间的紧密耦合。例如,一个对象可以通知另外一个对象,而不需要知道这个对象的信息。

两种模式下,观察者和被观察者之间都可以存在动态关系。这提供很好的灵活性,而当我们的应用中不同的部分之间紧密耦合的时候,是很难实现这种灵活性的。

尽管这些模式并不是万能的灵丹妙药,这些模式仍然是作为最好的设计松耦合系统的工具之一,因此在任何的JavaScript 开发者的工具箱里面,都应该有这样一个重要的工具。

发布订阅模式

什么是发布订阅模式?

发布/订阅模式是观察者模式的一种变体实现,虽然它们二者很相似,但是发布订阅模式并不完全等于观察者模式。

观察者模式中,如果观察者想要接收被观察对象的通知,则观察者必须到对应的被观察对象上注册该事件,

详见:观察者模式 => 每一个观察者都存入到了 set 集合中,当被观察对象发生改变时(这相当于一个事件),将会广播通知 set 集合中所有的观察者,这样观察者们就接收到了通知。

很显然,观察者和被观察者直接形成了耦合关系,这很不利于扩展,但是这可以很好的追踪二者的关系。

所以为了解决观察者模式的耦合关系,以观察者模式为基础的变体实现:发布订阅模式就诞生了。

发布订阅模式使得发布者和订阅者之间的依赖性降低,它们之间通过一个称之为”主题/事件频道“的东西进行通信,也就说:订阅者 X,Y,Z 都订阅一个”频道 A“ 和一个 “频道 B”,发布者可以让“频道 A”或“频道 B”向订阅者 X,Y,Z 发布通知,

并且我们可以使得“频道”发布通知时,为每个订阅者发送不同的通知,传递不同的参数,让参数中包含有订阅者所需要的值(这是因为我们可以使得订阅者自定义一个函数,然后频道在发布通知时,调用该函数,并传递参数),

这样一来,发布者和订阅者之间并不会直接进行沟通,全程由频道来执行;因此你可以把“频道”看做是一个“中间商”。

因为发布订阅模式存在这种“频道”概念,当我们将这种概念具象化时,就独立出了一个合适的事件处理函数,该函数用来实现发布、订阅和取消订阅事件。

下面让我们来看看一个简单的,但却完整地实现了功能强大的 publish(), subscribe() 和 unsubscribe() 吧。

示例

<!-- 具象化频道概念,实现:发布、订阅、取消订阅功能 -->

  // 策略模式
  const strategy = new Map([
    [
      'isUidNumber',
      ({ uid }) => {
        if (typeof uid !== 'number') {
          console.error('uid 只能为 number 类型'); return true;
        }
      }
    ],
    [
      'isUidExist',
      ({ uid, subUid }) => {
        if (subUid < uid) { // uid 不存在中返回 true
          console.error(`指定 uid:${uid} 不存在,请重试!`); // 指定的 uid 在频道不存在(超过我们累加的 id)
          return true;
        };
      }
    ],
  ])
  const implementStrategy = (key, data) => strategy?.get?.(key)?.(data);

  // 存放发布、订阅、取消订阅的对象
  const pubsub = {};
  // 将发布、订阅、取消订阅的方法放入 pubsub
  (function (pubsub) { // 自执行函数
    // 存放所有频道
    let topics = {}, subUid = -1; // 每个订阅者的 id,该 uid 自增。
    // 发布 @topicName 频道名;@args 自定义输出的信息
    pubsub.publish = function (topicName, args) { // 使指定的频道对所有订阅者发布信息(发布的行为由订阅者自己设置)
      if (!topics[topicName]) { // 不存在的频道发布了信息
        console.error(`发布失败,请先订阅频道 ${topicName}`)
        return false;
      }
      if (topics[topicName].length === 0) { // 频道存在但没有任何订阅者,不需要发布信息
        console.error(`发布失败,频道 ${topicName} 不存在订阅者`)
        return false;
      }
      const subscribers = topics[topicName]; // 得到指定频道中的所有订阅者
      const len = subscribers ? subscribers.length : 0; // 得到当前订阅的频道中存在多少订阅者
      // 发布一个频道的信息时,通知它的所有订阅者,从最后一个订阅的人开始通知
      // len 为 0 时还会进行最后一次循环,因为最后 1-- 时,会先用 1 来判断,最后再减
      // 如果是 --len,那么会先减,然后用 0 判断,就导致循环体内部的 len 不会为 0
      while (len--) {
        console.log(`${topicName} 频道向所有订阅者发布信息,传递的参数:'${topicName}'' 和 '${args}'`)
        subscribers[len].func(topicName, args); // 调用订阅者自定义的函数并向之传入需要的参数
      }
      return this;
    };

    /** 订阅
     * 将每个订阅者存储到 topics 对象中,每一个订阅者都是一个数组,数组中存一个对象,具有 func 和 uid 属性
     * @func. 频道对订阅者如何发布信息,由用户自定义,传递的参数则由频道来决定(发布者决定)
        (topicName,data)=>{},该函数接收【频道名】和【发布的内容】 作为参数
     * @uid 订阅者的 id,用来标识订阅者
     */
    pubsub.subscribe = function (topicName, func) { // 为指定的频道名添加订阅者
      if (!topics[topicName]) { topics[topicName] = []; }; // 若当前不存在该频道,则初始化频道
      let uid = (++subUid); // 对 uid 进行累加
      topics[topicName].push({ uid, func, }); // 向对应的频道存放每个订阅者的信息:uid 和 自定义的行为
      console.log('当前总共存在的订阅者:')
      console.log(topics)
      return uid;
    };

    // 取消订阅
    pubsub.unsubscribe = function (uid) { // 根据指定的订阅者的 uid 来使得订阅者取消对频道的订阅
      // id 类型非 number 且 uid 不存在,则报错
      if (
          implementStrategy('isUidNumber', { uid }) || 
          implementStrategy('isUidExist', { uid, subUid })
      ) return false;
      console.log(`即将移除 uid:${uid} 的订阅者`)
      for (let topicName in topics) { // 遍历每个频道
        let topicArr = topics[topicName]; // 得到当前频道的所有订阅者
        if (topicArr?.length === 0) { // 当前频道不存在订阅者 
          console.error('频道' + topicName + '不存在订阅者或不存在该频道')
          return false;
        }
        for (let i = 0, j = topicArr?.length; i < j; i++) { // 遍历当前频道中的所有订阅者
          if (topicArr[i].uid === uid) { // 查找频道中的订阅者 uid 和指定 uid 相同的项
            topicArr.splice(i, 1); // 将当前订阅者从频道中移除
            // Reflect.deleteProperty(topics, m) // 移除整个频道
            console.log('移除成功')
            console.log('移除后,现有订阅如下:')
            console.log(topics)
            return uid;
          }
        }
      }
      return this;
    };
  }(pubsub));

以上代码将“频道”这一概念进行具象化实现,虽然简单,但是功能却完整,

具有:订阅、取消订阅和发布功能,下面让我们来使用它吧:

<!-- 使用我们的实现 -->

  console.log('%c----------订阅频道 cctv1 ----------', 'color:red')
  // 频道发布消息时,将执行由用户定义的发布行为。,该函数接收【频道名】和【要发布的内容】
  const cctv1OneFunc = (topciName, data) => { console.log(`我是订阅者 cctv1One 的函数`) }
  const cctv1TwoFunc = (topciName, data) => { console.log(`我是订阅者 cctv1Two 的函数`) }
  // 订阅频道 cctv1,且用一个 callback 接收频道的信息,频道发布时会传递:当前频道名和一个其他自定义信息。
  let cctv1One = pubsub.subscribe("cctv1", cctv1OneFunc);
  let cctv1Two = pubsub.subscribe("cctv1", cctv1TwoFunc);
  // @arg1:要发布信息的频道, @arg2:发布的信息内容,频道对所有订阅者发布消息时,会执行订阅者自定义的函数。
  pubsub.publish('cctv1', 'cctv1 发布信息'); 


  console.log('%c----------订阅频道 cctv2 ----------', 'color:red')
  const cctv2OneFunc = (topciName, data) => { console.log(`我是订阅者 cctv2One 的函数`) }
  let cctv2One = pubsub.subscribe("cctv2", cctv2OneFunc);
  pubsub.publish('cctv2', 'cctv2 你好!')


  console.log('%c----------取消订阅  ----------', 'color:red')
  pubsub.unsubscribe('2'); // error,uid 类型错误
  pubsub.unsubscribe(2); // okay,uid:2 的订阅者取消订阅
  pubsub.unsubscribe(2); // error,频道不存在订阅(早已取消)
  pubsub.unsubscribe(22); // error,uid:22 的订阅者不存在

发布订阅模式的优势和缺陷

优势

发布者和订阅者之间没有直接依赖关系,在后面代码进行扩展或维护时,将带来便利。

观察者和发布/订阅模式都鼓励人们认真考虑应用不同部分之间的关系,同时帮助我们找出这样的层,该层中包含有直接的关系,

这些关系可以通过一些列的观察者和被观察者来替换掉。这中方式可以有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。

两种模式下,观察者和被观察者之间都可以存在动态关系。这提供很好的灵活性,而当我们的应用中不同的部分之间紧密耦合的时候,是很难实现这种灵活性的。

尽管这些模式并不是万能的灵丹妙药,这些模式仍然是作为最好的设计松耦合系统的工具之一,因此在任何的JavaScript 开发者的工具箱里面,都应该有这样一个重要的工具。

缺陷

事实上,发布订阅模式的一些问题实际上正是来自于它所带来的一些好处。

在发布/订阅模式中,将发布者和订阅者解耦,将会在一些情况下,导致很难确保我们应用中的特定部分按照我们预期的那样正常工作;例如,发布者可以假设有一个或者多个订阅者正在监听它们。

比如我们基于这样的假设,在某些应用处理过程中来记录或者输出错误日志。如果订阅者执行日志功能崩溃了(或者因为某些原因不能正常工作),因为系统本身的解耦本质,发布者没有办法感知到这些事情。

另外一个这种模式的缺点是,订阅者对彼此之间存在没有感知,对切换发布者的代价无从得知。因为订阅者和发布者之间的动态关系,更新依赖也很能去追踪。

观察者模式和发布订阅模式的相同点和区别

相同点

两个模式都有通信的双方,观察者和被观察者,发布者和订阅者。

区别

观察者模式中,观察者和被观察者是直接进行耦合,进行互相访问的。

而发布订阅模式中,存在“主题/事件频道”概念,“频道”来对信息进行过滤,执行发布命令的人(发布者)需要通过频道对订阅者发布信息,而订阅者需要去订阅“频道”才能接受发布者发布的信息。

这样发布者和订阅它们双方都不知道对方的存在,都是通过“频道”进行交流,互相依赖性低。

单例模式

什么是单例模式

单例模式也称之为单体模式,规定一个类只有一个实例,并且提供可全局访问点。

所谓的一个类只能有一个模式,即:不管使用 new 对某个类进行几次实例化,所返回的得到的结果都只能是相同的,即:第一次实例化时所得到的对象。

或许你可能没有听过单例模式在这个名词,但是我相信,你肯定在日常编码中使用过它,这是因为单例模式的特点是:“全局”和“唯一”,那么我们可以联想到 JavaScript 中的全局对象。

利用 ES6 的 let 或 const 不允许重复声明的特性,刚好符合这两个特点;是的,全局对象是最简单的单例模式;

let obj = {
    name:"yomua",
    getName:function(){}, // 提供一个函数,可以通过该函数访问到 obj 内部的变量
}

示例

简单版单例模式

分析:只能有一个实例,所以我们需要使用 if 分支来判断,如果已经存在就直接返回,如果不存在就新建一个实例;

let Singleton = function(name){ // 创建一个“类”
    this.name = name;
    this.instance = null; 
}
Singleton.prototype.getName = function(){console.log(this.name);} // 输出实例化“类”时传递的参数
Singleton.getInstance = function(name){
    if(this.instace){return this.instance;} // 使得只存在一个 Singleton 实例
    return this.instance = new Singleton(name);
}

let winner = Singleton.getInstance("winner"); 
winner.getName(); //winner 
let sunner = Singleton.getInstance("sunner"); // 即使多次实例化 Singleton,都会得到第一次实例化的结果
sunner.getName(); //winner

上面代码中我们是通过一个变量 instance 的值来进行判断是否已存在实例,如果存在就直接返回 this.instance,如果不存在,就新建实例并赋值给 instance,这就保证了永远只会存在一个 Singleton 的实例。

但是上面的代码还是存在问题,因为创建对象的操作和判断实例的操作耦合在一起,并不符合”单一职责原则“;

改良版:

思路:通过一个闭包,来实现判断实例的操作;

let CreateSingleton = (function(){
    let instance = null;
    return function(name){
        this.name = name;
        if(instance){return instance}
        return instance = this;
    }
})()
CreateSingleton.prototype.getName = function(){console.log(this.name);}
let winner = new CreateSingleton("winner"); 
winner.getName(); //winner
let sunner = new CreateSingleton("sunner");  
winner.getName();

上面改良的单例模式中,我们通过闭包(在这里是:一个自执行函数返回另一个函数)将用来判断是否已经实例化过的变量和创建实例分开(一开始它们二者都存在于一个函数中,使得这个函数具有两个功能,这不符合”单一职责原则“。

中介者模式

什么是中介者模式

字典中,中介者的定义是:一个中立方,在谈判和冲突解决过程中起辅助作用。

在程序设计模式中:一个中介者是一个行为设计模式,使我们可以导出统一的接口,这样系统不同部分就可以彼此通信。

比如:一个系统中存在大量组件,组件之间需要互相通信,那么我们可以通过中介者模式创建一个中介者并暴露出一个统一的接口,使得每个组件可以通过这个接口去访问其他组件,而非组件和组件进行直接通信。

这和发布/订阅模式很相像,因为发布/订阅模式中也存在着一个“中介”,该中介使得发布者和订阅者必须通过它来通信,而非直接进行交互,它们二者的比较详见:中介者模式 VS 发布订阅模式

优势和缺点

优势

能帮助我们对组件/对象之间进行解耦,改善组件的重用性,并使得整个系统维护成本降低。

缺点

中介者模式的缺点正是由于其优势带来的(发布订阅模式也是如此),对组件/对象之间的通信进行解耦,使得它们通过一个中介者对象进行互相通信,势必会让中介者对象变得庞大、臃肿,

且中介者对象本身就是难以维护的,并且由于中介者模式会新增一个对象,这会带来内存上的开销,性能也会降低,毕竟对象和对象之间的直接访问肯定 比 先访问一个中间商然后才去访问其他对象快得多。

示例

小游戏

// 中介者模式案例:泡泡堂(引入中介者)
let playerDirector = (function () {
  let players = {}; // 存放所有玩家
  let operations = { // 控制玩家状态
    addPlayer: function (player) {
      let teamColor = player.teamColor;
      if (!players[teamColor]) { players[teamColor] = []; }
      players[teamColor].push(player)
    },
    removePlayer: function (player) {
      let teamColor = player.teamColor;
      let teamPlayers = players[teamColor] || [];
      for (let index = 0, len = teamPlayers.length; index < len; index++) {
        if (teamPlayers[index] == player) { teamPlayers.splice(index, 1); break; }
      }
    },
    changeTeam: function (player, newTeamColor) {
      operations.removePlayer(player);
      player.teamColor = newTeamColor;
      operations.addPlayer(player);
    },
    playerDead: function (player) {
      let teamColor = player.teamColor;
      let teamPlayers = players[teamColor];
      let allDead = true;
      player.state = 'dead';
      for (let index = 0, len = teamPlayers.length; index < len; index++) {
        if (teamPlayers[index].state != 'dead') { allDead = false; break; }
      }
      if (allDead) {
        for (let index = 0, len = teamPlayers.length; index < len; index++) {
          teamPlayers[index].lose();
        }
        for (let color in players) {
          if (color != teamColor) {
            for (let index = 0, len = players[color].length; index < len; index++) {
              players[color][index].win();
            }
          }
        }
      }
    }
  };
  // 玩家状态改变(死亡/改变对象等)就执行对应的操作
  let ReceiveMessage = function () {
    // 以下两个 arguments 指的是玩家的属性:{name,state,teamColor,_proto_}
    let message = Array.prototype.shift.call(arguments); // 得到当前执行的操作,如:addPlayer。
    operations[message].apply(this, arguments); // 执行对应的方法并传入 arguments(玩家的属性)
  }
  return { ReceiveMessage }; // 返回一个对象
})()
// 定义玩家属性和公共行为
let Player = function (name, teamColor) {
  this.name = name;
  this.teamColor = teamColor;
  this.state = 'live';
}
Player.prototype.win = function () { console.log(this.name + '胜利了'); }
Player.prototype.lose = function () { console.log(this.name + '失败了'); }
Player.prototype.remove = function () {
  console.log(this.name + '掉线了');
  playerDirector.ReceiveMessage('removePlayer', this);
}
Player.prototype.die = function () {
  console.log(this.name + '死亡');
  playerDirector.ReceiveMessage('playerDead', this);
}
Player.prototype.changeTeam = function (color) {
  console.log(this.name + '换队');
  playerDirector.ReceiveMessage('changeTeam', this, color);
}

// 以工厂模式创建新玩家
let playerFactory = function (name, teamColor) {
  let newPlayer = new Player(name, teamColor);
  playerDirector.ReceiveMessage('addPlayer', newPlayer);
  return newPlayer;
}
// 红队
let player1 = playerFactory('张三', 'red'),
  player2 = playerFactory('张四', 'red'),
  player3 = playerFactory('张五', 'red'),
  player4 = playerFactory('张六', 'red');
// 蓝队
let player5 = playerFactory('辰大', 'blue'),
  player6 = playerFactory('辰二', 'blue'),
  player7 = playerFactory('辰三', 'blue'),
  player8 = playerFactory('辰四', 'blue');

/** 开始比赛 */
// 掉线
// 依次输出:张三掉线了 张四掉线了
player1.remove();
player2.remove();

// 更换队伍
// 依次输出:张五换队 张五死亡
player3.changeTeam('blue');

// 阵亡
// 依次输出:辰大死亡 辰二死亡  辰三死亡 辰四死亡
// 辰大失败了 辰二失败了  辰三失败了 辰四失败了 张五失败了
// 张六胜利了
player3.die();
player5.die();
player6.die();
player7.die();
player8.die();

类似发布订阅模式的中介者模式示例

let mediator = (function () {
  //存储可以广播或收听的 topic
  let topics = {};
  // 订阅一个 topic,提供一个 callback,当指定的 topic 需要被广播时,调用该 callback
  let subscribe = function (topic, fn) {
    if (!topics[topic]) { topics[topic] = []; }
    topics[topic].push({ context: this, callback: fn });
    return this;
  };
  //向应用程序的其余部分发布/广播事件
  let publish = function (topic) {
    let args;
    if (!topics[topic]) { return false; }
    args = Array.prototype.slice.call(arguments, 1);
    for (let i = 0, l = topics[topic].length; i < l; i++) {
      let subscription = topics[topic][i];
      subscription.callback.apply(subscription.context, args);
    }
    return this;
  };
  return {
    publish: publish,
    subscribe: subscribe,
    installTo: function (obj) {
      obj.subscribe = subscribe;
      obj.publish = publish;
    }
  };
}());

如果你仔细查看发布订阅模式中的示例,你会发现,本示例只是把变量名字由 pubsub 换成了 mediator 而已。

因为这两个模式在某种程度上,可以说实现都是类似的,只不过侧重点不同。

中介者模式 VS 发布订阅模式

开发人员往往不知道中介者模式和发布/订阅模式之间的区别。

不可否认,这两种模式之间有一点点重叠,如:它们都通过”中介“使得对象和对象之间进行解耦,但是它们之间还是有不同点的。

让我们来回顾一下发布订阅模式的特点:

它定义了发布者和订阅者之间的依赖关系或许是一对多(一个订阅者订阅多个频道),又或者是多对一(多个订阅者订阅一个频道)、多对多(多个订阅者订阅多个频道)等,当发布者通过频道发布通知时,该频道下所有的订阅者都会接收到该通知。

而中介者模式主要作用是:用一个中介对象来封装一系列的对象交互,使得对象和对象之间解耦。

显然的,发布订阅模式强调的是统一通讯这一概念,而中介者模式强调是对象和对象之间交互,而非统一通讯。

工厂模式

在中介者模式的示例小游戏中我们有使用到工厂模式,现在让我们来看看你什么是工厂模式吧。

什么是工厂模式

工厂模式是一种关注对象创建概念的创建模式。它旨在暴露出一个公共接口,且使用该公共接口去指定我们想要创建的对象的类型,这样我们就避免了通过直接使用 new 运算符去创建对象。

比如:我们需要创建一种类型的 UI 组件,在不使用工厂模式时,可以这么做:用一个函数去描述该组件,每次想要创建一个新的组件就实例化(new)该函数,这样我们就得到了不同属性但同类型的组件;

而工厂可以在定义一个函数,在该函数内部去实例化这个函数组件,想要创建新组件,只需要调用该函数并传入合适的参数即可;这个函数就像一个工厂,它内部的“流水线”非暴露的,我们交予一个东西给工厂,工厂就会生产出我们需要的东西。

示例

// 每种车辆类型的属性
function Car(options) {
  this.doors = options.doors || 4;
  this.state = options.state || "brand new";
  this.color = options.color || "silver";
}

function Truck(options) {
  this.state = options.state || "used";
  this.wheelSize = options.wheelSize || "large";
  this.color = options.color || "blue";
}

// 定义工厂、如何使用工厂生产的方法
function VehicleFactory() { }
VehicleFactory.prototype.createVehicle = function (options) {
  if (options.vehicleType === "car") {this.vehicleClass = Car;} 
      else {this.vehicleClass = Truck;}
  return new this.vehicleClass(options);
};

var carFactory = new VehicleFactory(); // 得到工厂

var car = carFactory.createVehicle({ // 让工厂帮我们创建需要的类型为 car 的车
  vehicleType: "car",
  color: "yellow",
  doors: 6
});

var truck = carFactory.createVehicle({  // 修改工厂创建的车辆类型为:truck
  vehicleType: "truck",
  state: "like new",
  color: "red",
  wheelSize: "small"
});

// 得到车辆信息
console.log(car);
console.log(truck);

不难看出,使用工厂模式能让目的很容易被得知,如果在以上示例中,直接使用 new Car/Truck(options) 的方式固然也能得到一样的结果,但是一旦当车辆类型变多时,复数个 new XX 会给维护人员或者几个月后的你带来疑惑,

且这也并不容易使得该程序进一步扩展,比如:我们创建车辆时,需要判断当前车辆的颜色是否为 "yellow" 才创建,那么我们需要在复数个类似 Car 这样的函数中去添加判断才能完成,

但是如果基于以上示例,我们只需要在工厂中判断 Options.color 是否为 "yellow" 即可,诸如此类还有很多,就不一一列举了。

工厂模式的适用情况

当被应用到下面的场景中时,工厂模式特别有用:

  • 当我们的对象或者组件设置涉及到高程度级别的复杂度时。
  • 当我们需要根据我们所在的环境方便的生成不同对象的实体时。
  • 当我们在许多共享同一个属性的许多小型对象或组件上工作时。
  • 当带有其它仅仅需要满足一种API约定(又名鸭式类型)的对象的组合对象工作时.这对于解耦来说是有用的。

何时不要去使用工厂模式:

当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性。

除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。

由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

抽象工厂

什么是抽象工厂

抽象工厂指的是:把一组独立的工厂封装在一起的工厂。

抽象工厂的目标:以一个通用的目标将一组独立的工厂进行封装。

以上二者表明抽象工厂仍是工厂模式的一种。

抽象工厂的适用

抽象工厂应该被用在一种必须从其创建或生成对象的方式处独立,或者需要同多种类型的对象一起工作这样的系统中。

示例

// 定义车辆以及车辆默认属性
function Car(options) {
  this.doors = options.doors || 4;
  this.state = options.state || "brand new";
  this.color = options.color || "silver";
}

function Truck(options) {
  this.state = options.state || "used";
  this.wheelSize = options.wheelSize || "large";
  this.color = options.color || "blue";
}

// 一个对象工厂,用来注册、创建、得到车辆
var AbstractVehicleFactory = (function () {
  var typesObj = {};
  return {
    getVehicle: function (type, customizations) {
      var Vehicle = typesObj[type];
      return (Vehicle ? new Vehicle(customizations) : null);
    },
    registerVehicle: function (type, VehicleFunc) {
      var proto = VehicleFunc.prototype;
      if (proto.contract) typesObj[type] = VehicleFunc;  // 仅仅注册有合同的车辆
      return AbstractVehicleFactory;
    },
    registerContract: function (type) {
      if (typeof type !== 'function') { console.log('应传入一个车辆类型'); return; }
      console.log(type.name === 'Car')
      if (type.name === 'Car') { Car.prototype.contract = true }
      if (type.name === 'Truck') { Truck.prototype.contract = true }
    }
  };
})();

// 为车注册合同
AbstractVehicleFactory.registerContract(Car);
// 注册车辆
AbstractVehicleFactory.registerVehicle("car", Car);
AbstractVehicleFactory.registerVehicle("truck", Truck);

// 车辆属性
var car = AbstractVehicleFactory.getVehicle("car", {
  color: "lime green",
  state: "like new",
});

var truck = AbstractVehicleFactory.getVehicle("truck", {
  wheelSize: "medium",
  color: "neon yellow"
});

console.log(car); // {doors: 4, state: "like new", color: "lime green"}
console.log(truck); // null

以上示例中,AbstractVehicleFactory 对象可以认为是一个抽象工厂,它将三个独立的目标进行组装,从而得到它。

当然,你或许认为这三个函数它们是一个工厂中的,而 AbstractVehicleFactory 并非抽象工厂,只是一个普通的工厂,这样理解也不是不行,毕竟抽象工厂就是普通工厂模式衍生过来的。

原型模式

什么是原型模式

原型模式指的是:通过克隆的方式,基于一个现有对象的模板创建对象的模式。

即:以现有对象作为蓝图,从而创建新对象的模式。

你可以把原型模式认为是一种基于原型(Prototype)的继承。

使用原型模式的好处

使用原型模式的好处之一就是,我们在 JavaScript 提供的原生能力之上工作的,而不是 JavaScript 试图模仿的其它语言的特性。

且该模式还会带来一些性能上的提升:当为”蓝图“(基对象)定义方法时,这些方法都是使用引用(对象.方法名)创建的,这会使得基于蓝图创建的子对象,都会指向同一个方法(蓝图中的方法),而不是子对象单独创建一个该函数的拷贝,

那么这样,自然就会带来性能上的提升。

示例

使用 Object.create() 应用原型模式

var vehicle = {
  getModel: function () {
    console.log(this); // 指的是 car 对象:{age: "21", name: "yomua"}
    console.log(this.age); // 21
  }
};
var car = Object.create(vehicle, {
  "age": {value: '21',enumerable: true}, // 这里的 age 值是:21,并非是一个对象
  "name": {value: "yomua",enumerable: true }
});
car.getModel(); // 21
console.log('getModel' in car); // true

使用 Object.create() 去实现原型模式是很容易的一件事情,因为该方法本身就创建了一个拥有特定原型的对象,并且还可以为指定的对象添加属性和其描述,如:

Object.create(proto[,propertiesObject])

  • proto:以该对象作为创建的新对象的原型
  • propertiesObject:要添加到新创建对象的可枚举属性(属于自身,而非属于原型链上的),默认为 undefined.
  • 返回值:一个新对象

不使用 Object.create() 应用原型模式

即使不使用 Object.create() 我们也能模拟原型模式:

var vehiclePrototype = {
  init(carName) { this.name = carName; },
  getModel() { console.log("汽车名:" + this.name); } // 汽车名:yomua
};

// 工厂模式,用一个工厂去帮我们把原型添加到函数上,并初始化车辆名字。
function vehicleFactory(carName) {
  function Car() { }
  Car.prototype = vehiclePrototype;
  const car = new Car();
  car.init(carName);
  return car;
}
const car = vehicleFactory('yomua')
car.getModel(); // 汽车名:yomua

通过以上示例,不难发现:如果不使用类似 Object.create() 这样的方法直接为某个对象创建原型,那么就只好先创建一个函数,为该函数创建原型,再实例化该函数得到其实例(对象),最后在用该函数的实例获取原型上的方法/属性了。

或者你能再简单点:

const car = (function (){
    function Car(){};
    Car.prototype = vehiclePrototype;
    return new Car();
})();
car.init('yomua');
car.getModel(); // 汽车名:yomua

本节中通过为函数添加原型去应用原型模式的这种方法,实际上不算正宗的原型模式,因为原型模式只是将一个对象链接到另一个对象,并没有其他概念,

而这里使用的方法很明显的多创建了一个函数,不过即使如此,但本节中使用的方法在一些情况下比直接使用 Object.create() 要更甚一筹。

命令模式

什么是命令模式

命令模式就是将请求或者操作封装到一个单独的对象中,使得请求/操作可以进行参数的传递,并以函数的形式被执行。

另外命令模式使得我们可以将【对象实现的行为】以及【对这些行为的调用】进行解耦。

命令模式的理念:将执行对象的某个行为这样的责任,从对象上分离,取而代之的是将这种责任委托给其他对象或是让当前对象的某个行为去发出命令。

语义更加清楚,令维护人员和开发人员神清气爽。

示例

let CarManager = {
  requestInfo(model, id) { return `${model},${id}`; },
  buyVehicle(model, id) { return `${model},${id}`; },
  arrangeViewing(model, id) { return `${model},${id}`; }
};

以上是将三个相关的行为封装到一个对象中,通常如果我们想要使用这些行为,是这么做的:CarManager.xxx(…)

当然,这样做无可厚非,这段 JS 代码并没有做错什么,但是没有做错什么并不代表就做好了,为什么?

比如:想象如果 CarManager 的核心 API 会发生改变的这种情况;这可能需要所有直接访问这些方法的对象也跟着被修改。

而这可以被看成是一种耦合,明显违背了 OOP 方法学尽量实现松耦合的理念,取而代之,我们可以通过更深入的抽象这些 API 来解决这个问题。

让我们来加一个命令,让该命令负责执行某对象的某行为:

const run = (thisArg, funcName, ...rests) => thisArg[funcName].apply(thisArg, rests);;
// 或
CarManager.execute = function (funcName) { // 将命令作为该对象的行为,让这个行为负责执行其他行为
    return this[funcName] && this[funcName].apply(CarManager, [].slice.call(arguments, 1))
}

这样,我们可以使用 run 命令,使用如下的形式,来执行指定对象的指定行为:

run(CarManager, 'arrangeViewing', '小丑模式', '9999'); // model:小丑模式,id:9999
...
// 或
CarManager.execute('arrangeViewing', '小丑模式', '9999'); // model:小丑模式,id:9999
...

命令模式的优势和缺点

优势

在示例中,我们应用了命令模式,那么这样做有什么好处呢?

其实最容易让人发掘的好处就是:这种以命令形式执行的行为,其语义和美观程度上更好

同时也降低了系统耦合度——对象直接访问行为变成了通过命令控制访问哪个对象的行为。

就是说:使用者其实其实并不需要关注该行为是如何被调用的或者说对象是如何调用该行为的,使用者只需要知道:这个命令可以帮你执行对象的行为,以及你可以向其行为中传递一些参数来决定行为的模式。

所以这就是让对象和行为进行解耦。

缺点

使用命令模式可能会导致某些系统有过多的具体命令类。

使用场景

在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。

在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。

即:我们可以应用命令模式:通过调用者调用接受者执行命令,顺序:调用者→命令→接受者

笔者注:这里的调用者指:存储一系列命令的对象,通过该对象调用命令,使得接受者执行。

外观模式

什么是外观模式

外观模式通常指的是:当你完成一个模块、框架等诸如此类时,对外暴露出使用你的模块、框架的方法(可能是是一个接口、函数什么的)。

这一模式提供了面向一种更大型的代码体提供了一个的更高级别的舒适的接口,隐藏了其真正的潜在复杂性。把这一模式想象成要是呈现给开发者简化的API,一些总是会提升使用性能的东西。

一些经典的外观模式如:jQuery 的 $(),它将背后深层次的复杂性隐藏,只提供了一个简单的 $() 函数供人使用。

使用外观模式而对代码抽象时的注意事项

外观模式一般没有多少缺陷,但是性能是值得注意的问题。

也就是说,需要确定外观模式提供的接口(可以称之为门面)在为我们提供实现的同时是否为我们带来了隐性的消耗,如果是这样的话,那么这种消耗是否合理。

回到 jQuery 库,我们都知道 getElementById('identifier') 和 $("#identifier") 都能够被用来借助ID查找页面上的一个元素,然而你是否知道 getElementById() 拥有更高数量级的速度呢?

这个特定的门面模式所面临的挑战就是,为了提供一种优雅的接受和转换多种查询类型的选择器功能,就会有在抽象上的隐性成本。用户并不需要访问 jQuery.getById("identifier") 或者 jQuery.getbyClass("identifier") 等等方法。

那就是说,在性能上权衡已经通过了多年的实践考量,并且带了 jQuery 的成功,一个实际上为团队工作得很好的门面。

当使用这个模式的时候,尝试了解任何有关性能上面的消耗,要知道它们是否值得以抽象的级别被提供出来调用。

Mixin 模式

什么是 Mixin 模式?

Mixin 模式指的是:提供一个基类,能够被一个或一组子类继承。

  • 值得一提是的:这种继承并不是并不是将一个对象 A 的原型指向某个对象 B ,再让 A 的实例从原型上获取方法,而是把 B 的方法赋值给 A 的实例,使得 A 的实例有自己的方法。

    详见:本节示例

Mixin 模式的目的:重用功能。

在诸如 C++ 或者 List 这样的传统语言中,广泛使用着 Mixin 模式;现在,我们可以通过 JavaScript 的原型来模拟这一模式。

示例

<!-- 创建 Mixin 和需要混入的构造器。 -->

// 需要 Mixin 的构造器
const Car = function (settings) {
  this.model = settings.model || "no model provided";
  this.color = settings.color || "no colour provided";
};

// Mixin 准备混入到某实例的构造器
const Mixin = function () { };
Mixin.prototype = {
  driveForward: function () { console.log("drive forward"); },
  driveBackward: function () { console.log("drive backward"); },
  driveSideways: function () { console.log("drive sideways"); }
};

// 使 receivingClass 可以基于 mixin 扩展
function augment(receivingClass, mixin) {
  // 使用者没有指定把 Mixin 的哪个方法混入,就混入所有方法
  if (!arguments[2]) { // 
    for (var methodName in mixin.prototype) { // 遍历第 mixin 原型上的属性
      if (!receivingClass.prototype[methodName]) { // 若 receivingClass 中不存在 Mixin 原型上的方法
        // 则将 Mixin 的原型方法赋值给 receivingClass 的原型
        receivingClass.prototype[methodName] = mixin.prototype[methodName];
      }
    }
    return;
  }
  // 混入指定方法
  for (var i = 2, len = arguments.length; i < len; i++) {
    if (!receivingClass.prototype[arguments[i]]) {
      // aguments[i] 是一个方法名,类型为字符串
      receivingClass.prototype[arguments[i]] = mixin.prototype[arguments[i]];
    }
  }
}

<!-- 使用 Mixin -->

// 把 Mixin 中指定的方法添加到 Car 的原型。
augment(Car, Mixin, "driveForward", "driveBackward");
var myCar = new Car({
  model: "Ford Escort",
  color: "blue"
});
// 测试以确保可以访问扩展自 Mixin.prototype 的方法。
myCar.driveForward();
myCar.driveBackward();
// 使得 Car 实例拥有 Mixin 的所有功能
augment(Car, Mixin);
var mySportsCar = new Car({
  model: "Porsche",
  color: "red"
});
mySportsCar.driveSideways(); // drive sideways

在以上实例中,我们通过 augment() 能将 Mixin 原型上的指定/所有方法混入到指定构造器的有原型上,使得构造器的实例能使用这些原本属性 Mixin 中的方法。

同时,若指定的构造器中重载/重写了 Mixin 原型上的方法,那么 Mixin 原型中对应的方法并不会覆盖这些重写/重载的方法,这是因为我们把 Mixin.protoype.xxx 赋值给指定 构造器.prototype.xxx 时做了判断:

// 只有当指定构造器的原型上,没有和 Mixin 原型上对应的方法时,
// 才将 Mixin.prototype.xxx 赋值给 构造器.prototype.xxx
if (!receivingClass.prototype[methodName]) {
     receivingClass.prototype[methodName] = mixin.prototype[methodName];
}

Mixin 模式的优缺点

Mixin 支持在一个系统中降解功能的重复性,增加功能的重用性。在一些应用程序也许需要在所有的对象实体共享行为的地方,我们能够通过在一个 Mixin 中维护这个共享的功能,来很容易的避免任何重复,而因此专注于只实现我们系统中真正彼此不同的功能。

也就是说,对 Mixin 的副作用是值得商榷的:一些开发者感觉将功能注入到对象的原型中是一个坏点子,因为它会同时导致原型污染和一定程度上的对我们原有功能的不确定性,在大型的系统中,很可能是有这种情况的。

但笔者认为,”一刀切“是不可取的我们在使用各个设计模式时,如果没有正确的使用它,即使再好的模式都会鼠屎污羹罢了,且除了正确使用设计模式之外,一份强大的文档是必不可少的,它能帮助未从接触过该系统的人快速上手。

所以,基于此而言,Mixin 模式的使用只要正确,是没有多大问题的。

装饰器模式

什么是装饰器模式?

装饰模式是旨在提升重用性能的一种结构性设计模式。同 Mixin 模式类似,装饰器模式也可以被看作是应用子类划分的另外一种有价值的可选方案。

装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构;

装饰器模式通常会创建一个装饰类,用来包装原本的类,使得原本的类在不改变自身的前提下增加包装类具有的功能;

开发者使用装饰器模式的其中一个理由是:应用程序中存在大量彼此不相干类型对象的特性,需要将之抽象。

  • 比如:一个 JS 游戏中存在人类、霍比特人、精灵、兽人、戒指、棍棒等一系列模块,而这些模块又可以进行相互组合,衍生出新的功能:精灵戴着戒指,拿着棍棒打兽人诸如此类。

    很显然,随着模块越来越多,程序中的构造器也就越来越多,最后肯定是难以控制,甚至是不可控的,

    所以开发者通常会使用装饰器模式来抽象出共有功能,然后将这些抽象出来的功能附加给现有对象,从而实现程序的解耦以及降低程序的复杂性。

这是因为装饰器模式并不去深入依赖于对象是如何创建的,而是专注于扩展它们的功能这一问题上,这并不同于只依赖于原型继承,我们在一个简单的基础对象上面逐步添加能够提供附加功能的装饰对象,从而使这个基础对象慢慢饱满起来,因此装饰器模式是更轻巧的

装饰器模式的实现

实现装饰器模式最基础的方法是通过:创建一个装饰类(JS 中指函数),在内部对原方法进行重载,得到一个具有原方法又有其他行为的方法,然后使用该装饰类,这样就通过重载的方式为原方法添加了这个装饰类想要装饰的新功能,详见:使用简单且典型的装饰器

另一种基本方法则是:通过一个“职责链”,将需要被装饰的类放入职责链中,当被装饰类执行时,被装饰类所处的职责链的其他类也将被执行,从而达到为被装饰添加新功能的目的,详见:使用 AOP 实现装饰器模式

装饰器模式目的

意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。

何时使用:在不想增加很多子类的情况下扩展类。

示例

使用简单且典型的装饰器

// The constructor to decorate
function MacBook() {
  this.cost = () => 997;
  this.name = 'yomua';
  this.color = 'color';
}

// Decorator 1
function Memory(macbook) {
  const v = macbook.cost(); // 得到当前对象的 cost() 返回值
  // 重载 macbook 的 cost(),并使得 cost() 返回值在原本的基础上再加 75
  macbook.cost = () => v + 75;;
}

// Decorator 2
function Engraving(macbook) {
  const v = macbook.cost();
  macbook.cost = () => v + 200;
}

const mb = new MacBook();

// 以下方法将重载 mb.cost(),使得它的 cost() 返回值增加 75 和 200
// 我们将类似这样形式的重载称作:装饰
// 即:Memory / Engraving 装饰了 MacBook 的实例 mb.
Memory(mb);
Engraving(mb);
console.log(mb.cost()); // 997 + 75 + 200 = 1272

在上面的示例中,我们的装饰器重载了超类对象:MacBook() 的 object.cost() 函数,使其返回的 Macbook 的当前价格加上了被定制后升级的价格。

这被看做是对原来的 Macbook 对象构造器方法的装饰,它并没有将其重写(例如,cost()),我们所定义的 Macbook 的其它属性也保持不变,完好无缺。

使用 AOP 实现装饰器模式

用AOP装饰函数的技巧在实际开发中非常有用,无论是业务代码的编写,还是在框架层面,我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把他们合并在一起,这有助于我们编写一个松耦合和高复用性的系统。

<body>
  <button id="btnLogin">点击打开登录浮层</button>
</body>
<script>
  // before() 和 after() 都是装饰器。
  Function.prototype.before = function (beforeFn) {
    const _self = this; // 指:showLogin 这个函数,因为该函数由 showLogin() 调用。
    return function () {
      // this: #btnLogin 这个对象,因为该返回值函数由 btnLogin 按钮触发
      beforeFn.apply(this, arguments);
      return _self.apply(this, arguments); // 以指定 this 作为 showLogin 中的 this,并传递 arguments 作为参数,然后执行 showLogin() 并得到其返回值。
    }
  }
  Function.prototype.after = function (afterFn) {
    let _self = this;  // before() 返回的函数,因为该函数由 before() 调用(befroe() 执行完后,才接着调用 after())
    return function () {
      // this: #btnLogin 这个对象,因为该返回值函数由 btnLogin 按钮触发
      var ret = _self.apply(this, arguments);
      afterFn.apply(this, arguments);
      return ret;
    }
  }
  const showLogin =  () => console.log('打开登录浮层');

  // 依次输出:按钮点击之前上报  打开登录浮层  按钮点击之后上报
  // 还未单击按钮时,就会获取 before() 个 after() 的返回值,单击时,会从 after() 的返回的函数开始执行。
  // 这是因为我们依次调用了 before() 和 after(),根据栈先入后出的原理,故先调用 after() 返回的函数。
  document.getElementById('btnLogin').onclick = showLogin.before(function () {
    console.log('按钮点击之前上报');
  }).after(function () {
    console.log('按钮点击之后上报');
  })
</script>
  • 单击按钮,执行 after() 返回的函数 -> before() 返回的函数

装饰器模式优缺点

优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模器式是继承的一个替代模式,装饰器模式可以动态扩展一个实现类的功能,即:对象可以用新的行为封装或者“装饰”起来,而后继续使用,并不用去担心基础的对象被改变。

缺点:多层装饰比较复杂。如果穷于管理,它也会由于引入了许多微小但是相似的对象到我们的命名空间中,从而显著的使得我们的应用程序架构变得复杂起来

亨元模式

什么是亨元模式

亨元模式又可以称之为:轻量级(Flyweight)模式,该模式是一个优化重复、缓慢和低效数据共享代码的经典结构化解决方案

亨元模式的目标是为相关对象尽可能多的让它们共享数据,来减少应用程序中内存的使用(例如:应用程序的配置、状态等)。

即:亨元模式主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

举个例子:每个人有 n 件衣服,普通的程序可能这么表示:

const Cloth = function (color) {
  console.log(`衣服,其颜色:${color}`)
  return color;
};
const Human = function () {
  const cloth1 = new Cloth('红色');
  const cloth2 = new Cloth('绿色');
  const cloth3 = new Cloth('蓝色');
  // ...
}
new Human();

以上示例表示每件衣服都是一个模块,而随着衣服变多,那么普通的从程序中将会有多个 Cloth 实例,以此来表示衣服。

但我们可以聪明些——显然的, 我们可以所有衣服看作一个实例:

const Cloth = function (color) {
  for (let v of color) { console.log(`衣服,其颜色:${v}`) };
  return color;
}
const Human = function () {
  const color = ['红色', '绿色', '蓝色']
  new Cloth(color); // 即使有多个衣服,只需要一个实例即可。
}
new Human()

以上两个示例都输出:

  • 衣服,其颜色:红色

    衣服,其颜色:绿色

    衣服,其颜色:蓝色

亨元模式的两种使用方法

亨元模式存在两种两种方法使用:

  1. 数据层,基于存储在内存中的大量相同对象的数据共享的概念
  2. DOM 层,享元模式被作为事件管理中心,以避免将事件处理程序关联到我们需要相同行为父容器的所有子节点上

通常来说,亨元模式更多的用于数据层,而非 DOM 层。

享元和数据共享

享元模式中有一个两种状态的概念:

  1. 内在

    内在信息可能会被我们的对象中的内部方法所需要,它们绝对不可以作为功能被带出

    带有相同内在数据的对象可以被一个单独的共享对象所代替,它通过一个工厂方法被创建出来。

    这允许我们去显著降低隐式数据的存储数量。

  2. 外在

    外在信息则可以被移除或者放在外部存储,通常来说,亨元就是外在数据。

    我们可能会使用一个管理器来处理外在状态,如何实现可以有所不同,但针对此的一种方法就是:

    让管理器对象包含一个【存储外在状态以及它们所属的享元对象】的中心数据库。

把对象作为共享数据 一节中,亨元( CoffeeOrder)其实指的就是外在数据,它被抽取出来,供其他对象共同使用,而非为每个对象都创建相同的功能。

内在数据则是如 CoffeeFlavorFactory 构造器中的 let flavors = {} 这样的,该对象将每个咖啡实例存储起来,一旦继续创建新的咖啡实例时,若该对象中已经存在了准备创建的咖啡实例,则直接返回该对象中的实例,而非继续创建相同的咖啡实例。

示例

简单使用亨元模式来共享数据

背景:某内衣厂生产有50种男士内衣和50种女士内衣,正常情况下,需要50个男模特和50个女模特来完成对内衣的试穿拍照。

不使用亨元模式:

const Model = function(gender,underwear) {
  this.sex = gender;
  this.underwear = underwear;
}
Model.prototype.takePhoto = function() {console.log('sex='+this.sex+',underwear='+this.underwear);}

for (let index = 0; index < 50; index++) {
  const model = new Model('male','underwear'+index); // 50 个男士+内衣
  model.takePhoto();
}
for (let index = 0; index < 50; index++) {
  const model = new Model('female','underwear'+index); // 50 个女士+内衣
  model.takePhoto();
}

在以上示例中,如果要得到一张女士/男士的内衣试拍照只需要实例化 Model,并传入其性别和准备试拍的内衣,再调用 takePhoto() 即可得到一张试拍照。

而想要得到一张试拍照,就需要一个 Model 的实例,50 张照片就需要 50 个实例,1000 张就需要 1000 个实例,这显然是无法接受的,所以我们引入亨元模式,找到以上示例中能共享的数据,

显然,共享的数据是:”模特“这个实例,即:男模特和女模特,一个男/女模特实例能分别对应 50 个内衣,这样,我们只需要两个实例(一个男模特和一个女模特实例),然后让它们穿不同种类的内衣再试拍即可,下面让我们用代码来描述一下:

使用亨元模式:

const Model = function (sex) { this.sex = sex;}
Model.prototype.takePhoto = function () {
  console.log('sex=' + this.sex + ',underwear=' + this.underwear);
}

// 实例化一个男模特和女模特
const male = new Model('male');
const female = new Model('female');

// 每次男模特试穿衣服时就拍张照,拍 50 张
for (let index = 0; index < 50; index++) {
  male.underwear = index+1;
  male.takePhoto();
}

// 每次女模特试穿衣服时就拍张照,拍 50 张
for (let index = 0; index < 50; index++) {
  female.underwear = index+1;
  female.takePhoto();
}

在以上示例中,我们只需要两个实例就完成了对 100 张照片的拍摄,而且即使所需要拍的照片增多也是如此,而这正式亨元模式共享数据带来的好处,降低了程序所需要的内存。

注意:我们这里手动为两个 Model 的实例添加了 underwear 属性,其实是不合理的,在更加复杂的系统中,这并不是一个好办法——外部状态(Model)可能是相对比较复杂的,它们与内部的对象(underwear)的联系会变得更加困难

  • 外部状态:Model
  • 内部对象:underwear

把对象作为共享数据

以下示例不仅使用了亨元模式,还使用了工厂模式

以下各个实现的定义如下:

  • CoffeeOrder:享元。即:被共享的数据。
  • CoffeeFlavor:构造享元
  • CoffeeTable:辅助器
  • CoffeeFlavorFactory:享元工厂
  • testFlyweight:对我们享元的使用

请详见 享元和数据共享 这节,该节中介绍了本节的内在和外在状态的判断,以及亨元模式最大的特点:共享数据是什么?

Function.prototype.implementsFor = function (parentClassOrObject) {
  if (parentClassOrObject.constructor === Function) {
    // 正常实例(传入的参数是一个构造器)
    this.prototype = new parentClassOrObject();
    this.prototype.constructor = this;
    this.prototype.parent = parentClassOrObject.prototype;
  } else {
    // 纯虚拟继承(传入的参数并非构造器)
    this.prototype = parentClassOrObject;
    this.prototype.constructor = this; // this 这里指:CoffeeFlavor
    this.prototype.parent
        = parentClassOrObject;
  }
  return this;
};
// Flyweight object 亨元。即:共享对象
const CoffeeOrder = {
  serveCoffee: function (context) { },
  getFlavor: function () { }
};

// 将以共享对象作为原型,并使得该类实例重载 Prototype 上的方法
function CoffeeFlavor(newFlavor) {
  const coffeeName = newFlavor;
  if (typeof this.getFlavor === "function") this.getFlavor = () => flavor;
  if (typeof this.serveCoffee === "function")
    this.serveCoffee = (context) => console.log(`提供咖啡 ${coffeeName} 给桌号:${context.getTable()}`);
}

// 实现
CoffeeFlavor.implementsFor(CoffeeOrder);
// ------------以上使 CoffeeFlavor implements CoffeeOrder------------

// 返回一个具有【得到桌号】函数的对象
function CoffeeTable(tableNumber) { return { getTable() { return tableNumber; } }; }

// 返回一个具有【依据咖啡名为之创建实例且记住点的总咖啡次数】【得到点的总咖啡次数】函数的对象
function CoffeeFlavorFactory() {
  let flavors = {}, length = 0;
  return {
    getCoffeeFlavor(flavorName) {
      let flavor = flavors[flavorName]; 
      if (flavor === undefined) { // 相同的咖啡,只创建一次实例
        flavor = new CoffeeFlavor(flavorName); // 依据咖啡名为之创建实例
        flavors[flavorName] = flavor;
      }
      length++; // 每次点咖啡都使得总数+1
      return flavor;
    },
    getTotalCoffeeFlavorsMade() { return length; } // 得到点的总咖啡次数
  };
}

// run,测试数据是否共享成功
void function testFlyweight() {
  let flavors = new CoffeeFlavor(),
    tables = new CoffeeTable(),
    ordersMade = 0,
    flavorFactory;
  // 创建咖啡实力并让咖啡实例和桌号一一对应
  function takeOrders(flavorIn, table) {
    // 为 flavors 添加咖啡实例(从 0 起添加咖啡实例)
    flavors[ordersMade] = flavorFactory.getCoffeeFlavor(flavorIn);
    // 使得桌号对应每个咖啡实例 (从 0 起计算桌号)
    tables[ordersMade++] = new CoffeeTable(table);
  }
  flavorFactory = new CoffeeFlavorFactory();
  takeOrders("卡布奇诺", 2);
  takeOrders("卡布奇诺", 2);
  takeOrders("冰咖啡", 1);
  for (var i = 0; i < ordersMade; ++i) {
    // 输出咖啡名和对应的桌号
    flavors[i].serveCoffee(tables[i]);
  }
  console.log("制作咖啡总数: " + flavorFactory.getTotalCoffeeFlavorsMade());
}()
  • function CoffeeFlavorFactory() {}

    {
      getCoffeeFlavor:'每次点咖啡时为之创建一个实例,且记住点的总咖啡次数',
      getTotalCoffeeFlavorsMade:'得到咖啡总共点的次数'
    }
  • takeOrders("卡布奇诺", 2);

    每次点一杯咖啡,都会创建一个咖啡实例并使得点的咖啡总数+1,

    也会创建一个桌号实例,记住这杯咖啡对应的桌号。

责任链模式

什么是责任链模式

责任链模式(Chain of Responsibility Pattern)指的是:为请求创建一个接受者对象的链。

即:让多个对象都有机会处理请求,比如:1 个请求发送后,会被 A 对象处理,但是 A 对象无法处理成功,此时就让 B 对象处理,以此类推,直到相关对象链上有对象处理成功或每个对象都无法处理就结束。

而这一个个对象就构成了一条链,我们则将它称之为:责任链,或者说职责链。

NOTE:请求在这里并非特指如 AJAX 这种,而是指一种获取数据的行为。

责任链的用处

由于发送者只需要发送请求,并不需要关注责任链上是如何对请求进行处理的,所以这就将请求的发送者和接受者(请求的处理者)进行了解耦。

这不仅仅使得多个对象都有可能处理请求,还使得请求尽可能的被处理成功。

  • 普通的程序中,发送者发送请求后,可能需要关注这个请求如何处理的,因为请求的处理者就一个,若不关注可能会导致意料之外的情况。

责任链模式的优点和缺点

优点:

  1. 降低发送者和处理者(接收者)的耦合
  2. 简化了对象,使得对象不需要知道链的结构
  3. 增强给对象指派职责的灵活性 => 链上的对象可能处理不同种类的请求
  4. 改变链内的成员或者调动它们的次序,能允许动态地新增或者删除责任
  5. 增加新的请求处理类很方便

缺点:

大部分设计模式的缺点通常也是优点所带来的,责任链模式也是如此。

  1. 系统性能将会受到一定影响,且在对代码进行调试时不太方便。
  2. 发送的请求也不一定能保证在责任链中被处理

示例

背景

背景:某公司电商网站,准备做一个活动:

若交纳 500 元定金(pay),可得100元优惠券,准备缴纳但未缴纳的不获得。

若交纳 200 元定金,可得50元优惠券;准备缴纳但未缴纳的不获得。

若不准备交纳定金,正常购买,不享受优惠券,同时在库存(stock)不充足时,不一定保证能买到商品。

而缴纳定金的,一定能买到商品。

字段描述:

  1. orderType:

    • 1:代表准备缴纳500元定金用户;
    • 2:代表准备缴纳200元定金用户;
    • 3:代表不准备缴纳定金的普通用户;
  2. pay:表示是否已缴纳定金
  3. stock:库存,支付了定金的用户不受库存限制

使用 if…else 完成

// 不愿意/没成功缴纳定金,则判断库存是否充足,足:能买;不足:不能买
const isStock = (stock) => {
  if (!stock) { console.log('库存不足'); return false; }
  console.log('普通订单,无优惠券');
  return true;
}

const order = function (orderType, pay, stock) {

  // 愿意缴纳 500 定金 
  if (orderType == 1) {
    // 已成功缴纳
    if (pay) { console.log('500元定金预购,享受100元优惠券'); return }
    isStock(stock);
    return;
  }

  // 愿意缴纳 200 定金
  if (orderType == 2) {
    // 已成功缴纳
    if (pay) { console.log('200元定金预购,享受100元优惠券'); return }
    isStock(stock);
    return;
  }

  // 不愿意缴纳定金(所以 pay 的值不会影响这个 if 的结果)
  if (orderType == 3) {
    isStock(stock);
    return;
  }
}

// 订单测试
order(1, true, 500);  // 500元定金预购,享受100元优惠券
order(1, false, 500); // 普通订单,无优惠券
order(2, true, 500);  // 200元定金预购,享受50元优惠券
order(3, false, 0);   // 库存不足

以上使用 if…else 版来完成这个背景,显然并没有使用责任链模式,只是单纯的通过 if 去判断,然后得到结果;

下面让我们来使用责任链模式重构它!

简单版责任链模式

// 职责链重构版
const order500 = function (orderType, pay, stock) {
  if (orderType == 1 && pay) { // 有意向缴纳且已缴纳 500 
    console.log('500元定金预购,享受100元优惠券');
  } else { // 否则委托下一个对象处理
    order200(orderType, pay, stock);
  }
}
const order200 = function (orderType, pay, stock) {
  if (orderType == 2 && pay) { // 有意向缴纳且已缴纳 200 
    console.log('200元定金预购,享受50元优惠券');
  } else {
    orderNormal(orderType, pay, stock);
  }
}
const orderNormal = function (_, _, stock) {
  if (stock > 0) { // 普通用户,没有意向缴纳
    console.log('普通订单,无优惠券');
  } else {
    console.log('库存不足');
  }
}

// 订单测试
order500(1, true, 500);  // 500元定金预购,享受100元优惠券
order500(1, false, 500);  // 普通订单,无优惠券
order500(2, true, 500);  // 500元定金预购,享受50元优惠券
order500(3, true, 0);    // 库存不足

以上示例,我们将 order500、order200、orderNormal ”链接“了起来,形成了一条链,而我们链接的方法则是:在一个对象无法对请求进行处理时,就调用第二个方法,将相同的参数传入,来帮助处理请求,以此类推…

而这比上面 if...else 版好在它解耦了请求的发布者和处理者,而且防止了一个过于庞大的函数诞生,但是这虽然不错,不过仍然存在者一些缺陷:

  1. 【处理】请求的代码和重新将请求【委托】给下一个对象的代码耦合了起来,不利于扩展,违反开放封闭原则
  2. 如果要在 500 和 200 之间新增一个价格,如:300 定金,就势必把原来的职责链拆解才可运行。

    如:将之添加到 order200 和 orderNormal 中间,那么,就要将 order200 的 else{} 中的 orderNormal 改成 order300;

    这很明显的违反了开放封闭原则

所以针对开放封闭原则,让我们来完善一下这个职责链模式示例。

对简单版进行完善

完善示例前,让我们定下个约定:

我们约定,在某个节点处理不了请求时,就返回一个字段,根据该是否存在该字段把请求往后传递

// 定义一条职责链(一个构造器),每个实例都是该职责链上的对象
const Chain = function (fn) {
  this.fn = fn;
  this.receiver = null;
}
// 设置当链上的某一个对象无法处理请求时,将委托给"谁"去处理
Chain.prototype.setReceiver = function (entrustNext) { this.entrustNext = entrustNext; }
// 向职责链发送请求,并让职责链上的对象处理
Chain.prototype.passRequest = function () {
  const returnMsg = this.fn.apply(this, arguments); // 将请求交予给对应实例的方法处理
  if (returnMsg == 'next') { // 若对应实例的请求无法处理,则返回 'next'
    // 调用链上下一个对象的对应的方法,让它去处理
    return this.entrustNext && this.entrustNext.passRequest.apply(this.entrustNext, arguments);
  }
  return returnMsg;
}
// 职责链上的对象对应的处理请求的方法。
const order500 = (orderType, pay) => {
  if (orderType == 1 && pay) {console.log('500元定金预购,享受100元优惠券');} 
  else {return 'next';}
}
const order200 = (orderType, pay) => {
  if (orderType == 2 && pay) {console.log('200元定金预购,享受50元优惠券');} 
  else {return 'next';}
}
const orderNormal = function (_, _, stock) {
  if (!stock) { console.log('库存不足'); return; }
  console.log('普通订单,无优惠券');
}

以上定义一个责任链,并事先规定责任链上每个对象该如何处理请求。

以下为该责任链添添加对象,并指定它们处理请求的方法,以及设置当某个对象无法处理请求时,应该委托于责任链上的哪个对象处理。

// 创建职责链上的对象,以及指定它们处理请求的方法
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrderNormal = new Chain(orderNormal);

// 设置职责链上的对象若无法处理请求时,将交予哪个对象处理。
// 如:Error 对象无法处理时,将之移交给 500 对象处理
chainOrder500.setReceiver(chainOrder200); // 当该对象无法处理时,参数就是下一个处理请求的对象
chainOrder200.setReceiver(chainOrderNormal);
// 发送请求
chainOrder500.passRequest(1, true, 500);  // 500元定金预购,享受100元优惠券
chainOrder500.passRequest(1, false, 500); // 普通订单,无优惠券
chainOrder500.passRequest(2, true, 500);  // 200元定金预购,享受50元优惠券
chainOrder500.passRequest(3, true, 500);  // 普通订单,无优惠券

很明显,本节和简单版责任链模式一节有所区别,本节示例将【请求的代码】和【重新将请求委托给下一个对象处理的代码】进行了解耦,并且正是由于此,使得该程序的扩展性大大加强,也遵守了开放封闭原则

让我们来向这个示例的职责链上扩展一个对象,用来处理当发送请求时,传递了不存在的 orderType 该怎么办吧,正常情况下,若如下向责任链发送请求,并不会得到我们想要的结果:

chainOrder500.passRequest(999, true, 1);  // 普通订单,无优惠券

我们的 orderType 并不存在 “999” 类型,但责任链上的最后一环对象 chainOrderNormal 仍然正常处理了该请求,并返回了错误的结果,很显然,这是不应该的,下面让我们向责任链添加一个 chainOrderError 对象来解决个问题:

const orderError = (orderType) => { // 添加处理方法
  if ([1, 2, 3].indexOf(orderType) === -1) { console.error('不存在的订单金额'); return }
  return 'next';
}
const chainOrderError = new Chain(orderError); // 向责任链添加对象
chainOrderError.setReceiver(chainOrder500);  // 当该对象无法处理时,委托给指定的对象处理

只需要这样做,我们就为该责任链扩展了新的处理对象,而且不需要改动有关责任链上的任何源代码,接下来只需要使用该对象作为责任链上第一个处理请求的对象即可:

chainOrderError.passRequest(1, true, 500);  // 500元定金预购,享受100元优惠券
chainOrderError.passRequest(1, false, 500); // 普通订单,无优惠券
chainOrderError.passRequest(2, true, 500);  // 200元定金预购,享受50元优惠券
chainOrderError.passRequest(3, true, 0);    // 库存不足
chainOrderError.passRequest(999, true, 0);  // 不存在的订单金额

设计原则和编程技巧

在我们看了这么多设计模式之后,让我们来进行一些总结,如何提高代码质量和健壮性吧!

总结

不管是何种模式,都不是万能药,每种模式没有高下之分,只要在合适的地方运用合适的模式/对模式进行组合使用,就能将模式的威力最大化,使得程序异常健壮。

且从本文章来看,以上的设计模式的核心目的只有一个:解耦

在使用各个设计模式时,对于新手或不能熟练使用这个模式的开发者来说,可能需要一段时间才能掌握它们,并且也还有可能会需要有一段时间来理解为什么使用它们的艰难时期;

但是足够多的注释或者对模式的研究,对你理解模式应该大有裨益,只要我们对在我们的应程序中的多大范围内使用某个设计模式有所掌控的话,我们就能让两个问题得到一定的解决。

古云:吾尝终日而思矣,不如须臾之所学也;吾尝跂而望矣,不如登高之博见也。

通常情况下, 学习永远比自己思考来得快的多,两者都不能落下。

即:学而不思则罔,思而不学则殆。

Reference


yomua
14 声望3 粉丝

一个要努力学CS的萌新:)......