设计模式

在程序设计中有很多实用的设计模式,而其中大部分语言的实现都是基于“类”。

原型模式

原型模式不关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一摸一样的对象。

var Plane = function() {
    this.blood = 100;
    this.attackLevel = -1;
    this.defenseLevel = -1;
}
var plane = new Plane()
plane.blood = 500;
plane.attackLevel = 4;
plane.defenseLevel = 5;
// 通过克隆创建一个一摸一样的对象
var clonePlane = Object.create(plane)
console.log(clonePlane.blood)  // 500
console.log(clonePlane.attackLevel)  // 4
console.log(clonePlane.defenseLevel)  // 5

原型模式的规则:

  • 所有的数据都是对象。
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
  • 对象会记住它的原型。
  • 如果对象无法响应某个请求,他会吧这个请求委托给它自己的原型。

在JavaScript中并没有类这种概念,JS中的函数属于一等对象,在JS中定义一个对象非常简单(var obj = {}),而基于JS中闭包与弱类型等特性,在实现一些设计模式的方式上与众不同。

单例模式

单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

下面是一个普通单例的例子:

var Person = function (name) {
    this.name = name
    this.instance = null
}
Person.prototype.getName = function () {
    return this.name
}
Person.getInstance = function (name) {
    if (!this.instance) {
        this.instance = new Person(name)
    }
    return this.instance
}
var a = Person.getInstance('sven')
var b = Person.getInstance('sven2')
console.log(a === b)  // true

上面单例每次都要靠Person.getInstance()来实现,使用的时候不怎么友好,因为我们创建实例的时候习惯直接使用new运算符,所以接下来出现一个改进版:

var Person= (function () {
  var instance;
  var Person= function (name) {
    if (instance) {return instance}
    this.name = name
    return instance = this
  }
  Person.prototype.getName = function(){
    return this.name
  }
  return Person
})()
var a = new Person('yy')
var b = new Person('zz')
console.log(a === b)  // true

上面这个例子还有一点问题,就是这个函数只能是单例了,如果我以后需要这个函数实现多例,那就要将Person中控制创建唯一对象的相关代码删除,这种修改会存在很多的隐患。
为了避免上述的隐患,我们继续来改进这个例子:

var Person = function(name) {
  this.name = name
}
Person.prototype.getName = function () {return this.name}
// 使用一个代理来完成唯一对象的创建
var proxySingletonPerson = (function () {
  var instance;
  return function (name) {
    if (!instance) {
      instance = new Person(name)
    }
    return instance
  }
})()
var a = new proxySingletonPerson('yy')
var b = new proxySingletonPerson('zz')
console.log(a === b)  // true

观察代理函数会发现主要用于变成单例的部分是这个模式

var obj
if (obj) {
  obj = xxxx
}

所以我们可以再抽象出一层:

  var Person = function(name) {
    this.name = name
  }
  Person.prototype.getName = function() {
    return this.name
  }
  var getSingle = function(fn) {
    var result;
    return function() {
      return result || (result = new fn(arguments))
    }
  }
  var singletonPerson = getSingle(Person)
  var a = new singletonPerson('a')
  var b = new singletonPerson('bbb')
  console.log(a === b)  // true

本文基于《JavaScript设计模式与开发实践》一书,用一些例子总结一下JS常见的设计模式与实现方法。文章略长,自备瓜子板凳~

策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

下面是一个策略模式的例子:
完成一段路程,可以走路、自行车或者汽车,不同的方式有不同的速度,要得到不同方式对应的完成时间。

    var strategies = {
      'walk': function (distance) {
        return distance / 5
      },
      'bike': function (distance) {
        return distance / 15
      },
      'car': function (distance) {
        return distance / 30
      }
    }
    var calculateTime = function (way, distance) {
      return strategies[way](distance)
    }
    console.log(calculateTime('walk', 1000))  // 200
    console.log(calculateTime('bike', 1000))  // 66.67
    console.log(calculateTime('car', 1000))  // 33.33

不同的出行方式对应不同的算法,并且当需要添加的新的出行方式的时候不用修改calculateTime,就不会对之前的内容造成影响了。

 

