设计模式的目的
提高代码的重用性、可读性、可靠性、可维护性。
六大原则
- 单一职责原则
- 理解:不同的类具备不同的职责,各司其职。做系统设计是,如果发现有一个类拥有了两种职责,那么就要问一个问题:可以将这个类分成两个类吗?如果真的有必要,那就分开,千万不要让一个类干的事情太多。
- 总结:一个类只承担一个职责,一个对象只有一种引起他变化的原因。
- 里氏替换原则
- 理解:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
- 总结:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
- 依赖倒置原则
- 理解:举例人吃苹果,我想吃苹果,但是我还想吃橘子,如果按照程序思维的话。就是三个类型,人Class,苹果Class,橘子Class,这种方式冗杂不好维护,不易理解,用水果来抽象化,苹果类继承并实现吃的动作。
- 总结:使用接口或抽象类
- 接口隔离原则
- 理解:如果一个类实现一个接口,但这个接口中有它不需要的方法,那么就需要把这个接口拆分,把它需要的方法提取出来,组成一个新的接口让这个类去实现
- 总结:一个接口对实现它的类都是有用的。接口足够小
- 迪米特原则(最少知道原则)
- 理解:一个对象应该对其他对象有最少的了解
- 总结:类中只暴露不得不暴露的,其内部实现不暴露出去。
- 开闭原则
- 理解:类、模块、函数,可以去扩展,但不要去修改。如果要修改代码,尽量用继承或组合的方式来扩展类的功能
- 总结:是扩展不是修改
(1)单例模式
定义:一个类只有一个实例,并且提供一个访问它的全局访问点。
- 最简单的单例模式无非是用一个变量来区分当前实例是否创建过,创建了返回之前的,没创建过创建。
var singleton = function (name) {
this.name=name;
this.instance=null;
}
singleton.prototype.getName=function(){
alert(this.name)
}
singleton.getInstance=function(name){
if(!this.instance){
this.instance= singleton(name);
}
return this.instance;
}
- 惰性单例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>遵循单一职责的单例模式</title>
</head>
<style>
#loginBtn {
width: 50px;
height: 50px;
background: #000;
}
</style>
<body>
<div id="loginBtn">
</div>
</body>
<script>
//负责创建登录浮窗对象
var createLoginLayer = function () {
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
//单例管理,确保这个实例创建并且只会创建一个
var getSingle = function (fn) {
var result;
return function () {
return result || (result = fn.apply(this, arguments));
}
};
//需要的时候才初始化实例,并且只初始化一个
document.getElementById('loginBtn').onclick = function () {
var loginLayer = getSingle(createLoginLayer)();
console.log(loginLayer)
loginLayer.style.display = 'block';
};
</script>
</html>
在这个例子中,遵循了单一职责原则。一个函数负责一种责任,getSingle负责单例状态的管理,createLoginLayer负责对象实例。
(2)策略模式
- 定义有一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
- 例子:计算年终奖
绩效为S 的人年终奖有4 倍工资,绩效为A 的人年终奖有3 倍工资,而绩效为B 的人年终奖是2 倍工资。
var calculateBonus = function (level, salary) {
if(level=== "S"){
return salary*4;
}
if(level === 'A'){
return salary*3;
}
if(level=== "B"){
return salary*2;
}
}
这样一个简单的计算年终奖的函数就出现了。但是这样的函数不易于扩展,假设我们现在绩效为S的年终奖变为了3.5倍,等级变多了,就需要改动代码的源码。这样就违反了开闭原则。通过策略模式改版后的计算年终奖的方法如下代码所示:
var strategies = {
"S": function (salary) {
return salary * 4;
},
"A": function (salary) {
return salary * 3;
},
"B": function (salary) {
return salary * 2;
}
};
var cacularBonus = function (level, salary) {
return strategies[level](salary)
}
var level2=cacularBonus("A",200);
我们将计算年终奖的算法,放在一个对象内部,封装起来,在调用的时候可以通过不同的等级获得不同的计算方式。而我们有新的等级或者新的计算方式的时候,我们对该对象进行更改就可以了。避免了在一个函数内部进行计算,提高了可维护性。
(3)代理模式
- 为一个对象提供一个代用品或占位符,以便控制对它的访问
- 虚拟代理
//真正的图片
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.svg');
img.src = src;
}
}
})();
proxyImage.setSrc('http://www.cdhrsip.com/static/imgs/high-tech/banner.png?version=201512141756');
- 缓存代理
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.log( proxyMult(1, 2, 3, 4)); // 输出:24
console.log(proxyMult(1, 2, 3, 4)); // 输出:24
通过增加缓存代理的方式,mult 函数可以继续专注于自身的职责——计算乘积,缓存的功能
是由代理对象实现
(4)迭代器模式
- 供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示
- 迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前
的绝大部分语言都内置了迭代器。
(5)发布-订阅模式(观察者模式)
- 它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
现实举例: - :有这个几个人,小红、小强,小明和小龙,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼MM会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。售楼处是发布者,这几个人是订阅者。
var salesOffices = {};//售楼处
salesOffices.clientList = {};//售楼处的客户列表,缓存列表,存放订阅者的回调函数
salesOffices.listen = function (key, fn) { //如果没有订阅,添加到订阅列表
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);//订阅的消息添加进消息缓存列表
}
salesOffices.trigger = function () { // 发布消息
debugger
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++];) {
console.log(arguments)
fn.apply(this, arguments); // (2) // arguments 是发布消息时附送的参数
}
};
salesOffices.listen('squareMeter88', function (price) { // 小明订阅88 平方米房子的消息
console.log('价格= ' + price); // 输出: 2000000
});
salesOffices.listen('squareMeter110', function (price) { // 小红订阅110 平方米房子的消息
console.log('价格= ' + price); // 输出: 3000000
});
salesOffices.trigger('squareMeter88', 2000000); // 发布88 平方米房子的价格
salesOffices.trigger('squareMeter110', 3000000); // 发布110 平方米房子的价格
发布-订阅模式需要有几个元素:
- 发布者(售楼处)
- 发布者的缓存列表,用户存放用户订阅消息的回调(售楼部的花名册)
- 发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)
(6)命令模式
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。
命令模式分为三步:
- 发送者: 不关心给哪个button,以及绑定什么事件,只要通过参数传入就好
var setCommand=function(btn,fn){
btn.onClick=function(){
fn();
}
}
- 执行命令者:需要接收到接受者的参数,当发送者发出命令时,执行就好
var menu={
refresh:function(){
console.log("刷新")
}
}
- 命令对象:不用关心在哪里被调用被谁调用,只需要按需执行就好了
var commondObj=function(rev){
return function(){
rev.refresh();
}
}
(7)组合模式
- 用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成
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.commandsList.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();
这是一个组合模式,closeDoorCommand,openPcCommand,openQQCommand是子对象,MacroCommand是组合对象,组合模式的特点:
- 表示对象的部分-整体层次结构
- 客户希望统一对待树中的所有对象
(8)模板方法模式
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常
在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺
序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
- coffice or tea
泡咖啡步骤:1.把水煮沸2.用沸水冲泡咖啡3.把咖啡倒进杯子4.加糖和牛奶
泡茶步骤:1.把水煮沸2.用沸水浸泡茶叶3.把茶水倒进杯子4.加柠檬
抽象对象Beverage:1.把水煮沸2.用沸水泡3.把饮料倒进杯子4.加料
var Beverage = function(){};//咖啡和茶抽象为饮料
Beverage.prototype.boilWater = function(){//煮水
console.log( '把水煮沸' );
};
Beverage.prototype.brew = function(){}; // 空方法,应该由子类重写 泡
Beverage.prototype.pourInCup = function(){}; // 空方法,应该由子类重写 倒
Beverage.prototype.addCondiments = function(){}; // 空方法,应该由子类重写 加料
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
咖啡子类和茶子类继承Beverage并实现它的方法,
var Coffee = function(){};
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function(){
console.log( '用沸水冲泡咖啡' );
};
var Tea=function(){}
Tea.prototype=new Beverage();
Tea.prototype.brew = function(){
console.log( '用沸水浸泡茶叶' );
};
在这一个继承重写的步骤中,Beverage.prototype.init这里的代码成为模板方法模式,描述清楚饮料的制作步骤。
(9)亨元模式
亨元模式是一种用于性能优化的模式,如果系统中由于创建了大量的类似的对象而导致内存占用率过高,亨元模式就有用了。
- 给50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。
var Model = function( sex, underwear){
this.sex = sex;
this.underwear= underwear;
};
Model.prototype.takePhoto = function(){
console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
for ( var i = 1; i <= 50; i++ ){
var maleModel = new Model( 'male', 'underwear' + i );
maleModel.takePhoto();
};
for ( var j = 1; j <= 50; j++ ){
var femaleModel= new Model( 'female', 'underwear' + j );
femaleModel.takePhoto();
};
这种方式去处理问题,导致在内存中创建了100个差不多的对象。如果是1W个呢?性能上就会达到饱和。
运用亨元模式处理。
剥离外部状态
var Model = function( sex){
this.sex = sex;
};
Model.prototype.takePhoto = function(){
console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
var maleModel = new Model( 'male' ),
femaleModel = new Model( 'female' );
for ( var i = 1; i <= 50; i++ ){
maleModel.underwear='underwear'+i;
maleModel.takePhoto();
};
for ( var j = 1; j <= 50; j++ ){
femaleModel.underwear='underwear'+i;
femaleModel.takePhoto();
};
运用了亨元模式之后,在内存中创建了两个对象就完成了所有的事情,故亨元模式的作用场景在:
- 一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 对象的大多数状态都可以变为外部状态。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
(10)职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间
的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
- 售卖手机的电商网站
在正式购买后,已经支付过500 元定金的用
户会收到100 元的商城优惠券,200 元定金的用户可以收到50 元的优惠券,而之前没有支付定金
的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
var order500 = function (orderType, pay, stock) {
if (orderType == 1 && pay) {
console.log('500 元定金预购, 得到100 优惠券');
} else {
order200(orderType, pay, stock);
}
}
var order200 = function (orderType, pay, stock) {
if (orderType == 2 && pay) {
console.log('200 元定金预购, 得到50 优惠券');
} else {
orderNormal(orderType, pay, stock);
}
}
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买, 无优惠券');
} else {
console.log('库存不足');
}
}
order500(1, true, 500); // 输出:500 元定金预购, 得到100 优惠券
order500(1, false, 500); // 输出:普通购买, 无优惠券
order500(2, true, 500); // 输出:200 元定金预购, 得到500 优惠券
order500(3, false, 500); // 输出:普通购买, 无优惠券
order500(3, false, 0); // 输出:手机库存不足
这种写法达到了职责链的方式方法,一次向下去查找合适的处理函数,但是加入我们加入添加一个300定金的呢?就需要去改动代码内部,违反了开闭原则,
var order500 = function (orderType, pay, stock) {
if (orderType == 1 && pay) {
console.log('500 元定金预购, 得到100 优惠券');
} else {
return 'nextSuccessor';
}
}
var order200 = function (orderType, pay, stock) {
if (orderType == 2 && pay) {
console.log('200 元定金预购, 得到50 优惠券');
} else {
return 'nextSuccessor';
}
}
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买, 无优惠券');
} else {
console.log('库存不足');
}
}
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 chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
chainOrder500.passRequest( 1, true, 500 ); // 输出:500 元定金预购,得到100 优惠券
chainOrder500.passRequest( 2, true, 500 ); // 输出:200 元定金预购,得到50 优惠券
chainOrder500.passRequest( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 ); // 输出:手机库存不足
(11)中介者模式
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应
该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象
发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中
介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。
(12)装饰者模式
给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
var plane = {
fire: function(){
console.log( '发射普通子弹' );
}
}
var missileDecorator = function(){
console.log( '发射导弹' );
}
var atomDecorator = function(){
console.log( '发射原子弹' );
}
var fire1 = plane.fire;
plane.fire = function(){
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
(13)状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
var OffLightState = function( light ){
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){
console.log( '弱光' ); // offLightState 对应的行为
this.light.setState( this.light.weakLightState ); // 切换状态到weakLightState
};
// WeakLightState:
var WeakLightState = function( light ){
this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function(){
console.log( '强光' ); // weakLightState 对应的行为
this.light.setState( this.light.strongLightState ); // 切换状态到strongLightState
};
// StrongLightState:
var StrongLightState = function( light ){
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function(){
console.log( '关灯' ); // strongLightState 对应的行为
this.light.setState( this.light.offLightState ); // 切换状态到offLightState
};
var Light = function(){
this.offLightState = new OffLightState( this );
this.weakLightState = new WeakLightState( this );
this.strongLightState = new StrongLightState( this );
this.button = null;
};
Light.prototype.init = function(){
var button = document.createElement( 'button' ),
self = this;
this.button = document.body.appendChild( button );
this.button.innerHTML = '开关';
this.currState = this.offLightState; // 设置当前状态
this.button.onclick = function(){
self.currState.buttonWasPressed();
}
};
Light.prototype.setState = function( newState ){
this.currState = newState;
};
总结
拜读《javascript设计模式和开发实践》
设计模式就是对一系列业务或程序运行的封装,设计模式可以写出可维护性高, 运行效率高的代码,但每一种业务场景有不同的开发逻辑,不同的逻辑选用最适合的代码去编写,最终得到的才会是最好的,没有最优的设计模式,只有更好的设计模式,一个好的代码是经过多次的代码重构才能得到,才能算作对设计模式的完整遵循。了解前任给我们总结得出的设计模式,我们可以站在巨人的肩膀上,在自己写代码的时候,重构的时候,考虑代码的实现。是否合理?是否具备可扩展性?强壮性?等。