更多文章参见: https://github.com/elevenbeans/elevenbeans.github.io

前言

本系列可以看作是我个人对于 Addy Osmani 的著作《Learning JavaScript Design Patterns》中内容的提炼,类似阅读笔记,目的是为了简单快速、又不失全面地了解设计模式的相关概念、特点、分类及其在 Javascript 中的实际用法。

分为五篇:

  1. 概述
  2. 创建型
  3. 结构型
  4. 行为型设计模式(本篇)
  5. MV*(待补充)

行为设计模式

1. Observer(观察者) 模式

Observer(观察者) 模式是指:一个对象(subject)维持一系列依赖于它(观察者)的对象,将状态变更自动通知给这些对象。subject 可以添加删除观察者。

要素:

  • Subject(目标):维护一系列的观察者,方便添加或者删除观察者

  • Observer(观察者):为那些在目标状态发生改变时需获得通知的对象提供更新接口

代码示例:

模拟一个 Subject 中各种有依赖关系的 Observer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function ObserverList() {
this.observerList = [];
}
ObserverList.prototype.Add = function(obj) {
return this.observerList.push(obj);
};
ObserverList.prototype.RemoveIndexAt = function(index) {
this.observerList.splice(index,1);
};
ObserverList.prototype.Count = function() {
return this.observerList.length;
};
ObserverList.prototype.Get = function(index) {
if(index > -1 && index < this.observerList.length) {
return this.observerList[index];
}
};
...

模拟 Subject 在 Observer 列表里添加、删除、通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Subject() {
this.observers = new ObserverList();
}
Subject.prototype.AddObserver = function(observer) {
this.observers.Add(observer);
};
Subject.prototype.RemoveObserver = function(observer) {
this.observers.RemoveIndexAt(this.observers.indexOf(observer));
}
Subject.prototype.Notify = function(context) {
var observerCount = this.observers.Count();
for(var i = 0; i < observerCount; i++) {
this.observers.Get(i).Update(context);
}
}

建立一个 Observer

1
2
3
4
5
6
function Observer() {
this.Update = function(ctx){
// 具体的操作
console.log(ctx);
}
}

完成观察者的添加以及事件发布流程:

1
2
3
var sub = new Subject();
sub.AddObserver(new Observer());
sub.Notify('Hello'); // output: Hello

和 Publish/Subscribe (发布/订阅) 的区别

两种模式十分类似,区别在于:Observer 模式要求观察者必须订阅内容改变的事件,而发布订阅模式则使用了一个单独的事件通道,事件通道可以传递自定义参数,包含了订阅者所需的信息。事件通道介于订阅者和发布者之间,结耦了订阅者和发布者,不再用发布者直接调用观察者的方法。例子可移步这里

JavaScript 是一个事件驱动型语言,观察者模式随处可见,例如常用的一些onClickattachEventaddEventListener 都是具体应用:DOM 的每个节点都可以作为 Subject,提供了很多事件处理(Event handle) 接口,你只需要给这些接口添加监听函数(也就是具体的 Observer),就可以捕获触发的事件进行处理。

而 jQuery 中的自定义事件则是标准的 Publish/Subscribe 系统。

2. Mediator(中介者) 模式

Mediator(中介者) 模式一般认为:一个对象应该尽可能少地了解另外的对象。如果对象之间的耦合性太高(如观察者模式中的 Subject 和 Observer),则改变一个对象,会牵动很多对象,难于维护。在复杂的系统中,这些耦合度随项目的变化呈指数增长!我们可以考虑用中介者模式来解耦,提升代码的可维护性。

所有的对象都通过中介者对象来通信,而不是相互引用,所以当一个对象发生改变时,只需要通知中介者即可。

示例代码:游戏

玩家对象是通过Player()构造函数来创建的,有自己的pointsname属性。原型上的play()方法负责给自己加一分然后通知中介者:

1
2
3
4
5
6
7
8
function Player(name) {
this.points = 0;
this.name = name;
}
Player.prototype.play = function () {
this.points += 1;
mediator.played();
};

