关于设计模式:JS设计模式

40次阅读

共计 21040 个字符,预计需要花费 53 分钟才能阅读完成。

策略模式

概念

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

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

  • 能够将策略集中到一个 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,...); 

代理模式

代理模式实现图片懒加载

不必代理实现

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

  • 在一个函数中 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);

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

观察者模式

什么是观察者模式

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

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

在以下的示例中,每一个观察者都存入到了 set 汇合中,当被察看对象产生扭转时(这是一个事件),将会播送告诉 set 汇合中所有的观察者,这样观察者们就接管到了告诉。

上面让咱们应用 Proxy 和 Reflect 来写一个简略的观察者模式的示例。

示例

<!– 定义 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 对某个类进行几次实例化,所返回的失去的后果都只能是雷同的,即:第一次实例化时所失去的对象。

或者你可能没有听过单例模式在这个名词,然而我置信,你必定在日常编码中 h 应用过它,这是因为单例模式的特点是:“全局”和“惟一”,那么咱们能够联想到 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
...

命令模式的劣势和毛病

劣势

在示例中,咱们利用了命令模式,那么这样做有什么益处呢?

其实最容易让人挖掘的益处就是:这种以命令模式执行的行为,其语义和好看水平上更好

同时也升高了零碎耦合度——对象间接拜访行为变成了通过命令管制拜访哪个对象的行为。

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

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

毛病

应用命令模式可能会导致某些零碎有过多的具体命令类。

应用场景

在某些场合,比方要对行为进行 ” 记录、撤销 / 重做、事务 ” 等解决,这种无奈抵挡变动的紧耦合是不适合的。

在这种状况下,如何将 ” 行为请求者 ” 与 ” 行为实现者 ” 解耦?将一组行为形象为对象,能够实现二者之间的松耦合。

即:咱们能够利用命令模式:通过调用者调用接受者执行命令,程序:调用者→命令→接受者

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

总结

不论是何种模式,都不是万能药,每种模式没有高下之分,只有在适合的中央使用适合的模式 / 对模式进行组合应用,就能将模式的威力最大化,使得程序异样强壮。

且从本文章来看,以上的设计模式的外围目标只有一个:解耦。

Reference

  • 汪图南 - 设计模式
  • W3C- 设计模式
  • 菜鸟教程 - 设计模式

正文完
 0