代理模式

代理模式是为对象提供一个代用品或占位符,以便控制对它的访问。

代理模式有多种形式,保护代理、虚拟代理、缓存代理、防火墙代理、远程代理等等。在JavaScript中,一般比较常用的是虚拟代理和缓存代理。
虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。
下面看一个虚拟代理的例子:

  var myImage = (function() {
    var imgNode = document.createElement('img')
    document.body.appendChild(imgNode)
    return {
      setSrc: function(src) {
        imgNode.src = src
      }
    }
  })()
  var proxyImage = (function() {
    var img = new Image
    img.onload = function() {
      myImage.setSrc(this.src)
    }
    return {
      setSrc: function(src) {
        myImage.setSrc('loading.gif')
        img.src = src
      }
    }
  })()
  proxyImage.setSrc('pucture.png')

上面这个例子中,使用的是图片预加载技术,先在proxyImage中加载需要的图片,然后让myImage显示loading图片,等需要的图片完全加载完毕后,再替换掉loading图片。
那为什么要将两个功能分开,它们应该可以合并为一个函数才对。这是因为单一职责原则(就一个类而言,应该仅有一个引起它变化的原因),面向对象设计中,一个对象承担的职责越多,那引起它变化的原因就会变得越多,当变化发生时,设计可能会遭到意外的破坏。
两个函数的使用都是setSrc接口,所以这样当不需要预加载的时候,可以直接请求对象本体,理解与修改也非常方便。
虚拟代理还有合并http请求、让文件实现惰性加载等能力。(惰性加载实例见JavaScript设计模式与开发实践6.7节)
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

  var mult = function() {
    console.log('开始计算乘积')
    var a = 1
    for (var i = 0, l = arguments.length; i < l; i++) {
      a = a * arguments[i]
    }
    return a
  }
  var proxyMult = (function() {
    var cache = {}
    return function() {
      var args = Array.prototype.join.call(arguments, ',')
      if (args in cache) {
        return cache[args]
      }
      return cache[args] = mult.apply(this, arguments)
    }
  })()
  console.time('a')
  proxyMult(1, 2, 3, 4, 5)
  console.timeEnd('a')  // 3ms以上
  console.time('b')
  proxyMult(1, 2, 3, 4, 5)
  console.timeEnd('b')  // 1ms以下

上例中,计算结果被缓存,所以相同参数传入后不需要再进行计算直接读取就可以了,所以比较节约时间。

设计原则

单一职责原则(SRP)

一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

应该把对象或方法划分成较小的粒度

最少知识原则(LKP)

一个软件实体应当 尽可能少地与其他实体发生相互作用 

应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的 相互联系,可以转交给第三方进行处理

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定

 

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

按照上面的定义,数组的forEach、reduce、reduceRight、filter这些应该都属于迭代函数,首先我们实现一个forEach函数

  var each = function(arr, fn) {
    var l = arr.length,
      i = 0;
    for (; i < l; i++) {
      fn(arr[i], i, arr)
    }
  }
  each([1,2,3,4,5], function (e) {
    console.log(e)
  })

each属于内部迭代器,因为each函数的内部已经定义好了迭代规则,就是对一个数组内容进行遍历并分别调用函数fn。如果需要对两个数组进行处理,那么就需要对迭代规则进行修改。
下面来看一个外部迭代器:

    /* 迭代器主体 */
    var Iterator = function (obj) {
      var current = 0
      var next = function () {
        current += 1
      }
      var getCurrItem = function () {
        return obj[current]
      }
      var isDone = function () {
        return current >= obj.length
      }
      return {
        next: next,
        getCurrItem: getCurrItem,
        isDone: isDone
      }
    }
    var each = function (iterator) {
      while(!iterator.isDone()) {
        /* 这里是循环遍历的主题,也就是一般each函数中自定义的fn */
        console.log(iterator.getCurrItem())
        iterator.next()
      }
    }
    var iterator1 = Iterator([1,2,3,4,5])
    each(iterator1)

什么是设计模式

