前言
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式可以分为下面三种类型:
1、创建型模式:用来描述 “如何创建对象”,它的主要特点是 “将对象的创建和使用分离”。包括单例、原型、工厂方法、抽象工厂和建造者 5 种模式。
2、结构型模式:用来描述如何将类或对象按照某种布局组成更大的结构。包括代理、适配器、桥接、装饰、外观、享元和组合 7 种模式。
3、行为型模式:用来识别对象之间的常用交流模式以及如何分配职责。包括模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录和解释器 11 种模式。
以下为整理的前端常用的几种设计模式的学习笔记
一、单例模式
单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。
单例模式(Singleton Pattern)是一种常用的模式,有一些对象我们往往只需要一个,比如全局缓存、浏览器中的 window 对象等。单例模式用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。
1.1 typescript代码实现如下:
懒汉式
顾名思义,懒汉设计模式,如果没有调用时,那么懒汉不会进行实例化类(添加了一个是否存在对象实例的判断)。
特点:只有在第一次调用创建实例的时候才会赋值,这样也就避免了不管用不用这个类,都直接进行初始化浪费内存。
class Singleton {
// 定义私有的静态属性,来保存对象实例
private static singleton: Singleton;
//设置成private,就是防止通过new的方式实例化
private constructor() {}
// 提供一个静态的方法来获取对象实例(在静态方法里只能用静态属性)
public static getInstance(): Singleton {
//如果还没有当前对象实例,就创建一个对象实例
if (!Singleton.singleton) {
Singleton.singleton = new Singleton();
}
//一个类只能有一个对象实例
return Singleton.singleton;
}
}
饿汉式
相较于懒汉设计模式,饿汉设计模式就相比较简单多了,直接创建对象实例,一劳永逸,真的就是懒汉。
特点:执行效率比较高,同时执行就加载也就带来了浪费内存。
class Singleton {
// 定义私有的静态属性,来保存对象实例(直接创建对象实例)
private static singleton: Singleton=new Singleton();
private constructor() {}
// 提供一个静态的方法来获取对象实例
public static getInstance(): Singleton {
//一个类只能有一个对象实例
return Singleton.singleton;
}
}
1.2 使用示例
let instance1 = Singleton.getInstance();
let instance2 = Singleton.getInstance();
//得到的对象实例是一样的,全局调用使用的都是同一个对象(饿汉式和懒汉式调用方式都是这样的)
console.log(instance1 === instance2); // true
1.3 应用场景
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或耗资源过多,但又经常用到的对象。
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
二、适配器模式
适配器模式在 TypeScript 代码中很常见。基于一些遗留代码的系统常常会使用该模式。在这种情况下,适配器让遗留代码与现代的类得以相互合作。
适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。当适配器的任何方法被调用时,它会将参数转换为合适的格式,然后将调用定向到其封装对象中的一个或多个方法。
2.1 typescript代码实现如下:
//记录器的接口
interface Logger {
info(message: string): Promise<void>;
}
//云记录器的接口
interface CloudLogger {
sendToServer(message: string, type: string): Promise<void>;
}
//阿里记录器实现了云记录器,它也属于一种云记录器
class AliLogger implements CloudLogger {
public async sendToServer(message: string, type: string): Promise<void> {
console.info(message);
console.info('This Message was saved with AliLogger');
}
}
//云记录器的适配器,实现了记录器(CloudLoggerAdapter必须实现Logger接口的所有方法,否则会报错)
class CloudLoggerAdapter implements Logger {
protected cloudLogger: CloudLogger;
constructor (cloudLogger: CloudLogger) {
this.cloudLogger = cloudLogger;
}
public async info(message: string): Promise<void> {
await this.cloudLogger.sendToServer(message, 'info');
}
}
//通告服务
class NotificationService {
protected logger: Logger;
constructor (logger: Logger) {
this.logger = logger;
}
public async send(message: string): Promise<void> {
await this.logger.info(`Notification sended: ${message}`);
}
}
在以上代码中,因为Logger
和CloudLogger
这两个接口不匹配,所以我们引入了CloudLoggerAdapter
适配器来解决兼容性问题。
2.2 使用示例
(async () => {
const aliLogger = new AliLogger();
const cloudLoggerAdapter = new CloudLoggerAdapter(aliLogger);
const notificationService = new NotificationService(cloudLoggerAdapter);
await notificationService.send('Hello semlinker, To Cloud');
})();
现在的客户端要使用的是AliLogger的类,它是实现了CloudLogger,但是NotificationService服务里的send函数方法里使用的是Logger接口的info方法实现,AliLogger实现的CloudLogger接口里面是不包含有info方法的,因此现在NotificationService服务是无法直接使用AliLogger完成send操作的。所以只能在中间使用一个CloudLoggerAdapter 适配器完成CloudLogger向Logger的适配,适配器的作用原理很简单:
1、由于NotificationService需要用到info函数方法,所以适配器要implements(实现)Logger;
2、实现Logger接口的info函数方法(实现接口就必须将接口里面的函数方法全部实现,不然会报错),将CloudLogger的sendToServer函数方法通过包裹在Logger的info函数方法里面;
3、使NotificationService可以通过调用info函数间接调用到CloudLogger的sendToServer函数;
2.3 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
三、观察者模式
观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
在观察者模式中有两个主要角色:Subject(主题)和 Observer(观察者)。
3.1 typescript代码实现如下:
//观察者
interface Observer {
notify: Function;
}
//具体观察者(实现了Observer及函数方法)
class ConcreteObserver implements Observer{
constructor(private name: string) {}
notify() {
console.log(`${this.name} has been notified.`);
}
}
//主题(被观察者)
class Subject {
//观察当前“主题”的“观察者”们
private observers: Observer[] = [];
//添加当前“主题”的观察者
public addObserver(observer: Observer): void {
console.log(observer, "is pushed!");
this.observers.push(observer);
}
//删除当前“主题”的观察者
public deleteObserver(observer: Observer): void {
console.log("remove", observer);
const n: number = this.observers.indexOf(observer);
if(n>-1){
this.observers.splice(n, 1);
}
}
//通知所有的“观察者”
public notifyObservers(): void {
console.log("notify all the observers", this.observers);
this.observers.forEach(observer => observer.notify());
}
}
3.2 使用示例
const subject: Subject = new Subject();
const xiaoQin = new ConcreteObserver("小秦");
const xiaoWang = new ConcreteObserver("小王");
subject.addObserver(xiaoQin);
subject.addObserver(xiaoWang);
subject.notifyObservers();
//删除“小秦”后,只会通知小王
subject.deleteObserver(xiaoQin);
subject.notifyObservers();
其实上边代码原理非常简单,目的就是实现一对多的关系。
3.3 应用场景
- 一个对象的行为依赖于另一个对象的状态。或者换一种说法,当被观察对象(目标对象)的状态发生改变时 ,会直接影响到观察对象的行为。
四、发布订阅模式
发布订阅模式其实就是观察者模式的进阶版,他们实现的功能基本是一致的。
但发布订阅模式更侧重于事件,当订阅的某个事件被触发时,订阅此事件的所有对象将会得到通知。
在发布订阅模式中有三个主要角色:Publisher(发布者)、 Channels(通道)和 Subscriber(订阅者)。
发布订阅模式
4.1 typescript代码实现如下:
//定义事件的类型声明
type EventHandler = (...args: any[]) => any;
// 创建事件调度中心,为订阅者和发布者提供调度服务
class EventEmitter {
//通过键值对的方式存储事件
private c = new Map<string, EventHandler[]>();
// 订阅指定的主题(添加键值对值)
subscribe(topic: string, ...handlers: EventHandler[]) {
let topics = this.c.get(topic);
if (!topics) {
this.c.set(topic, topics = []);
}
topics.push(...handlers);
}
// 删除指定topic(主题)拥有的事件中的handler
unsubscribe(topic: string, handler?: EventHandler): boolean {
//当前指定主题下的删除事件参数为空,就默认直接删除当前指定主题和拥有的所有事件
if (!handler) {
return this.c.delete(topic);
}
const topics = this.c.get(topic);
//当前指定主题未存在
if (!topics) {
return false;
}
const index = topics.indexOf(handler);
//当前指定主题下没有需要删除的这个事件
if (index < 0) {
return false;
}
//删除当前指定主题下的需要删除的这个事件
topics.splice(index, 1);
//如果已经删除完,当前主题的所有事件,就直接将这个主题删除
if (topics.length === 0) {
this.c.delete(topic);
}
return true;
}
// 为指定的主题发布消息(循环执行回调函数,并传值)
publish(topic: string, ...args: any[]): any[] | null {
const topics = this.c.get(topic);
if (!topics) {
return null;
}
//循环执行当前指定主题拥有的所有handler(回调函数),并将args(函数参数)传入
return topics.map(handler => {
try {
return handler(...args);
} catch (e) {
console.error(e);
return null;
}
});
}
}
我个人的理解,上述代码EventEmitter 实现的就是key-value的添加、删除、查询。
4.2 使用示例
// 创建事件调度中心,为订阅者和发布者提供调度服务
const eventEmitter = new EventEmitter();
// A订阅了SMS事件(A只关注SMS本身,而不关心谁发布这个事件)
eventEmitter.subscribe("SMS", (msg) => console.log(`A收到订阅的消息:${msg}`) );
// B订阅了SMS事件(B只关注SMS本身,而不关心谁发布这个事件)
eventEmitter.subscribe("SMS", (msg) => console.log(`B收到订阅的消息:${msg}`) );
// C发布了SMS事件(C只关注SMS本身,不关心谁订阅了这个事件)
eventEmitter.publish("SMS", "SMS发布订阅模式");
//打印结果:A收到订阅的消息:SMS发布订阅模式
B收到订阅的消息:SMS发布订阅模式
//删除“SMS”的主题
eventEmitter.unsubscribe("SMS");
eventEmitter.publish("SMS", "TypeScript发布订阅模式");//直接返回null
4.3 应用场景
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 作为事件总线,来实现不同组件间或模块间的通信。
五、观察者模式_VS_发布订阅模式
可以看出,发布订阅模式相比观察者模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝了订阅者和发布者的依赖关系。即订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。
观察者模式有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加到目标(Subject) 中进行管理;另一方面,目标在触发事件的时候,也无法将通知操作(notify) 委托给事件通道,因此只能亲自去通知所有的观察者。
六、策略模式
策略模式(Strategy Pattern)定义了一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。
目前在一些主流的 Web 站点中,都提供了多种不同的登录方式。比如账号密码登录、手机验证码登录和第三方登录。为了方便维护不同的登录方式,我们可以把不同的登录方式封装成不同的登录策略。
对应的 UML 类图(网上找的图片箭头标识错误)
6.1 typescript代码实现如下:
//策略接口
interface Strategy {
//认证
authenticate(...args: any): any;
}
//认证器(决定使用什么策略)
class Authenticator {
strategy: any;
constructor() {
this.strategy = null;
}
setStrategy(strategy: any) {
this.strategy = strategy;
}
authenticate(...args: any) {
if (!this.strategy) {
console.log('尚未设置认证策略');
return;
}
return this.strategy.authenticate(...args);
}
}
//微信认证策略
class WechatStrategy implements Strategy {
//认证方式
authenticate(wechatToken: string) {
if (wechatToken !== '123') {
console.log('无效的微信用户');
return;
}
console.log('微信认证成功');
}
}
//账号密码登录策略
class LocalStrategy implements Strategy {
//认证方式
authenticate(username: string, password: string) {
if (username !== 'abao' && password !== '123') {
console.log('账号或密码错误');
return;
}
console.log('账号和密码认证成功');
}
}
WechatStrategy(微信认证策略)和LocalStrategy (账号密码登录策略)都要implements实现Strategy接口,因为两个策略都需要有authenticate函数方法,只是参数传入不同,里面实现方式不同,通过Authenticator 实现策略的选择和函数调用。
6.2 使用示例
const auth = new Authenticator();
//设置为微信认证策略
auth.setStrategy(new WechatStrategy());
auth.authenticate('123456');
//设置为账号密码认证策略
auth.setStrategy(new LocalStrategy());
auth.authenticate('abao', '123');
6.3 简单代码实现:
上边的属于比较复杂的策略模式示例,平时前端开发中很少用到,其实实际开发中经常用到策略模式,使用非常简洁,代码如下:
//键值对的方式存储所有策略
const vaccineDict = {
chinaVaccine: () => {
return "中国疫苗"
},
russiaVaccine: () => {
return "俄罗斯疫苗"
},
americaVaccine: () => {
return "漂亮国疫苗"
}
}
class Strategy {
getVaccineInfo(vaccineCode) {
if (vaccineDict[vaccineCode]) {
return vaccineDict[vaccineCode]();
} else {
return "输入内容无效"
}
}
}
//策略模式使用
const strategy = new Strategy();
strategy.getVaccineInfo('chinaVaccine');//中国疫苗
strategy.getVaccineInfo('demo');//输入内容无效
直接使用key-value的方式存储所有策略,通过传入key值的方式选择策略,是不是感到很熟悉,就是这样的简单。
6.4 应用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
七、模板方法模式
模板方法模式由两部分结构组成:抽象父类和具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
在上图中,客户端通过使用不同的解析器来分别解析 CSV 和 Markup 文件。虽然解析的是不同的类型的文件,但文件的处理流程是一样的。这里主要包含读取文件、解析文件和打印数据三个步骤。针对这个场景,我们就可以引入模板方法来封装以上三个步骤的处理顺序。
对应的 UML 类图
7.1 typescript代码实现如下:
//读取文件的包
import fs from 'fs';
//文件数据解析器的抽象类,里面包含了解析的流程及一些公共的函数方法,子类也可以按需重写这些函数方法
abstract class DataParser {
//读取的文件数据
data: string = '';
//输出内容
out: any = null;
// 这就是所谓的模板方法(文件解析流程控制)
parse(pathUrl: string) {
//先读取文件内容
this.readFile(pathUrl);
//解析文件
this.doParsing();
//打印处理好的文件内容
this.printData();
}
readFile(pathUrl: string) {
this.data = fs.readFileSync(pathUrl, 'utf8');
}
//抽象方法没有具体的实现,要求子类必须实现该方法
abstract doParsing(): void;
printData() {
console.log(this.out);
}
}
//CSV解析器,需要重写解析文件的函数方法
class CSVParser extends DataParser {
doParsing() {
this.out = this.data.split(',');
}
}
//Markup解析器,需要重写解析文件的函数方法
class MarkupParser extends DataParser {
doParsing() {
this.out = this.data.match(/<\w+>.*<\/\w+>/gim);
}
}
7.2 使用示例
const csvPath = './data.csv';
const mdPath = './design-pattern.md';
new CSVParser().parse(csvPath);
new MarkupParser().parse(mdPath);
7.3 应用场景
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
- 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
八、工厂模式
在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式。
8.1 简单工厂模式
简单工厂模式又叫静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的“产品”类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。
简单工厂模式的例子
8.1.1 typescript代码实现如下:
//BMW车的抽象类
abstract class BMW {
//抽象方法,子类必须重写
abstract run(): void;
}
class BMW730 extends BMW {
run(): void {
console.log("BMW730 发动咯");
}
}
class BMW840 extends BMW {
run(): void {
console.log("BMW840 发动咯");
}
}
class BMWFactory {
public static produceBMW(model: "730" | "840"): BMW {
if (model === "730") {
return new BMW730();
} else {
return new BMW840();
}
}
}
在以上代码中,我们定义一个BMWFactory
类,该类提供了一个静态的produceBMW()
方法,用于根据不同的模型参数来创建不同型号的车子。但是每新增一种产品就得修改工厂类,加入必要的处理逻辑。
8.1.2 使用示例
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");
bmw730.run();
bmw840.run();
8.1.3 应用场景
- 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
- 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。
8.2 工厂方法模式
工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。
在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
在简单工厂模式中,所有的产品都是由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。
工厂方法模式的例子
在上图中,模拟了用户购车的流程,小王和小秦分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车子,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。
8.2.1 typescript代码实现如下:
abstract class BMWFactory {
abstract produceBMW(): BMW;
}
class BMW730Factory extends BMWFactory {
produceBMW(): BMW {
return new BMW730();
}
}
class BMW840Factory extends BMWFactory {
produceBMW(): BMW {
return new BMW840();
}
}
在以上代码中,我们分别创建了BMW730Factory
和BMW840Factory
两个工厂类,然后使用这两个类的实例来生产不同型号的车子。
8.2.2 使用示例
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();
const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();
bmw730.run();
bmw840.run();
8.2.3 应用场景
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
8.3 抽象工厂模式
抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。 但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。
抽象工厂模式的例子
在上图中,模拟了用户购车的流程,小王向 BMW 工厂订购了 BMW730,工厂按照 730 对应的模型进行生产并在生产完成后交付给小王。而小秦向同一个 BMW 工厂订购了 BMW840,工厂按照 840 对应的模型进行生产并在生产完成后交付给小秦。
8.3.1 typescript代码实现如下:
//工厂抽象类,包含生成各种产品的抽象函数方法
abstract class BMWFactory {
abstract produce730BMW(): BMW730;
abstract produce840BMW(): BMW840;
}
//集成抽象类和实现里面的抽象方法
class ConcreteBMWFactory extends BMWFactory {
produce730BMW(): BMW730 {
return new BMW730();
}
produce840BMW(): BMW840 {
return new BMW840();
}
}
8.3.2 使用示例
const bmwFactory = new ConcreteBMWFactory();
const bmw730 = bmwFactory.produce730BMW();
const bmw840 = bmwFactory.produce840BMW();
bmw730.run();
bmw840.run();
8.3.3 应用场景
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
- 系统中有多于一个的产品族,而每次只使用其中某一产品族。
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
参考文章:
图解九种常见的设计模式
图解23种设计模式(TypeScript版)——前端切图仔提升内功的必经之路
观察者模式与订阅发布模式的区别