设计模式的基本原则
单一职责原则 (Single Responsibility Principle)
就一个类而言,应该仅有一个引起它变化的原因;一个对象(方法)只做一件事情;
SRP 原则是所有原则中最简单也是最难正确运用的原则之一。
要明确的是,并不是所有的职责都应该一一分离。
一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在 ajax 请求的时候,创建 xhr 对象和发送 xhr 请求几乎总是在一起的,那么创建 xhr 对象的职责和发送 xhr 请求的职责就没有必要分开。
另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦 合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的 时候再进行分离也不迟。
SRP 原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度, 这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他 的职责。
但 SRP 原则也有一些缺点,最明显的是会增加编写代码的复杂度。当我们按照职责把对象 分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。
开放-封闭原则(OCP)
对扩展开放,对修改关闭
在面向对象的程序设计中,开放-封闭原则(OCP)是最重要的一条原则。很多时候,一个 5 程序具有良好的设计,往往说明它是符合开放封闭原则的。
几乎所有的设计模式都是遵守开放 封闭原则的,我们见到的好设计,通常都经得起开放封闭原则的考验。不管是具体的各种设计 模式,还是更抽象的面向对象设计原则,比如单一职责原则、最少知识原则、依赖倒置原则等, 都是为了让程序遵守开放封闭原则而出现的。可以这样说,开放封闭原则是编写一个好程序的 目标,其他设计原则都是达到这个目标的过程。
里氏转换原则
子类继承父类,单独完全可以运行
依赖倒置原则
引用一个对象,如果这个对象有底层类型,直接引用底层类型
接口隔离原则
每一个接口应该是一种角色
合成/聚合复用原则
新的对象应使用一些已有的对象,使之成为新对象的一部分
迪米特原则(Law of Demeter,LoD)(也称最少知识原则LKP)
一个软件实体应当尽可能少地与其他实体发生相互作用。这 里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等
最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不 必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对 象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三 者对象来转发这些请求。
中介者模式很好地体现了最少知识原则。通过增加一个中介者对象,让所有的相关对象都通 过中介者对象来通信,而不是互相引用。所以,当一个对象发生改变时,只需要通知中介者对象 即可。
设计模式分类
创建型模式提供生存环境,结构型模式提供生存理由,行为型模式提供如何生存。
- 创建型模式为其他两种模式使用提供了环境。
- 结构型模式侧重于接口的使用,它做的一切工作都是对象或是类之间的交互,提供一个门。
- 行为型模式顾名思义,侧重于具体行为,所以概念中才会出现职责分配和算法通信等内容。
创建型
- 简单工厂模式(Simple Factory)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 创建者模式(Builder)
- 原型模式(Prototype)
- 单例模式(Singleton)
结构型
- 外观模式/门面模式(Facade门面模式)
- 适配器模式(Adapter)
- 代理模式(Proxy)
- 装饰模式(Decorator)
- 桥梁模式/桥接模式(Bridge)
- 组合模式(Composite)
- 享元模式(Flyweight)
行为型
- 模板方法模式(Template Method)
- 观察者模式(Observer)
- 状态模式(State)
- 策略模式(Strategy)
- 职责链模式(Chain of Responsibility)
- 命令模式(Command)
- 访问者模式(Visitor)
- 调停者模式(Mediator)
- 备忘录模式(Memento)
- 迭代器模式(Iterator)
- 解释器模式(Interpreter)
设计模式
在 JavaScript 中实现组合模式,看起来缺乏一些严谨性,我们的代码算不上安全,但能更快 速和自由地开发,这既是 JavaScript 的缺点,也是它的优点。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
JavaScript 其实是一门无类(class-free)语言,也正因为如此,生搬单例模式的概念并无 意义。在 JavaScript 中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什 么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在 JavaScript 中并 不适用。
单例模式的核心是确保只有一个实例,并提供全局访问。但是全局变量存在很多问题,它很容易造成命名空间污染。使用命名空间和使用闭包封装私有变量可以防止污染。
通用惰性单例:
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换
在以类为中心的传统面向对象语言中,不同的算法或者行为被封装在各个策略类 中,Context 将请求委托给这些策略对象,这些策略对象会根据请求返回不同的执行结果,这样 便能表现出对象的多态性。
在函数作为一等对象的语言中,策略模式是隐形的。 strategy 就是值为函数的变量。
在 JavaScript 中,除了使用类来封装算法和行为之外,使用函数 当然也是一种选择。这些“算法”可以被封装到函数中并且四处传递,也就是我们常说的“高阶 函数”。实际上在 JavaScript 这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身 当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。当我们对这些函 数发出“调用”的消息时,不同的函数会返回不同的执行结果。在 JavaScript 中,“函数对象的多态性”来得更加简单。
var strategies = {
S: function (salary) {
return salary * 4;
},
A: function (salary) {
return salary * 3;
},
B: function (salary) {
return salary * 2;
},
};
var calculateBonus = function (level, salary) {
return strategies[level](salary);
};
console.log(calculateBonus("S", 20000));
console.log(calculateBonus("A", 10000));
代理模式
保护代理:用于对象应该有不同访问权限的情况
虚拟代理:把一些开销很大的对象,延迟到 真正需要它的时候才去创建
var myImage = (function () {
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return function (src) {
imgNode.src = src;
};
})();
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage(this.src);
};
return function (src) {
myImage("file:// /C:/Users/svenzeng/Desktop/loading.gif");
img.src = src;
};
})();
proxyImage("http:// imgcache.qq.com/music// N/k/000GGDys0yA0Nk.jpg");
缓存代理:可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
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));
};
})();
proxyMult(1, 2, 3, 4); // 输出:24
proxyMult(1, 2, 3, 4); // 输出:24
迭代器模式
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
内部迭代器和外部迭代器:forEach VS generator
无论是内部迭代器还是外部迭代器,只要被 迭代的聚合对象拥有 length 属性而且可以用下标访问,那它就可以被迭代
var Iterator = function (obj) {
var current = 0;
var getCurrItem = function () {
return obj[current];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem,
};
};
发布-订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型 来替代传统的发布—订阅模式
发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM, 都少不了发布—订阅模式的参与,而且 JavaScript 本身也是一门基于事件驱动的语言。
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数 salesOffices.listen = function( key, fn ){
salesOffices.listen = function (key, fn) {};
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
// 如果还没有订阅过此类消息,给该类消息创建一个缓存列表 // 订阅的消息添加进消息缓存列表
this.clientList[key].push(fn);
// 如果还没有订阅过此类消息,给该类消息创建一个缓存列表 // 订阅的消息添加进消息缓存列表
salesOffices.trigger = 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); // (2) // arguments 是发布消息时附送的参数
}
};
// 小明订阅 88 平方米房子的消息
// 小红订阅 110 平方米房子的消息
salesOffices.listen("squareMeter88", function (price) {
console.log("价格= " + price); // 输出: 2000000
});
salesOffices.listen("squareMeter110", function (price) {
console.log("价格= " + price); // 输出: 3000000
});
salesOffices.trigger("squareMeter88", 2000000); // 发布 88 平方米房子的价格
salesOffices.trigger("squareMeter110", 3000000); // 发布 110 平方米房子的价格
命令模式
有时候需要向某些对象发送请求,但是并不知道请求的接收 者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品
JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。运算块不一定要封装在 command.execute 方法中,也可以封装在普通函数中。 函数作为一等对象,本身就可以被四处传递。即使我们依然需要请求“接收者”,那也未必使用 面向对象的方式,闭包可以完成同样的功能。
在面向对象设计中,命令模式的接收者被当成 command 对象的属性保存起来,同时约定执行 命令的操作调用 command.execute 方法。在使用闭包的命令模式实现中,接收者被封闭在闭包产 生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。无论接收者被保存为对象 的属性,还是被封闭在闭包产生的环境中,在将来执行命令的时候,接收者都能被顺利访问。用 闭包实现的命令模式如下代码所示:
var RefreshMenuBarCommand = function (receiver) {
return {
execute: function () {
receiver.refresh();
},
};
};
var setCommand = function (button, command) {
button.onclick = function () {
command.execute();
};
};
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button, refreshMenuBarCommand);
没有接收者的智 能命令,退化到和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图 的不同。策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标 的不同手段,它们的内部实现是针对“算法”而言的。而智能命令模式指向的问题域更广,command 对象解决的目标更具发散性。命令模式还可以完成撤销、排队等功能。
var closeDoorCommand = {
execute: function () {
console.log("关门");
},
};
组合模式
组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更 小的“孙对象”构成的。
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结 构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使 用具有一致性。
组合模式是一种 HAS-A(聚合)的关系,而不是 IS-A。组合对象包含一组叶对象,但 Leaf 并不是 Composite 的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键 7 是拥有相同的接口。
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 openTvCommand = {
execute: function () {
console.log("打开电视");
},
add: function () {
throw new Error("叶对象不能添加子节点");
},
};
var macroCommand = MacroCommand();
macroCommand.add(openTvCommand);
// Uncaught Error: 叶对象不能添加子节点
openTvCommand.add(macroCommand);
组合模式适用于以下这两种情况:
- 表示对象的部分整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分整 体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最 终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模 式中增加和删除树的节点非常方便,并且符合开放封闭原则。
- 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别, 客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就 不用写一堆 if、else 语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情, 这是组合模式最重要的能力。
然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都 与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外, 如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。
模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常 在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺 序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
var Beverage = function () {};
Beverage.prototype.boilWater = function () {
console.log("把水煮沸");
};
Beverage.prototype.brew = function () {
throw new Error("子类必须重写 brew 方法");
};
Beverage.prototype.pourInCup = function () {
throw new Error("子类必须重写 pourInCup 方法");
};
Beverage.prototype.addCondiments = function () {
throw new Error("子类必须重写 addCondiments 方法");
};
Beverage.prototype.customerWantsCondiments = function () {
return true; // 默认需要调料
};
Beverage.prototype.init = function () {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) {
this.addCondiments();
}
};
var CoffeeWithHook = function () {};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function () {
console.log("用沸水冲泡咖啡");
};
// 如果挂钩返回 true,则需要调料
CoffeeWithHook.prototype.pourInCup = function () {
console.log("把咖啡倒进杯子");
};
CoffeeWithHook.prototype.addCondiments = function () {
console.log("加糖和牛奶");
};
CoffeeWithHook.prototype.customerWantsCondiments = function () {
return window.confirm("请问需要调料吗?");
};
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
但在 JavaScript 中,我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数 是更好的选择。
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();
享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非 常有意义的事情。
享元模式的关键是区别内部 状态和外部状态。享元模式的过程是剥离外部状态,并把外部状态保存在其他地方,在合适的时刻 再把外部状态组装进共享对象。
- 内部状态储存于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
var Upload = function (uploadType) {
this.uploadType = uploadType;
};
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 =
"<span>文件名称:" +
fileName +
", 文件大小: " +
fileSize +
"</span>" +
'<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;
},
setExternalState: function (id, flyWeightObj) {
var uploadData = uploadDatabase[id];
for (var i in uploadData) {
flyWeightObj[i] = uploadData[i];
}
},
};
})();
而通过享元模式重构之后,对象的数量减少为 2,就算同时上传 2000 个文件,需要创建的 upload 对象数量依然是 2
责任链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间 5 的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
请求发送者只需要知道链中的第 一个节点,从而弱化了发送者和一组接收者之间的强联系。
使用责任链模式:
重构一个购票逻辑:
var order = function (orderType, pay, stock) {
if (orderType === 1) {
// 500 元定金购买模式
if (pay === true) {
// 已支付定金
console.log("500 元定金预购, 得到 100 优惠券");
} else {
// 未支付定金,降级到普通购买模式
if (stock > 0) {
// 用于普通购买的手机还有库存
console.log("普通购买, 无优惠券");
} else {
console.log("手机库存不足");
}
}
} else if (orderType === 2) {
if (pay === true) {
// 200 元定金购买模式
console.log("200 元定金预购, 得到 50 优惠券");
} else {
if (stock > 0) {
console.log("普通购买, 无优惠券");
} else {
console.log("手机库存不足");
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log("普通购买, 无优惠券");
} else {
console.log("手机库存不足");
}
}
};
order(1, true, 500);
// 输出: 500 元定金预购, 得到 100 优惠券
重构后:
var order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log("500 元定金预购,得到 100 优惠券");
} else {
return "nextSuccessor"; // 我不知道下一个节点是谁,反正把请求往后面传递
}
};
var order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log("200 元定金预购,得到 50 优惠券");
} else {
return "nextSuccessor"; // 我不知道下一个节点是谁,反正把请求往后面传递 }
}
};
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log("普通购买,无优惠券");
} else {
console.log("手机库存不足");
}
};
// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
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); // 输出:手机库存不足
职责链模式的最大优点就是解耦了请求发送者和 N 个接收者之间的复杂关 系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可。
如果运用得当,职责链模式可以很好地帮助我们组织代码,但这种模式也并非没有弊端,首 先我们不能保证某个请求一定会被链中的节点处理。
另外,职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分 节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避 免过长的职责链带来的性能损耗。
使用AOP:
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 order = order500yuan.after(order200yuan).after(orderNormal);
order(1, true, 500);
order(2, true, 500);
order(1, false, 500);
职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。
中介者模式
面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性。
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的 相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应 该尽可能少地了解另外的对象(类似不和陌生人说话)。
var goods = {
// 手机库存
"red|32G": 3,
"red|16G": 0,
"blue|32G": 1,
"blue|16G": 6,
};
var mediator = (function () {
var colorSelect = document.getElementById("colorSelect"),
memorySelect = document.getElementById("memorySelect"),
numberInput = document.getElementById("numberInput"),
colorInfo = document.getElementById("colorInfo"),
memoryInfo = document.getElementById("memoryInfo"),
numberInfo = document.getElementById("numberInfo"),
nextBtn = document.getElementById("nextBtn");
return {
changed: function (obj) {
var color = colorSelect.value, // 颜色
memory = memorySelect.value, // 内存
number = numberInput.value, // 数量
stock = goods[color + "|" + memory];
// 颜色和内存对应的手机库存数量
if (obj === colorSelect) {
// 如果改变的是选择颜色下拉框 colorInfo.innerHTML = color;
} else if (obj === memorySelect) {
memoryInfo.innerHTML = memory;
} else if (obj === numberInput) {
numberInfo.innerHTML = number;
}
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = "请选择手机颜色";
return;
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = "请选择内存大小";
return;
}
if (((number - 0) | 0) !== number - 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = "请输入正确的购买数量";
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = "放入购物车";
},
// 输入购买数量是否为正整数
};
})();
// 事件函数:
colorSelect.onchange = function () {
mediator.changed(this);
};
memorySelect.onchange = function () {
mediator.changed(this);
};
numberInput.oninput = function () {
mediator.changed(this);
};
引入中介者对象,所有的节点对象只跟中介者通信。当下拉选择框 colorSelect、 memorySelect 和文本输入框 numberInput 发生了事件行为时,它们仅仅通知中介者它们被改变了, 同时把自身当作参数传入中介者,以便中介者辨别是谁发生了改变。剩下的所有事情都交给中介 者对象来完成,这样一来,无论是修改还是新增节点,都只需要改动中介者对象里的代码。
装饰者模式
在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地 给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象
在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活, 还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之 改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。
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();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象 之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire 方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的 下一个对象。
它在框架开发中也十分有用,高阶组件也常用此设计模式。
用AOP装饰函数:
Function.prototype.before = function (beforefn) {
var __self = this; // 保存原函数的引用
return function () {
// 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数 // 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果, 2 // 并且保证 this 不被劫持
};
};
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};
var getToken = function () {
return "Token";
};
ajax = ajax.before(function (type, url, param) {
param.Token = getToken();
});
ajax("get", "http:// xxx.com/userinfo", { name: "sven" });
// 从 ajax 函数打印的 log 可以看到,Token 参数已经被附加到了 ajax 请求的参数中:
// {name: "sven", Token: "Token"}
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直 接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代 理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对 象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系 可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确 定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条 长长的装饰链。
状态模式
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的 if、else 条件 分支语言来控制状态之间的转换。
class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
// 初始化牛奶的存储量
this.leftMilk = '500ml';
}
stateToProcessor = {
that: this,
american() {
// 尝试在行为函数里拿到咖啡机实例的信息并输出
console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
console.log('我只吐黑咖啡');
},
latte() {
this.american()
console.log('加点奶');
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆');
},
mocha() {
this.latte();
console.log('再加巧克力');
}
}
// 关注咖啡机状态切换函数
changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}
const mk = new CoffeeMaker();
mk.changeState('latte');
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态 类,很容易增加新的状态和转换。
- 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过 5 多的条件分支。
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
- Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响
- 状态模式的缺点是会在系统中定义许多状态类
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请 求委托给这些类来执行。
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系, 所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态 和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情 发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本 由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的 场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。 这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿 到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建 一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
- 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实 现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够 使它们协同作用。
- 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象 增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理 模式是为了控制对对象的访问,通常也只包装一次。
- 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外 观模式最显著的特点是定义了一个新的接口。
外观模式
外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高 层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过 请求外观接口来达到访问子系统的目的。但在一段使用了外观模式的程序中,请求外观并不是强 制的。如果外观不能满足客户的个性化需求,那么客户也可以选择越过外观来直接访问子系统。
var A = function () {
a1();
a2();
};
var B = function () {
b1();
b2();
};
var facade = function () {
A();
B();
};
facade();