作者的这个说明解释得挺好

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模式

学习设计模式,有助于写出可复用和可维护性高的程序

设计模式的原则是“找出 程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。

不过要注意,使用不当的话,可能会事倍功半。

 

一、单例模式

二、策略模式

三、代理模式

四、迭代器模式

五、发布—订阅模式

六、命令模式

七、组合模式

八、模板方法模式

九、享元模式

十、职责链模式

十一、中介者模式

十二、装饰者模式

十三、状态模式

十四、适配器模式

十五、外观模式

 

发布-订阅模式(观察者模式)

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript中,我们一般用事件模型来替代传统的发布-订阅模式。

发布-订阅模式需要哪些东西:

  • 订阅者列表
  • 订阅的方法
  • 发布的方法
var event = {
  // 消息列表
  clientList: [],
  // 订阅消息
  subscribe: function(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = []
    }
    this.clientList[key].push(fn)
  },
  // 发布消息
  publish: function() {
    var key = Array.prototype.shift.call(arguments),
      fns = this.clientList[key]
    if (!fns || fns.length === 0) {
      return false
    }
    for (var i = 0, fn; fn = fns[i++]) {
      fn.apply(this, arguments)
    }
  }
}
// 让对象拥有这些方法
function installEvent(obj) {
  for ( var i in event) {
    obj[i] = event[i]
  }
}

var demo = {}
installEvent(demo)
demo.subscribe('milk', function (num) {
  console.log('牛奶新到:' + num)
})
demo.subscribe('milk2', function (num) {
  console.log('牛奶2新到:' + num)
})
demo.publish('milk', 100)
demo.publish('milk2', 120)

上面是发布-订阅模式简单的示例,订阅者设置需要的消息和回调函数,发布者在发布消息的时候触发相应消息中的函数并将参数传入。

一、单例模式

1. 定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点

2. 核心

确保只有一个实例,并提供全局访问

3. 实现

假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例

function SetManager(name) {
    this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
};

