一、什么是设计模式
《JavaScript设计模式与开发实践》书中作者这样解释:
假设有一个空房间,我们要日复一日地往里面放一些东西。最简单的办法当然是把这些东西直接扔进去,但是时间久了,就会发现很难从这个房子里找到自己想要的东西,要调整某几样东西的位置也不容易。所以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,或许就是一种模式。
学习设计模式,有助于写出可复用和可维护性高的程序。
设计模式的原则是“找出 程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。
不过要注意,使用不当的话,可能会事倍功半。
二、设计原则
1. 单一职责原则(SRP)
一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。应该把对象或方法划分成较小的粒度。
2. 最少知识原则(LKP)
一个软件实体应当尽可能少地与其他实体发生相互作用,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系,可以转交给第三方进行处理。
3. 开放-封闭原则(OCP)
软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改。当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定。
三、常见的设计模式
1. 单例模式
定义:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
核心:
确保只有一个实例,并提供全局访问。
实现:我们可以使用闭包缓存一个内部变量来实现这个单例。
function getSingleton(createInstance) {
let instance;
return function () {
if (!instance) {
instance = createInstance();
}
return instance;
};
}
// 工厂函数
function TeacherFactor() {
function Teacher() {
console.log('init');
}
return new Teacher();
}
const teacher = getSingleton(TeacherFactor);
2. 策略模式
定义:
策略模式是指有一定行动内容的相对稳定的策略名称。
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。
核心:
定义了一组算法(业务规则)。
封装了每个算法。
这些算法可互换代替。
实现:
抽象策略角色: 策略类,通常由一个接口或者抽象类实现。
具体策略角色:包装了相关的算法和行为。
环境角色:持有一个策略类的引用,最终给客户端调用。
TypeScript 版本实现如下:
// 策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
// 具体实现:【出行旅游策略: 乘飞机、坐火车、骑自行车、徒步】
interface ITravelStrategy {
travel(): void;
}
// 乘飞机策略
class AirplaneStrategy implements ITravelStrategy {
travel(): void {
console.log('乘飞机...');
}
}
// 坐火车策略
class TrainStrategy implements ITravelStrategy {
travel(): void {
console.log('坐火车...');
}
}
// 骑自行车
class BikeStrategy implements ITravelStrategy {
travel(): void {
console.log('骑自行车...');
}
}
// 徒步策略
class WalkStrategy implements ITravelStrategy {
travel(): void {
console.log('徒步...');
}
}
// 策略上下文环境
class StrategyContext {
constructor(private readonly context: ITravelStrategy) {}
public executeStrategy() {
return this.context.travel();
}
}
// 测试案例
// 我要徒步旅行
const walk = new StrategyContext(new WalkStrategy());
walk.executeStrategy();
// 我要坐飞机
const fly = new StrategyContext(new AirplaneStrategy());
fly.executeStrategy();
/**【目前还是有个问题,策略的具体实现类,还是可以通过new创建实例,进行具体的操作】**/
// 比如:我要骑自行车去旅游
const bike = new BikeStrategy();
bike.travel();
/////////////////////// 如何避免上述情况呢 ?///////////////////
3. 代理模式
定义:
为一个对象提供一个代用品或占位符,以便控制对它的访问。
核心:
当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象,代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一 些额外的事情。
实现:
按职责来划分,通常有以下使用场景:
1、远程代理。
2、虚拟代理。
3、Copy-on-Write 代理。
4、保护(Protect or Access)代理。
5、Cache代理。
6、防火墙(Firewall)代理。
7、同步化(Synchronization)代理。
8、智能引用(Smart Reference)代理。
注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
我们将创建一个 Image 接口和实现了 Image 接口的实体类。ProxyImage 是一个代理类,减少 RealImage 对象加载的内存占用。ProxyPatternDemo 类使用 ProxyImage 来获取要加载的 Image 对象,并按照需求进行显示。
//////////【ts文件】////////////////
// 图片显示的接口
export interface Image {
display(): void;
}
// 创建实现Image接口的实体类
class RealImage implements Image {
constructor(private readonly fileName: string) {
this.loadFromDisk(fileName);
}
display(): void {
console.log('Displaying ' + this.fileName);
}
private loadFromDisk(fileName: string) {
console.log('Loading ' + fileName);
}
}
export class ProxyImage implements Image {
private realImage: RealImage | null = null;
private fileName: string;
constructor(fileName: string) {
this.fileName = fileName;
}
display(): void {
if (this.realImage === null) {
this.realImage = new RealImage(this.fileName);
}
this.realImage.display();
}
}
// 测试用例
const image = new ProxyImage('demo1.jpg');
// 图像将从磁盘加载
image.display();
console.log('-------');
// 图像不需要从磁盘加载
image.display();
/*
执行程序,输出结果如下:
Loading demo1.jpg
Displaying demo1.jpg
-------
Displaying demo1.jpg
*/
4. 迭代器模式
定义:
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
核心:
在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
实现:
JS中数组的map forEach 已经内置了迭代器
5. 发布订阅模式
定义:
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知
核心:
取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。
与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅。
实现:
JS中的事件就是经典的发布-订阅模式的实现。
6. 命令模式
定义:
用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。命令(command)指的是一个执行某些特定事情的指令。
核心:
命令中带有execute执行、undo撤销、redo重做等相关命令方法,建议显示地指示这些方法名。
实现:
简单的命令模式实现可以直接使用对象字面量的形式定义一个命令。
7. 组合模式
定义:
是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的。
核心:
可以用树形结构来表示这种“部分- 整体”的层次结构。调用组合对象 的execute方法,程序会递归调用组合对象下面的叶对象的execute方法。但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象。基于这种委托,就需要保证组合对象和叶对象拥有相同的 接口此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作。
实现:
使用组合模式来实现扫描文件夹中的文件。
8. 模版方法模式
定义:
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。
核心:
在抽象父类中封装子类的算法框架,它的 init方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。
由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法。
实现:
模板方法模式一般的实现方式为继承。
以运动作为例子,运动有比较通用的一些处理,这部分可以抽离开来,在父类中实现。具体某项运动的特殊性则有子类来重写实现。最终子类直接调用父类的模板函数来执行。
9. 享元模式
定义:
享元(flyweight)模式是一种用于性能优化的模式,它的目标是尽量减少共享对象的数量。
核心:
运用共享技术来有效支持大量细粒度的对象。
强调将对象的属性划分为内部状态(属性)与外部状态(属性)。内部状态用于对象的共享,通常不变;而外部状态则剥离开来,由具体的场景决定。
实现:
在程序中使用了大量的相似对象时,可以利用享元模式来优化,减少对象的数量。举个栗子,要对某个班进行身体素质测量,仅测量身高体重来评判。
10. 职责链模式
定义:
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链 传递该请求,直到有一个对象处理它为止。
核心:
请求发送者只需要知道链中的第一个节点,弱化发送者和一组接收者之间的强联系,可以便捷地在职责链中增加或删除一个节点,同样地,指定谁是第一个节点也很便捷。
实现:
以展示不同类型的变量为例,设置一条职责链,可以免去多重if条件分支。
11. 中介者模式
定义:
所有的相关 对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可
核心:
使网状的多对多关系变成了相对简单的一对多关系(复杂的调度处理都交给中介者)。
实现:
多个对象,指的不一定得是实例化的对象,也可以将其理解成互为独立的多个项。当这些项在处理时,需要知晓并通过其他项的数据来处理。
如果每个项都直接处理,程序会非常复杂,修改某个地方就得在多个项内部修改。我们将这个处理过程抽离出来,封装成中介者来处理,各项需要处理时,通知中介者即可。
12. 装饰者模式
定义:
以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。是一种“即用即付”的方式,能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
核心:
是为对象动态加入行为,经过多重包装,可以形成一条装饰链。
实现:
最简单的装饰者,就是重写对象的属性。
13. 状态模式
定义:
事物内部状态的改变往往会带来事物的行为改变。在处理的时候,将这个处理委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
核心:
区分事物内部的状态,把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
实现:
以一个人的工作状态作为例子,在刚醒、精神、疲倦几个状态中切换着。
14. 适配器模式
定义:
是解决两个软件实体间的接口不兼容的问题,对不兼容的部分进行适配。
核心:
解决两个已有接口之间不匹配的问题。
实现:
比如一个简单的数据格式转换的适配器。
15. 外观模式
定义:
为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使子系统更加容易使用。
核心:
可以通过请求外观接口来达到访问子系统,也可以选择越过外观来直接访问子系统。
实现:
外观模式在JS中,可以认为是一组函数的集合。