scoreboard对象(计分板)有一个update()方法,它会在每次玩家玩完后被中介者调用。计分析根本不知道玩家的任何信息,也不保存分数,它只负责显示中介者给过来的分数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var scoreboard = {
element: document.getElementById('results'),
update: function (score) {
var i, msg = '';
for (i in score) {
if (score.hasOwnProperty(i)) {
msg += '<p><strong>' + i + '<\/strong>: ';
msg += score[i];
msg += '<\/p>';
}
}
this.element.innerHTML = msg;
}
};

现在我们来看一下 mediator 对象(中介者)。在游戏初始化的时候,在setup()方法中创建游戏者,然后放后players属性以便后续使用。played()方法会被游戏者在每轮玩完后调用,它更新score哈希然表然后将它传给scoreboard用于显示。最后一个方法是keypress(),负责处理键盘事件,决定是哪位玩家玩的,并且通知它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var mediator = {
players: {},
setup: function () {
var players = this.players;
players.home = new Player('Home');
players.guest = new Player('Guest');
},
played: function () {
var players = this.players,
score = {
Home: players.home.points,
Guest: players.guest.points
};
scoreboard.update(score);
},
keypress: function (e) {
e = e || window.event; // IE
if (e.which === 49) { // key "1"
mediator.players.home.play();
return;
}
if (e.which === 48) { // key "0"
mediator.players.guest.play();
return;
}
}
};

最后一件事是初始化和结束游戏:

1
2
3
4
5
6
7
8
9
// go!
mediator.setup();
window.onkeypress = mediator.keypress;
// game over in 30 seconds
setTimeout(function () {
window.onkeypress = null;
alert('Game over!');
}, 30000);

是否使用 Mediator(中介者) 模式,还需要看对象之间耦合程度以及对象本身变动频率而定。

3. Visitor(访问者) 模式

Visitor(访问者) 模式目标是使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者模式将有关的行为集中到一个访问者对象中,而不是分散在一个个环节节点中。当使用访问者模式的时候要将尽可能多的对象逻辑放在访问者类中,而不是子类中。

在 Javascript 这种弱类型语言里,很多方法里都不做对象的类型检测,而是只关心这些对象能做什么。Array 构造器和 String 构造器的 prototype 上的方法就被特意设计成了访问者。这些方法不对this的数据类型做任何校验。

利用访问者模式,我们可以给一个 object 对象增加push方法.

1
2
3
4
5
6
7
8
9
var Visitor = {} // 访问者对象
Visitor.push = function() { // 定义新的操作
return Array.prototype.push.apply( this, arguments );
}
var obj = {};
obj.push = Visitor.push;
obj.push('first');
console.log(obj[0]) // "first"
console.log(obj.length); // 1

注意:我们并没有改变Object对象,而是通过一个实例定义了新的方法

4. Iterator(迭代器) 模式

Iterator(迭代器) 模式是一种允许我们遍历集合的所有元素对象而无需公开基本形式的一种模式。很明显,它是一种特殊的 Facade(外观)模式

主要构成:

  • Iterator(迭代器):迭代器定义访问和遍历元素的接口
  • ConcreteIterator(具体迭代器):具体迭代器实现迭代器接口,对该聚合遍历时跟踪当前位置
  • Aggregate (聚合):聚合定义创建相应迭代器对象的接口
  • ConcreteAggregate (具体聚合):具体聚合实现创建相应迭代器的接口,返回ConcreteIterator 的一个适当的实例

在 Javascript 中,也有该模式的具体实现。

比如,在 jQuery 中的具体实现: jQuery.each()

在 ES6 中,提出了 Iterator 接口的概念。并且有一种统一的数据结构访问机制,即 for...of 循环。当使用for...of循环遍历某种只要部署了 Iterator 接口的数据结构,该循环会自动去寻找 Iterator 接口。

原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

Referrence

https://www.jianshu.com/p/ab1f737ab424
http://es6.ruanyifeng.com/#docs/iterator

三大类设计模式汇总介绍到此结束,持续关注请 Star and Watch This github repo, 谢谢 :)