var SingletonSetManager = (function() {
    var manager = null;

    return function(name) {
        if (!manager) {
            manager = new SetManager(name);
        }

        return manager;
    } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a

这是比较简单的做法,但是假如我们还要设置一个HR呢?就得复制一遍代码了

所以,可以改写单例内部,实现地更通用一些

// 提取出通用的单例
function getSingleton(fn) {
    var instance = null;

    return function() {
        if (!instance) {
            instance = fn.apply(this, arguments);
        }

        return instance;
    }
}

再进行调用,结果还是一样

// 获取单例
var managerSingleton = getSingleton(function(name) {
    var manager = new SetManager(name);
    return manager;
});

managerSingleton('a').getName(); // a
managerSingleton('b').getName(); // a
managerSingleton('c').getName(); // a

这时,我们添加HR时,就不需要更改获取单例内部的实现了,仅需要实现添加HR所需要做的,再调用即可

function SetHr(name) {
    this.hr = name;
}

SetHr.prototype.getName = function() {
    console.log(this.hr);
};

var hrSingleton = getSingleton(function(name) {
    var hr = new SetHr(name);
    return hr;
});

hrSingleton('aa').getName(); // aa
hrSingleton('bb').getName(); // aa
hrSingleton('cc').getName(); // aa

或者,仅想要创建一个div层,不需要将对象实例化,直接调用函数

结果为页面中仅有第一个创建的div

function createPopup(html) {
    var div = document.createElement('div');
    div.innerHTML = html;
    document.body.append(div);

    return div;
}

var popupSingleton = getSingleton(function() {
    var div = createPopup.apply(this, arguments);
    return div;
});

console.log(
    popupSingleton('aaa').innerHTML,
    popupSingleton('bbb').innerHTML,
    popupSingleton('bbb').innerHTML
); // aaa  aaa  aaa

 

命令模式

命令模式是最简单和优雅的模式之一,命令模式种的命令指的是一个执行某些特定事情的指令。
命令模式最常见的场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁也不知道被请求的操作是上面。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够清除彼此之间的耦合关系。

  <button id="execute">点击</button>
  <button id="undo">点击</button>
  <script>
  var Tv = {
    open: function() { console.log('打开电视机') },
    close: function() { console.log('关闭电视机') }
  }
  // 将命令封装成相应的对象
  var OpenTvCommand = function(receiver) { this.receiver = receiver; }
  OpenTvCommand.prototype.execute = function() { this.receiver.open() }
  OpenTvCommand.prototype.undo = function() { this.receiver.close() }

  // 设置命令的请求者需要执行的命令
  var setCommand = function(command) {
    execute.onclick = function() { command.execute() }
    undo.onclick = function() { command.undo() }
  }
  // 将命令的请求者和接收者结合在一起
  setCommand(new OpenTvCommand(Tv))
  </script>
    // 将命令封装成相应的对象
    var OpenTvCommand = function (receiver) {this.receiver = receiver;}
    OpenTvCommand.prototype.execute = function () {this.receiver.open()}
    OpenTvCommand.prototype.undo = function () {this.receiver.close()}

    // 设置命令的请求者需要执行的命令
    var setCommand = function (command) {
      execute.onclick = function () {command.execute()}
      undo.onclick = function () {command.undo()}
    }
    // 将命令的请求者和接收者结合在一起
    setCommand( new OpenTvCommand(Tv))
  </script>

命令模式有时候和策略模式很像,区别是策略模式中所有策略的目标都是相同的,只是内部的“算法”有区别,但命令模式中的命令并不是针对一个目标,命令模式能完成更多的功能。

二、策略模式

1. 定义

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

2. 核心

将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成:

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context 中要维持对某个策略对象的引用

3. 实现

策略模式可以用于组合一系列算法,也可用于组合一系列业务规则

假设需要通过成绩等级来计算学生的最终得分,每个成绩等级有对应的加权值。我们可以利用对象字面量的形式直接定义这个组策略

// 加权映射关系
var levelMap = {
    S: 10,
    A: 8,
    B: 6,
    C: 4
};

// 组策略
var scoreLevel = {
    basicScore: 80,

    S: function() {
        return this.basicScore + levelMap['S']; 
    },

    A: function() {
        return this.basicScore + levelMap['A']; 
    },

    B: function() {
        return this.basicScore + levelMap['B']; 
    },

    C: function() {
        return this.basicScore + levelMap['C']; 
    }
}

// 调用
function getScore(level) {
    return scoreLevel[level] ? scoreLevel[level]() : 0;
}

console.log(
    getScore('S'),
    getScore('A'),
    getScore('B'),
    getScore('C'),
    getScore('D')
); // 90 88 86 84 0

在组合业务规则方面,比较经典的是表单的验证方法。这里列出比较关键的部分

// 错误提示
var errorMsgs = {
    default: '输入数据格式不正确',
    minLength: '输入数据长度不足',
    isNumber: '请输入数字',
    required: '内容不为空'
};

// 规则集
var rules = {
    minLength: function(value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg || errorMsgs['minLength']
        }
    },
    isNumber: function(value, errorMsg) {
        if (!/d+/.test(value)) {
            return errorMsg || errorMsgs['isNumber'];
        }
    },
    required: function(value, errorMsg) {
        if (value === '') {
            return errorMsg || errorMsgs['required'];
        }
    }
};

// 校验器
function Validator() {
    this.items = [];
};

Validator.prototype = {
    constructor: Validator,

    // 添加校验规则
    add: function(value, rule, errorMsg) {
        var arg = [value];

        if (rule.indexOf('minLength') !== -1) {
            var temp = rule.split(':');
            arg.push(temp[1]);
            rule = temp[0];
        }

        arg.push(errorMsg);

        this.items.push(function() {
            // 进行校验
            return rules[rule].apply(this, arg);
        });
    },

    // 开始校验
    start: function() {
        for (var i = 0; i < this.items.length; ++i) {
            var ret = this.items[i]();

            if (ret) {
                console.log(ret);
                // return ret;
            }
        }
    }
};

// 测试数据
function testTel(val) {
    return val;
}

var validate = new Validator();

validate.add(testTel('ccc'), 'isNumber', '只能为数字'); // 只能为数字
validate.add(testTel(''), 'required'); // 内容不为空
validate.add(testTel('123'), 'minLength:5', '最少5位'); // 最少5位
validate.add(testTel('12345'), 'minLength:5', '最少5位');

var ret = validate.start();

console.log(ret);

4. 优缺点

优点

可以有效地避免多重条件语句,将一系列方法封装起来也更直观,利于维护

缺点

往往策略集会比较多,我们需要事先就了解定义好所有的情况

 

组合模式

组合模式可以让我们使用树形方式创建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。

  // 普通命令
  var closeDoorCommand = {
    execute: function() {
      console.log('关门')
    }
  }
  var openPcCommand = {
    execute: function() {
      console.log('开电脑')
    }
  }
  var openQQCommand = {
    execute: function() {
      console.log('登录QQ')
    }
  }

  // 组合命令函数
  var MacroCommand = function() {
    return {
      commandsList: [],
      add: function(command) {
        this.commandList.push(command)
      },
      execute: function() {
        for (var i = 0, command; command = this.commandsList[i++];) {
          command.execute()
        }
      }
    }
  }

  // 组合命令
  var macroCommand = MacroCommand()

  macroCommand.add(closeDoorCommand)
  macroCommand.add(openPcCommand)
  macroCommand.add(openQQCommand)

  macroCommand.execute()

观察上例可以发现:我们通过一个函数,将多个命令组合成了对象,并且单条命令的执行方式和组合命令的执行方式相同;组合命令的结构类似于树形结构,而且执行的顺序可以看做是对树深度优先的搜索

组合模式的使用场景一般有两种:

  1. 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。
  2. 客户希望统一对待树中的所有对象。

三、代理模式

1. 定义

为一个对象提供一个代用品或占位符,以便控制对它的访问

2. 核心

当客户不方便直接访问一个 对象或者不满足需要的时候,提供一个替身对象 来控制对这个对象的访问,客户实际上访问的是 替身对象。

替身对象对请求做出一些处理之后, 再把请求转交给本体对象

代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一 些额外的事情

3. 实现

代理模式主要有三种:保护代理、虚拟代理、缓存代理

保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子

// 主体,发送消息
function sendMsg(msg) {
    console.log(msg);
}

// 代理,对消息进行过滤
function proxySendMsg(msg) {
    // 无消息则直接返回
    if (typeof msg === 'undefined') {
        console.log('deny');
        return;
    }

    // 有消息则进行过滤
    msg = ('' + msg).replace(/泥s*煤/g, '');

    sendMsg(msg);
}


sendMsg('泥煤呀泥 煤呀'); // 泥煤呀泥 煤呀
proxySendMsg('泥煤呀泥 煤'); // 呀
proxySendMsg(); // deny

它的意图很明显,在访问主体之前进行控制,没有消息的时候直接在代理中返回了,拒绝访问主体,这数据保护代理的形式

有消息的时候对敏感字符进行了处理,这属于虚拟代理的模式

 

虚拟代理在控制对主体的访问时,加入了一些额外的操作

在滚动事件触发的时候,也许不需要频繁触发,我们可以引入函数节流,这是一种虚拟代理的实现

// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
function debounce(fn, delay) {
    delay = delay || 200;

    var timer = null;

    return function() {
        var arg = arguments;

        // 每次操作时,清除上次的定时器
        clearTimeout(timer);
        timer = null;

        // 定义新的定时器,一段时间后进行操作
        timer = setTimeout(function() {
            fn.apply(this, arg);
        }, delay);
    }
};

var count = 0;

// 主体
function scrollHandle(e) {
    console.log(e.type, ++count); // scroll
}

// 代理
var proxyScrollHandle = (function() {
    return debounce(scrollHandle, 500);
})();

window.onscroll = proxyScrollHandle;

 

缓存代理可以为一些开销大的运算结果提供暂时的缓存,提升效率

来个栗子,缓存加法操作

// 主体
function add() {
    var arg = [].slice.call(arguments);

    return arg.reduce(function(a, b) {
        return a + b;
    });
}

// 代理
var proxyAdd = (function() {
    var cache = [];

    return function() {
        var arg = [].slice.call(arguments).join(',');

        // 如果有,则直接从缓存返回
        if (cache[arg]) {
            return cache[arg];
        } else {
            var ret = add.apply(this, arguments);
            return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100

 

模板方法模式

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

抽象类:在Java中,类分为具体类和抽象类两种。具体类可以被实例化,抽象类不能被实例化。因为抽象类不能被实例化,所以抽象类一定是用来被某些具体类继承的。
标准的模板方法实现见JavaScript设计模式与开发实践11.2节。
适合JS的实现:

  var Beverage = function(param) {
    var boilWater = function() {
      console.log('把水煮沸')
    }
    var brew = param.brew || function() {
      throw new Error('必须传递brew方法')
    }
    var pourInCup = param.pourInCup || function() {
      throw new Error('必须传递pourInCup方法')
    }
    var addCondiments = param.addCondiments || function() {
      throw new Error('必须传递addCondiments方法')
    }

    var F = function() {}
    F.prototype.init = function() {
      boilWater()
      brew()
      pourInCup()
      addCondiments()
    }
    return F
  }

  var Coffee = Beverage({
    brew: function() {
      console.log('用沸水冲泡咖啡')
    },
    pourInCup: function() {
      console.log('把咖啡倒进被子')
    },
    addCondiments: function() {
      console.log('加糖和牛奶')
    }
  })

  var Tea = Beverage({
    brew: function() {
      console.log('用沸水浸泡茶叶')
    },
    pourInCup: function() {
      console.log('把茶倒进被子')
    },
    addCondiments: function() {
      console.log('加柠檬')
    }
  })

  var coffee = new Coffee()
  coffee.init()

  var tea = new Tea()
  tea.init()

在上例中,Beverage 是抽象类,Coffee和Tea是具体类,Beverage中的F.init()封装了子类的算法框架,指导子类以何种顺序执行哪些方法,所以F.init()是模板方法。Beverage 将Coffee和Tea中都要执行的烧水过程确定,并定义好了Coffee和Tea中具体方法的执行顺序,也就是将两个类中的通用部分进行封装,提高了函数的扩展性。

四、迭代器模式

1. 定义

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

2. 核心

在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

3. 实现

JS中数组的map forEach 已经内置了迭代器

[1, 2, 3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
});

不过对于对象的遍历,往往不能与数组一样使用同一的遍历代码

我们可以封装一下

function each(obj, cb) {
    var value;

    if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    } else {
        for (var i in obj) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    }
}

each([1, 2, 3], function(index, value) {
    console.log(index, value);
});

each({a: 1, b: 2}, function(index, value) {
    console.log(index, value);
});

// 0 1
// 1 2
// 2 3

// a 1
// b 2

再来看一个例子,强行地使用迭代器,来了解一下迭代器也可以替换频繁的条件语句

虽然例子不太好,但在其他负责的分支判断情况下,也是值得考虑的

function getManager() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log('A');
    } else if (year >= 2100) {
        console.log('C');
    } else {
        console.log('B');
    }
}

getManager(); // B

将每个条件语句拆分出逻辑函数,放入迭代器中迭代

function year2000() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log('A');
    }

    return false;
}

