模式 Patterns
目录
1. 简介 ⬆
一群无法无天的猪从无辜的小鸟那里偷走了所有的前端架构,现在小鸟们要把它们夺回来!一队特殊的小鸟英雄将攻击这些卑鄙的猪,直到夺回原本属于它们的前端 JavaScript 架构!
小鸟们最终会成功吗?它们会打败那些培根味儿的敌人吗?让我们一起揭示 JavaScript 之愤怒的小鸟系列的另一个扣人心弦的章节!
译注:翻译“bacon flavored foe”时,想起来了《少林足球》里的“做人如果没有梦想,那跟咸鱼有什么区别?”,就翻译成了“咸猪敌人”,@sunnylost 建议翻译为“培根味儿的敌人”,应该更准确和有趣些。
阅读系列介绍文章,查看所有小鸟以及它们的攻击力。
1.1 战报 ⬆
- Red Bird - IIFE
- Blue Bird - Events
- Yellow Bird - RequireJS
- Black Bird - Backbone
- White Bird - Linting
- Green Bird - Mocking
- Orange Bird - Templating
1.2 大鸟哥的攻击 ⬆
在这篇文章中,我们将看到大鸟哥祭出它的大规模杀伤性武器:有限状态机以及成熟的设计模式。渐渐的,小鸟们将一个接一个的夺回属于它们的东西!
2. 猪偷走了什么? ⬆
小鸟们了解如何编程的大部分知识,但是从未形成通用的术语,一些代表了常见场景、同时能被大家理解的术语。然后某一天大鸟哥出现了,并记录了一组常见的设计模式的名称和描述,这样它们在谈论架构时就可以达成共识(就有了共同的语言)。最终大鸟哥的模式广受欢迎,并以四人帮一书闻名于鸟界。
译注:这段文字欢乐的很。
然而在最近的一次猪群入侵中,小鸟们的四人帮书惨重失窃!现在,大鸟哥被派去找回被盗的书。它将用压到一切诡计的力量摧毁猪群,夺回属于它们的东西。
3. 鸟界的四人帮(23 种) ⬆
3.1 创建模式(5 种) ⬆
创建模式涉及到将对象实例化,这类模式都提供一个方法,将客户从所需实例化的对象中解耦。
- Abstract Factory
抽象工厂。提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。 - Builder
构造器。封装一个产品的构造过程,并允许按步骤构造。 - Factory Method
工厂方法。定义了一个创建对象的方法,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。 - Prototype
原型。当创建给定类的实例的过程很昂贵或很复杂时,就使用原型模式。 - Singleton
单例。确保一个类只有一个实例,并提供一个全局访问点。
3.2 结构模式(7 种) ⬆
结构模式可以让你把类或对象组合到更大的结构中
- Adapter
适配器。将一个类的接口,转换成客户期望的另一个接口。适配器让原来接口不兼容的类可以合作无间。 - Bridge
桥接。不只改变你的实现,也改变你的抽象。 - Composite
组合。允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。 - Decorator
装饰者。动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。 - Facade
外观。提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。 - Flyweight
蝇量。让某个类的一个实例能用来提供许多“虚拟实例”。 - Proxy
代理。为另一个对象提供一个替身或占位符以控制对这个对象的访问。
3.3 行为模式(11 种) ⬆
- Chain of Resp.
责任链。让一个以上的对象有机会处理某个请求。 - Command
命令。将“请求”封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。 - Interpreter
解释器。为语言创建解释器。 - Iterator
迭代器。提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。 - Mediator
中介者。集中相关对象之间复杂的沟通和控制方式。 - Memento
备忘录。让对象返回之前的状态。 - Observer
观察者。定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。 - State
状态。允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。 - Strategy
策略。定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 - Template Method
模板方法。在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。 - Visitor
访问者。当你想要为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式。
4. JavaScript 中的设计模式 ⬆
4.1 单例模式 ⬆
单例模式最简单的实现是采用对象字面量,如下面的代码所示。基本上,我们只是创建了一个对象,含有一些属性。从技术角度讲,有人可能会建议使用 Object.create
,但是大部分情况下,对象字面量符合单例模式的定义。你可以在本文末尾的推荐资源中找到更强大的解决方案。
译注:Object.create(proto [, propertiesObject ]) 创建一个拥有指定原型和若干个指定属性的对象。
1 2 3 4 5 6 7 8 9 |
var bird = {
type: "Red",
fly: function() {
console.log( "Weeeee!" );
},
destroy: function() {
console.log( "Hasta la vista, baby!" );
}
};
|
4.2 工厂模式 ⬆
工厂模式实际是一种不使用 new
关键字来创建新对象的方式。理念是在工厂方法中提取抽象。在下面的例子中,我们不一定要做很多事情,但是你可以想象的到,我们可以沿着这个方向添加一些自定义代码和不会改变的额外 API,这才是工厂模式的关键点所在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var Bird = function() {};
Bird.factory = function( type ) {
var bird;
if ( typeof Bird[ type ] === "function" ) {
bird = new Bird[ type ]();
}
return bird;
};
Bird.Red = function() {};
Bird.Blue = function() {};
var redBird = Bird.factory( "Red" );
var blueBird = Bird.factory( "Blue" );
|
4.3 桥接模式 ⬆
在下面的代码片段中,我们在事件处理函数和将要执行的代码(getUrl( url, callback )
)之间建立了一个简单的桥接。从而使得被执行的代码(getUrl( url, callback )
)更易于测试,因为它不再依赖于 jQuery 传入的上下文元素。
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 28 29 30 |
// Not Bridged
var getUrl = function() {
var url = $( this ).attr( "href" );
$.ajax({
url: url,
success: function( data ) {
console.log( data );
}
});
};
$( "a.ajax" ).on( "click", getUrl );
// Bridged
var getUrl = function( url, callback ) {
$.ajax({
url: url,
success: function( data ) {
if ( callback ) { callback( data ); }
}
});
};
var getUrlBridge = function() {
var url = $( this ).attr( "href" );
getUrl( url, function( data ) {
console.log( data );
});
}
$( "a.ajax" ).on( "click", getUrlBridge );
|
4.4 外观模式 ⬆
外观模式在 Web 前端开发中很普遍,因为有如此多的跨浏览器不一致问题。外观模式为这种不一致提供了一个统一的 API。在下面的代码中,我们将 addEventListener 在不同浏览器中的实现进行逻辑抽象。
1 2 3 4 5 6 7 8 |
// Facade
var addEvent = function( element, type, eventHandler ) {
if ( element.addEventListener ) {
element.addEventListener( type, eventHandler, false );
} else if ( elemement.attachEvent ) {
element.attachEvent( "on" + type, eventHandler );
}
};
|
4.5 适配器模式 ⬆
一个适配器可以轻量的处理一段代码,让其与另一段代码无间合作。当你需要切换到另一个库又无法忍受重写大量代码时,适配器会非常有用。在下面的示例中,我们将修改 jQuery 的 $.when
方法以支持 WinJS.Promise
。这是我在 appendTo 工作期间写下的代码,当时我们想让 Windows 8 APP 可以使用 jQuery。你可以在 jquery-win8 找到这个库。
jquery-win8 库的大部分功能已经不再需要了,因为 Jonathan Sampson 已经与 jQuery 开发团队一起协作,以确保他对这一垫片的更新被添加到 jQuery 2.0 版本中,这篇文章记录了这一点。
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 28 29 30 31 32 33 34 |
/*!
* jquery-win8-deferred - jQuery $.when that understands WinJS.promise
* version: 0.1
* author: appendTo, LLC
* copyright: 2012
* license: MIT (http://www.opensource.org/licenses/mit-license)
* date: Thu, 01 Nov 2012 07:38:13 GMT
*/
(function () {
var $when = $.when;
$.when = function () {
var args = Array.prototype.slice.call(arguments);
args = $.map(args, function (arg) {
if (arg instanceof WinJS.Promise) {
arg = $.Deferred(function (dfd) {
arg.then(
function complete() {
dfd.resolveWith(this, arguments);
}, function error() {
dfd.rejectWith(this, arguments);
}, function progress() {
dfd.notifyWith(this, arguments);
}
);
}).promise();
}
return arg;
});
return $when.apply(this, args);
};
}());
|
4.6 观察者模式 ⬆
我们已经在这个系列的 Blue Bird 一文中阐述了观察者模式,这是一个功能强大的模式,可以实现各种组件的解耦。个人推荐使用 postal.js 库。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var channel = postal.channel( "game" );
channel.subscribe( "bird.attack", function( data ) {
console.log( "Geronimo!" );
});
channel.subscribe( "pig.collide", function( impact ) {
if ( impact > 100 ) {
console.log( "AHHHHHHH!" );
}
});
channel.publish( "bird.attack", { angle: 45 } );
|
5. 更多模式 ⬆
5.1 继承 ⬆
在 JavaScript 中有多种方式实现继承。当你在应用程序中创建对象时,最好了解一下这些模式。
原型继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var bird = {
name: "Red Bird",
power: "",
getName: function() {
return this.name;
},
catapult: function() {
return this.name + " is catapulting with " + this.power;
}
};
var yellowBird = Object.create( bird );
yellowBird.name = "Yellow Bird";
yellowBird.power = "Speed";
console.log( yellowBird.catapult() );
|
模拟继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var Bird = function( name, power ) {
this.name = name + " Bird";
this.power = power || "";
};
Bird.prototype.getName = function() {
return this.name;
};
Bird.prototype.catapult = function() {
return this.getName() + " is catapulting with " + this.power;
};
var YellowBird = function() {
this.constructor.apply( this, arguments );
};
YellowBird.prototype = new Bird();
var yellowBird = new YellowBird( "Yellow", "Speed" );
yellowBird.getName = function() {
return "Super Awesome " + this.name;
};
console.log( yellowBird.catapult() );
|
5.2 链式语法 ⬆
因为 jQuery 库的缘故,链式语法在前端界变得非常流行。实际上这是一种非常容易实现的模式。基本上,你只需要让每个函数返回 'this',这样其他函数就可以立即被调用。看看下面的例子。
1 2 3 4 5 6 7 8 9 10 11 12 |
var bird = {
catapult: function() {
console.log( "Yippeeeeee!" );
return this;
},
destroy: function() {
console.log( "That'll teach you... you dirty pig!" );
return this;
}
};
bird.catapult().destroy();
|
5.3 封装模式 ⬆
我们在 Red Bird 一文中已经阐述了封装模式,不过当时说的 IIFE 模式。封装模式允许你拥有公共和私有的属性和方法,以此来封装你的代码。下面是一个非常简单的示例。更多细节请参阅文章 Red Bird。
1 2 3 4 5 6 7 8 9 10 11 12 |
// IIFE
var yellowBird = (function() {
var superSecret = {
power: "Speed"
};
return {
type: "Red",
mood: "Angry",
goal: "Vengence"
}
}());
|
5.4 有限状态机 ⬆
有限状态机是我最喜欢的模式之一。我的朋友 Jim Cowart(@ifandelse)创建了 Machina.js 库,用来在 JavaScript 中实现这一模式。下面的示例使用状态来描述愤怒的小鸟游戏。更多信息请参阅它的博客文章和 GitHub 库。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
var attackFsm = new machina.Fsm({
initialState: "idle",
states : {
"idle" : {
_onEnter: function() {
this.handle( "Zzzzzz" );
},
"bird.launch" : function( data ) {
console.log( "Weeeeee at " + data.angle + " degrees!" );
this.transition( "attacking" );
}
},
"attacking" : {
_onEnter: function() {
console.log( "Yay, hear me tweet!" );
},
"pig.destroyed" : function() {
this.transition( "victorious" );
},
"pig.alive" : function() {
this.transition( "defeated" );
}
},
"victorious": {
_onEnter: function() {
console.log( "Yay, we are victorious!" );
},
"game.restart": function() {
this.transition( "idle" );
},
"game.next": function() {
// Goto next level
this.transition( "idle" );
}
},
"defeated": {
_onEnter: function() {
console.log( "You may have won this time, but I'll be back!" );
},
"gmae.restart": function() {
this.transition( "idle" );
}
}
}
});
attackFsm.handle( "bird.launch", { angle: 45 } );
attackFsm.handle( "pig.destroyed" );
|
6. 建议 ⬆
除了学习这些模式之外,我建议你挑一个喜欢的库,并钻研它们的源代码。在其中你可以学到丰富的知识。起初你可能觉得云山雾罩,但过一段时间你可以从 真正理解这些模式的开发者身上收集到大量的模式。你也可以试着只着眼于一个特别的方法并剖析它。如果你不知道到底要去哪里寻找这么一个特别的方法,那么为 什么不选择 jQuery 并使用 James Padolsey(@padosley)的 jQuery 源码查看器来帮助你寻找呢?
7. 其他资源 ⬆
已经有太多的模式以至我无法在这里一一列出。在我之前,已经有许多人撰写了关于这些模式的博客,并且以后还会有。如果我错过了什么好模式,请告诉我。
- JavaScript Design Patterns by Joe Zim (@JoeZimJS)
- JavaScript Design Patterns by Joe Zim (@JoeZimJS)
- Understanding Design Patterns in JavaScript by Tilo Mitra (@tilomitra)
- Learning JavaScript Design Patterns by Addy Osmani (@addyosmani)
- JS Patterns by Shi Chuan (@shichuan)
- JavaScript Patterns by Stoyan Stefanov (@xyz)
- JavaScript: The Good Parts by Douglas Crockford
8. 攻击! ⬆
下面是一个用 boxbox 构建的简版 Angry Birds,boxbox 是一个用于 box2dweb 的框架,由 Bocoup 的 Greg Smith 编写。
按下
空格键
来发射大鸟哥,你也可以使用方向键。
9. 结论 ⬆
好消息是,你不必知道所有的答案也顺利完成 Web 前端开发,但多少了解一些开发中反复遇到的通用模式确实很有帮助。一旦你习惯了这些模式,谈论架构方案将变得更容易,也可以更快的找出解决方案。花一些时间浏览前面推荐的资源,然后仔细考虑那些适合你的模式。
还有更多的前端架构技术被猪偷走了。在下篇文章中,另一只愤怒的小鸟将继续复仇!Dun, dun, daaaaaaa!
@sunnylost 补充:Dun, dun, daaaaaaaaaa! 应该是在模拟背景音乐,类似于这种 http://missingno.ocremix.org/musicpages/game_on.html