function year2100() {
    var year = new Date().getFullYear();

    if (year >= 2100) {
        console.log('C');
    }

    return false;
}

function year() {
    var year = new Date().getFullYear();

    if (year > 2000 && year < 2100) {
        console.log('B');
    }

    return false;
}

function iteratorYear() {
    for (var i = 0; i < arguments.length; ++i) {
        var ret = arguments[i]();

        if (ret !== false) {
            return ret;
        }
    }
}

var manager = iteratorYear(year2000, year2100, year); // B

 

享元模式

享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态,而外部状态取决于具体的场景,并根据场景而变化。

内部状态和外部状态:

  • 内部状态存储与对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
  var Upload = function(uploadType) {
    this.uploadType = uploadType
  }

  Upload.prototype.delFile = function(id) {
    uploadManager.setExternalState(id, this)

    if (this.fileSize < 3000) {
      return this.dom.parentNode.removeChild(this.dom)
    }
    if (window.confirm('确定要删除该文件吗?' + this.fileName)) {
      return this.dom.parentNode.removeChild(this.dom)
    }
  }
  // 工厂模式进行对象实例化,根据内部对象的数量来创建出新对象
  var UploadFactory = (function() {
    // 保存对象
    var createdFlyWeightObjs = {}
    return {
      create: function(uploadType) {
        if (createdFlyWeightObjs[uploadType]) {
          return createdFlyWeightObjs[uploadType]
        }
        return createdFlyWeightObjs[uploadType] = new Upload(uploadType)
      }
    }
  })()
  // 管理器封装外部状态
  var uploadManager = (function() {
    // 用于保存外部状态
    var uploadDatabase = {}
    return {
      add: function(id, uploadType, fileName, fileSize) {
        var flyWeightObj = UploadFactory.create(uploadType)

        var dom = document.createElement('div')
        dom.innerHTML = '文件名称:' + fileName + '文件大小:' + fileSize + '' + '<button class="delFile">删除</button>'
        dom.querySelector('.delFile').onclick = function() {
          flyWeightObj.delFile(id)
        }
        document.body.appendChild(dom)
        uploadDatabase[id] = {
          fileName: fileName,
          fileSize: fileSize,
          dom: dom
        }
        return flyWeightObj
      },
      // 通过id来返回对应的外部状态
      setExternalState: function(id, flyWeightObj) {
        var uploadData = uploadDatabase[id]
        for (var i in uploadData) {
          flyWeightObj[i] = uploadData[i]
        }
      }
    }
  })()

  var id = 0

  window.startUpload = function(uploadType, files) {
    for (var i = 0, file; file = files[i++];) {
      var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
    }
  }

  startUpload('plugin', [{
      fileName: '1.txt',
      fileSize: 1000
    },
    {
      fileName: '2.txt',
      fileSize: 3000
    },
    {
      fileName: '3.txt',
      fileSize: 5000
    },
  ])
  startUpload('flash', [{
      fileName: '4.txt',
      fileSize: 1000
    },
    {
      fileName: '5.txt',
      fileSize: 3000
    },
    {
      fileName: '6.txt',
      fileSize: 5000
    },
  ])

五、发布-订阅模式

1. 定义

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知

2. 核心

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅

3. 实现

JS中的事件就是经典的发布-订阅模式的实现

// 订阅
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

document.body.addEventListener('click', function() {
    console.log('click2');
}, false);

// 发布
document.body.click(); // click1  click2

自己实现一下

小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。

一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB

这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布

// 观察者
var observer = {
    // 订阅集合
    subscribes: [],

    // 订阅
    subscribe: function(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = [];
        }

        // 收集订阅者的处理
        typeof fn === 'function' && this.subscribes[type].push(fn);
    },

    // 发布  可能会携带一些信息发布出去
    publish: function() {
        var type = [].shift.call(arguments),
            fns = this.subscribes[type];

        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }

        // 挨个处理调用
        for (var i = 0; i < fns.length; ++i) {
            fns[i].apply(this, arguments);
        }
    },

    // 删除订阅
    remove: function(type, fn) {
        // 删除全部
        if (typeof type === 'undefined') {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }

        if (typeof fn === 'undefined') {
            fns.length = 0;
            return;
        }

        // 挨个处理删除
        for (var i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
};

// 订阅岗位列表
function jobListForA(jobs) {
    console.log('A', jobs);
}

function jobListForB(jobs) {
    console.log('B', jobs);
}

// A订阅了笔试成绩
observer.subscribe('job', jobListForA);
// B订阅了笔试成绩
observer.subscribe('job', jobListForB);


// A订阅了笔试成绩
observer.subscribe('examinationA', function(score) {
    console.log(score);
});

// B订阅了笔试成绩
observer.subscribe('examinationB', function(score) {
    console.log(score);
});

// A订阅了面试结果
observer.subscribe('interviewA', function(result) {
    console.log(result);
});

observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '备用'); // 备用

observer.publish('job', ['前端', '后端', '测试']); // 输出A和B的岗位


// B取消订阅了笔试成绩
observer.remove('examinationB');
// A都取消订阅了岗位
observer.remove('job', jobListForA);

observer.publish('examinationB', 80); // 没有可匹配的订阅,无输出
observer.publish('job', ['前端', '后端', '测试']); // 输出B的岗位

4. 优缺点

优点

一为时间上的解耦,二为对象之间的解耦。可以用在异步编程中与MV*框架中

缺点

创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销

弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解

 

职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象成为链中的节点。

举例:我口袋有一些钱,想买饮料,当在10块以下的时候,买矿泉水;当20块以下的时候,买奶茶;当30块以下的时候,买咖啡。

  var drink = function(money) {
    if (money < 10) {
      console.log('矿泉水')
    } else if (money < 20) {
      console.log('奶茶')
    } else {
      console.log('咖啡')
    }
  }

通过上面这个函数,我们能得到正确的结果,但这段代码并不值得称赞,当条件变化的时候,我们需要不断修改这个函数,会导致越来越难维护。所以接下来我们使用职责链模式来处理这件事情:

  /* 将不同条件分开 */
  var drinkWater = function(money) {
    if (money > 0 && money <= 10) {
      console.log('矿泉水')
    } else {
      return 'nextSuccessor'
    }
  }
  var drinkMilkTea = function(money) {
    if (money > 10 && money <= 20) {
      console.log('奶茶')
    } else {
      return 'nextSuccessor'
    }
  }
  var drinkCoffee = function(money) {
    if (money > 20 && money <= 50) {
      console.log('咖啡')
    } else {
      return 'nextSuccessor'
    }
  }

  /* 定义职责链传递的方式 */
  var Chain = function(fn) {
    this.fn = fn
    this.successor = null
  }
  Chain.prototype.setNextSuccessor = function(successor) {
    return this.successor = successor
  }
  Chain.prototype.passRequest = function() {
    var ret = this.fn.apply(this, arguments)
    if (ret === 'nextSuccessor') {
      return this.successor && this.successor.passRequest.apply(this.successor, arguments)
    }
    return ret
  }

  /* 开始使用 */
  var chainWater = new Chain(drinkWater)
  var chainMilkTea = new Chain(drinkMilkTea)
  var chainCoffee = new Chain(drinkCoffee)

  chainWater.setNextSuccessor(chainMilkTea)
  chainMilkTea.setNextSuccessor(chainCoffee)

  chainWater.passRequest(4) // 矿泉水
  chainWater.passRequest(14) // 奶茶
  chainWater.passRequest(24) // 咖啡

可以看出函数干净了很多,现在要修改饮料种类的话就不用管原来函数的内容是什么,直接增加一个函数然后添加到职责链里面就好。
接下来再看一个比较方便的实现方式:

  Function.prototype.after = function(fn) {
    var self = this
    return function() {
      var ret = self.apply(this, arguments)
      if (ret === 'nextSuccessor') {
        return fn.apply(this, arguments)
      }
      return ret
    }
  }
  var drinkWater = function(money) {
    if (money > 0 && money <= 10) {
      console.log('矿泉水')
    } else {
      return 'nextSuccessor'
    }
  }
  var drinkMilkTea = function(money) {
    if (money > 10 && money <= 20) {
      console.log('奶茶')
    } else {
      return 'nextSuccessor'
    }
  }
  var drinkCoffee = function(money) {
    if (money > 20 && money <= 50) {
      console.log('咖啡')
    } else {
      return 'nextSuccessor'
    }
  }
  var drink = drinkWater.after(drinkMilkTea).after(drinkCoffee)
  drink(20) // 奶茶

after的实现使用了递归的思想,类似于二叉树搜索的深度优先,将需要执行的函数按顺序排列好,再开始逐个执行调用。

本文由金沙官网线上发布于Web前端,转载请注明出处:设计模式

您可能还会对下面的文章感兴趣: