图解常见的九种设计模式

开发 前端
在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。

 [[345020]]

本文转载自微信公众号「全栈修仙之路」,作者阿宝哥。转载本文请联系全栈修仙之路公众号。

在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。根据模式的目的来划分的话,GoF(Gang of Four)设计模式可以分为以下 3 种类型:

 

1、创建型模式:用来描述 “如何创建对象”,它的主要特点是 “将对象的创建和使用分离”。包括单例、原型、工厂方法、抽象工厂和建造者 5 种模式。

2、结构型模式:用来描述如何将类或对象按照某种布局组成更大的结构。包括代理、适配器、桥接、装饰、外观、享元和组合 7 种模式。

3、行为型模式:用来识别对象之间的常用交流模式以及如何分配职责。包括模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录和解释器 11 种模式。

接下来阿宝哥将结合一些生活中的场景并通过精美的配图,来向大家介绍 9 种常用的设计模式。

一、建造者模式

建造者模式(Builder Pattern)将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

一辆小汽车 🚗 通常由 发动机、底盘、车身和电气设备 四大部分组成。汽车电气设备的内部构造很复杂,简单起见,我们只考虑三个部分:引擎、底盘和车身。

 

在现实生活中,小汽车也是由不同的零部件组装而成,比如上图中我们把小汽车分成引擎、底盘和车身三大部分。下面我们来看一下如何使用建造者模式来造车子。

1.1 实现代码

  1. class Car { 
  2.   constructor( 
  3.     public engine: string, 
  4.     public chassis: string,  
  5.     public body: string 
  6.   ) {} 
  7.  
  8. class CarBuilder { 
  9.   engine!: string; // 引擎 
  10.   chassis!: string; // 底盘 
  11.   body!: string; // 车身 
  12.  
  13.   addChassis(chassis: string) { 
  14.     this.chassis = chassis; 
  15.     return this; 
  16.   } 
  17.  
  18.   addEngine(engine: string) { 
  19.     this.engine = engine; 
  20.     return this; 
  21.   } 
  22.  
  23.   addBody(body: string) { 
  24.     this.body = body; 
  25.     return this; 
  26.   } 
  27.  
  28.   build() { 
  29.     return new Car(this.engine, this.chassis, this.body); 
  30.   } 

在以上代码中,我们定义一个 CarBuilder 类,并提供了 addChassis、addEngine和 addBody 3 个方法用于组装车子的不同部位,当车子的 3 个部分都组装完成后,调用build 方法就可以开始造车。

1.2 使用示例

  1. const car = new CarBuilder() 
  2.   .addEngine('v12'
  3.   .addBody('镁合金'
  4.   .addChassis('复合材料'
  5.   .build(); 

1.3 应用场景及案例

需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。

需要生成的产品对象的属性相互依赖,需要指定其生成顺序。

隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。

Github - node-sql-query:https://github.com/dresende/node-sql-query

二、工厂模式

在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。

 

在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式。

2.1 简单工厂

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

 

在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂会先判断用户选择的车型,然后按照对应的模型进行生产并在生产完成后交付给用户。

下面我们来看一下如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。

2.1.1 实现代码

  1. abstract class BMW { 
  2.   abstract run(): void; 
  3.  
  4. class BMW730 extends BMW { 
  5.   run(): void { 
  6.     console.log("BMW730 发动咯"); 
  7.   } 
  8.  
  9. class BMW840 extends BMW { 
  10.   run(): void { 
  11.     console.log("BMW840 发动咯"); 
  12.   } 
  13.  
  14. class BMWFactory { 
  15.   public static produceBMW(model: "730" | "840"): BMW { 
  16.     if (model === "730") { 
  17.       return new BMW730(); 
  18.     } else { 
  19.       return new BMW840(); 
  20.     } 
  21.   } 

在以上代码中,我们定义一个 BMWFactory 类,该类提供了一个静态的 produceBMW()方法,用于根据不同的模型参数来创建不同型号的车子。

2.1.2 使用示例

  1. const bmw730 = BMWFactory.produceBMW("730"); 
  2. const bmw840 = BMWFactory.produceBMW("840"); 
  3.  
  4. bmw730.run(); 
  5. bmw840.run(); 

2.1.3 应用场景

  • 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
  • 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。

2.2 工厂方法

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

 

在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车子,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。

同样,我们来看一下如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。

2.2.1 实现代码

  1. abstract class BMWFactory { 
  2.   abstract produceBMW(): BMW; 
  3.  
  4. class BMW730Factory extends BMWFactory { 
  5.   produceBMW(): BMW { 
  6.     return new BMW730(); 
  7.   } 
  8.  
  9. class BMW840Factory extends BMWFactory { 
  10.   produceBMW(): BMW { 
  11.     return new BMW840(); 
  12.   } 

在以上代码中,我们分别创建了 BMW730Factory 和 BMW840Factory 两个工厂类,然后使用这两个类的实例来生产不同型号的车子。

2.2.2 使用示例

  1. const bmw730Factory = new BMW730Factory(); 
  2. const bmw840Factory = new BMW840Factory(); 
  3.  
  4. const bmw730 = bmw730Factory.produceBMW(); 
  5. const bmw840 = bmw840Factory.produceBMW(); 
  6.  
  7. bmw730.run(); 
  8. bmw840.run(); 

2.2.3 应用场景

  • 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  • 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

2.3 抽象工厂

抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。

 

在上图中,阿宝哥模拟了用户购车的流程,小王向 BMW 工厂订购了 BMW730,工厂按照 730 对应的模型进行生产并在生产完成后交付给小王。而小秦向同一个 BMW 工厂订购了 BMW840,工厂按照 840 对应的模型进行生产并在生产完成后交付给小秦。

下面我们来看一下如何使用抽象工厂来描述上述的购车过程。

2.3.1 实现代码

  1. abstract class BMWFactory { 
  2.   abstract produce730BMW(): BMW730; 
  3.   abstract produce840BMW(): BMW840; 
  4.  
  5. class ConcreteBMWFactory extends BMWFactory { 
  6.   produce730BMW(): BMW730 { 
  7.     return new BMW730(); 
  8.   } 
  9.  
  10.   produce840BMW(): BMW840 { 
  11.     return new BMW840(); 
  12.   } 

2.3.2 使用示例

  1. const bmwFactory = new ConcreteBMWFactory(); 
  2.  
  3. const bmw730 = bmwFactory.produce730BMW(); 
  4. const bmw840 = bmwFactory.produce840BMW(); 
  5.  
  6. bmw730.run(); 
  7. bmw840.run(); 

2.3.3 应用场景

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
  • 系统中有多于一个的产品族,而每次只使用其中某一产品族。
  • 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

三、单例模式

单例模式(Singleton Pattern)是一种常用的模式,有一些对象我们往往只需要一个,比如全局缓存、浏览器中的 window 对象等。单例模式用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。

 

在上图中,阿宝哥模拟了借车的流程,小王临时有急事找阿宝哥借车子,阿宝哥家的车子刚好没用,就借给小王了。当天,小秦也需要用车子,也找阿宝哥借车,因为阿宝哥家里只有一辆车子,所以就没有车可借了。

对于车子来说,它虽然给生活带来了很大的便利,但养车也需要一笔不小的费用(车位费、油费和保养费等),所以阿宝哥家里只有一辆车子。

在开发软件系统时,如果遇到创建对象时耗时过多或耗资源过多,但又经常用到的对象,我们就可以考虑使用单例模式。

下面我们来看一下如何使用 TypeScript 来实现单例模式。

3.1 实现代码

  1. class Singleton { 
  2.   // 定义私有的静态属性,来保存对象实例 
  3.   private static singleton: Singleton; 
  4.   private constructor() {} 
  5.  
  6.   // 提供一个静态的方法来获取对象实例 
  7.   public static getInstance(): Singleton { 
  8.     if (!Singleton.singleton) { 
  9.       Singleton.singleton = new Singleton(); 
  10.     } 
  11.     return Singleton.singleton; 
  12.   } 

3.2 使用示例

  1. let instance1 = Singleton.getInstance(); 
  2. let instance2 = Singleton.getInstance(); 
  3.  
  4. console.log(instance1 === instance2); // true 

3.3 应用场景

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或耗资源过多,但又经常用到的对象。
  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。

四、适配器模式

在实际生活中,也存在适配器的使用场景,比如:港式插头转换器、电源适配器和 USB 转接口。而在软件工程中,适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体就可以一起工作。

 

4.1 实现代码

  1. interface Logger { 
  2.   info(message: string): Promise<void>; 
  3.  
  4. interface CloudLogger { 
  5.   sendToServer(message: string, type: string): Promise<void>; 
  6.  
  7. class AliLogger implements CloudLogger { 
  8.   public async sendToServer(message: string, type: string): Promise<void> { 
  9.     console.info(message); 
  10.     console.info('This Message was saved with AliLogger'); 
  11.   } 
  12.  
  13. class CloudLoggerAdapter implements Logger { 
  14.   protected cloudLogger: CloudLogger; 
  15.  
  16.   constructor (cloudLogger: CloudLogger) { 
  17.     this.cloudLogger = cloudLogger; 
  18.   } 
  19.  
  20.   public async info(message: string): Promise<void> { 
  21.     await this.cloudLogger.sendToServer(message, 'info'); 
  22.   } 
  23.  
  24. class NotificationService { 
  25.   protected logger: Logger; 
  26.    
  27.   constructor (logger: Logger) {     
  28.     this.logger = logger; 
  29.   } 
  30.  
  31.   public async send(message: string): Promise<void> { 
  32.     await this.logger.info(`Notification sended: ${message}`); 
  33.   } 

在以上代码中,因为 Logger 和 CloudLogger 这两个接口不匹配,所以我们引入了 CloudLoggerAdapter 适配器来解决兼容性问题。

4.2 使用示例

  1. (async () => { 
  2.   const aliLogger = new AliLogger(); 
  3.   const cloudLoggerAdapter = new CloudLoggerAdapter(aliLogger); 
  4.   const notificationService = new NotificationService(cloudLoggerAdapter); 
  5.   await notificationService.send('Hello semlinker, To Cloud'); 
  6. })(); 

4.3 应用场景及案例

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
  • Github - axios-mock-adapter:https://github.com/ctimmerm/axios-mock-adapter

五、观察者模式 & 发布订阅模式

5.1 观察者模式

观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

在观察者模式中有两个主要角色:Subject(主题)和 Observer(观察者)。

 

在上图中,Subject(主题)就是阿宝哥的 TS 专题文章,而观察者就是小秦和小王。由于观察者模式支持简单的广播通信,当消息更新时,会自动通知所有的观察者。

下面我们来看一下如何使用 TypeScript 来实现观察者模式。

5.1.1 实现代码

  1. interface Observer { 
  2.   notify: Function
  3.  
  4. class ConcreteObserver implements Observer{ 
  5.   constructor(private name: string) {} 
  6.  
  7.   notify() { 
  8.     console.log(`${this.name} has been notified.`); 
  9.   } 
  10.  
  11. class Subject {  
  12.   private observers: Observer[] = []; 
  13.  
  14.   public addObserver(observer: Observer): void { 
  15.     console.log(observer, "is pushed!"); 
  16.     this.observers.push(observer); 
  17.   } 
  18.  
  19.   public deleteObserver(observer: Observer): void { 
  20.     console.log("remove", observer); 
  21.     const n: number = this.observers.indexOf(observer); 
  22.     n != -1 && this.observers.splice(n, 1); 
  23.   } 
  24.  
  25.   public notifyObservers(): void { 
  26.     console.log("notify all the observers", this.observers); 
  27.     this.observers.forEach(observer => observer.notify()); 
  28.   } 

5.1.2 使用示例

  1. const subject: Subject = new Subject(); 
  2. const xiaoQin = new ConcreteObserver("小秦"); 
  3. const xiaoWang = new ConcreteObserver("小王"); 
  4. subject.addObserver(xiaoQin); 
  5. subject.addObserver(xiaoWang); 
  6. subject.notifyObservers(); 
  7.  
  8. subject.deleteObserver(xiaoQin); 
  9. subject.notifyObservers(); 

5.1.3 应用场景及案例

  • 一个对象的行为依赖于另一个对象的状态。或者换一种说法,当被观察对象(目标对象)的状态发生改变时 ,会直接影响到观察对象的行为。
  • RxJS Subject:https://github.com/ReactiveX/rxjs/blob/master/src/internal/Subject.ts
  • RxJS Subject 文档:https://rxjs.dev/guide/subject

5.2 发布订阅模式

在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,然后分别发送给不同的订阅者。 同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在。

在发布订阅模式中有三个主要角色:Publisher(发布者)、 Channels(通道)和 Subscriber(订阅者)。

 

在上图中,Publisher(发布者)是阿宝哥,Channels(通道)中 Topic A 和 Topic B 分别对应于 TS 专题和 Deno 专题,而 Subscriber(订阅者)就是小秦、小王和小池。

下面我们来看一下如何使用 TypeScript 来实现发布订阅模式。

5.2.1 实现代码

  1. type EventHandler = (...args: any[]) => any
  2.  
  3. class EventEmitter { 
  4.   private c = new Map<string, EventHandler[]>(); 
  5.  
  6.   // 订阅指定的主题 
  7.   subscribe(topic: string, ...handlers: EventHandler[]) { 
  8.     let topics = this.c.get(topic); 
  9.     if (!topics) { 
  10.       this.c.set(topic, topics = []); 
  11.     } 
  12.     topics.push(...handlers); 
  13.   } 
  14.  
  15.   // 取消订阅指定的主题 
  16.   unsubscribe(topic: string, handler?: EventHandler): boolean { 
  17.     if (!handler) { 
  18.       return this.c.delete(topic); 
  19.     } 
  20.  
  21.     const topics = this.c.get(topic); 
  22.     if (!topics) { 
  23.       return false
  24.     } 
  25.      
  26.     const index = topics.indexOf(handler); 
  27.  
  28.     if (index < 0) { 
  29.       return false
  30.     } 
  31.     topics.splice(index, 1); 
  32.     if (topics.length === 0) { 
  33.       this.c.delete(topic); 
  34.     } 
  35.     return true
  36.   } 
  37.  
  38.   // 为指定的主题发布消息 
  39.   publish(topic: string, ...args: any[]): any[] | null { 
  40.     const topics = this.c.get(topic); 
  41.     if (!topics) { 
  42.       return null
  43.     } 
  44.     return topics.map(handler => { 
  45.       try { 
  46.         return handler(...args); 
  47.       } catch (e) { 
  48.         console.error(e); 
  49.         return null
  50.       } 
  51.     }); 
  52.   } 

5.2.2 使用示例

  1. const eventEmitter = new EventEmitter(); 
  2. eventEmitter.subscribe("ts", (msg) => console.log(`收到订阅的消息:${msg}`) ); 
  3.  
  4. eventEmitter.publish("ts""TypeScript发布订阅模式"); 
  5. eventEmitter.unsubscribe("ts"); 
  6. eventEmitter.publish("ts""TypeScript发布订阅模式"); 

5.2.3 应用场景

  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  • 作为事件总线,来实现不同组件间或模块间的通信。
  • BetterScroll - EventEmitter:https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/events.ts
  • EventEmitter 在插件化架构的应用:https://mp.weixin.qq.com/s/N4iw3bi0bxJ57J8EAp5ctQ

六、策略模式

策略模式(Strategy Pattern)定义了一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。

 

目前在一些主流的 Web 站点中,都提供了多种不同的登录方式。比如账号密码登录、手机验证码登录和第三方登录。为了方便维护不同的登录方式,我们可以把不同的登录方式封装成不同的登录策略。

下面我们来看一下如何使用策略模式来封装不同的登录方式。

6.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

  1. interface Strategy { 
  2.   authenticate(...args: any): any
  3.  
  4. class Authenticator { 
  5.   strategy: any
  6.   constructor() { 
  7.     this.strategy = null
  8.   } 
  9.  
  10.   setStrategy(strategy: any) { 
  11.     this.strategy = strategy; 
  12.   } 
  13.  
  14.   authenticate(...args: any) { 
  15.     if (!this.strategy) { 
  16.       console.log('尚未设置认证策略'); 
  17.       return
  18.     } 
  19.     return this.strategy.authenticate(...args); 
  20.   } 
  21.  
  22. class WechatStrategy implements Strategy { 
  23.   authenticate(wechatToken: string) { 
  24.     if (wechatToken !== '123') { 
  25.       console.log('无效的微信用户'); 
  26.       return
  27.     } 
  28.     console.log('微信认证成功'); 
  29.   } 
  30.  
  31. class LocalStrategy implements Strategy { 
  32.   authenticate(username: string, password: string) { 
  33.     if (username !== 'abao' && password !== '123') { 
  34.       console.log('账号或密码错误'); 
  35.       return
  36.     } 
  37.     console.log('账号和密码认证成功'); 
  38.   } 

6.2 使用示例

  1. const auth = new Authenticator(); 
  2.  
  3. auth.setStrategy(new WechatStrategy()); 
  4. auth.authenticate('123456'); 
  5.  
  6. auth.setStrategy(new LocalStrategy()); 
  7. auth.authenticate('abao''123'); 

6.3 应用场景及案例

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • Github - passport-local:https://github.com/jaredhanson/passport-local
  • Github - passport-oauth2:https://github.com/jaredhanson/passport-oauth2
  • Github - zod:https://github.com/vriad/zod/blob/master/src/types/string.ts

七、职责链模式

职责链模式是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。

 

在公司中不同的岗位拥有不同的职责与权限。以上述的请假流程为例,当阿宝哥请 1 天假时,只要组长审批就可以了,不需要流转到主管和总监。如果职责链上的某个环节无法处理当前的请求,若含有下个环节,则会把请求转交给下个环节来处理。

在日常的软件开发过程中,对于职责链来说,一种常见的应用场景是中间件,下面我们来看一下如何利用职责链来处理请求。

7.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

  1. interface IHandler { 
  2.   addMiddleware(h: IHandler): IHandler; 
  3.   get(url: string, callback: (data: any) => void): void; 
  4.  
  5. abstract class AbstractHandler implements IHandler { 
  6.   next!: IHandler; 
  7.   addMiddleware(h: IHandler) { 
  8.     this.next = h; 
  9.     return this.next
  10.   } 
  11.  
  12.   get(url: string, callback: (data: any) => void) { 
  13.     if (this.next) { 
  14.       return this.next.get(url, callback); 
  15.     } 
  16.   } 
  17.  
  18. // 定义Auth中间件 
  19. class Auth extends AbstractHandler { 
  20.   isAuthenticated: boolean; 
  21.   constructor(username: string, password: string) { 
  22.     super(); 
  23.  
  24.     this.isAuthenticated = false
  25.     if (username === 'abao' && password === '123') { 
  26.       this.isAuthenticated = true
  27.     } 
  28.   } 
  29.  
  30.   get(url: string, callback: (data: any) => void) { 
  31.     if (this.isAuthenticated) { 
  32.       return super.get(url, callback); 
  33.     } else { 
  34.       throw new Error('Not Authorized'); 
  35.     } 
  36.   } 
  37.  
  38. // 定义Logger中间件 
  39. class Logger extends AbstractHandler { 
  40.   get(url: string, callback: (data: any) => void) { 
  41.     console.log('/GET Request to: ', url); 
  42.     return super.get(url, callback); 
  43.   } 
  44.  
  45. class Route extends AbstractHandler { 
  46.   URLMaps: {[key: string]: any}; 
  47.   constructor() { 
  48.     super(); 
  49.     this.URLMaps = { 
  50.       '/api/todos': [{ title: 'learn ts' }, { title: 'learn react' }], 
  51.       '/api/random': Math.random(), 
  52.     }; 
  53.   } 
  54.  
  55.   get(url: string, callback: (data: any) => void) { 
  56.     super.get(url, callback); 
  57.  
  58.     if (this.URLMaps.hasOwnProperty(url)) { 
  59.       callback(this.URLMaps[url]); 
  60.     } 
  61.   } 

7.2 使用示例

  1. const route = new Route(); 
  2. route.addMiddleware(new Auth('abao''123')).addMiddleware(new Logger()); 
  3.  
  4. route.get('/api/todos', data => { 
  5.   console.log(JSON.stringify({ data }, null, 2)); 
  6. }); 
  7.  
  8. route.get('/api/random', data => { 
  9.   console.log(data); 
  10. }); 

7.3 应用场景

可处理一个请求的对象集合应被动态指定。

想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。

有多个对象可以处理一个请求,哪个对象处理该请求运行时自动确定,客户端只需要把请求提交到链上即可。

八、模板方法模式

模板方法模式由两部分结构组成:抽象父类和具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

 

在上图中,阿宝哥通过使用不同的解析器来分别解析 CSV 和 Markup 文件。虽然解析的是不同的类型的文件,但文件的处理流程是一样的。这里主要包含读取文件、解析文件和打印数据三个步骤。针对这个场景,我们就可以引入模板方法来封装以上三个步骤的处理顺序。

下面我们来看一下如何使用模板方法来实现上述的解析流程。

8.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

  1. import fs from 'fs'
  2.  
  3. abstract class DataParser { 
  4.   data: string = ''
  5.   outany = null
  6.  
  7.   // 这就是所谓的模板方法 
  8.   parse(pathUrl: string) { 
  9.     this.readFile(pathUrl); 
  10.     this.doParsing(); 
  11.     this.printData(); 
  12.   } 
  13.  
  14.   readFile(pathUrl: string) { 
  15.     this.data = fs.readFileSync(pathUrl, 'utf8'); 
  16.   } 
  17.  
  18.   abstract doParsing(): void; 
  19.    
  20.   printData() { 
  21.     console.log(this.out); 
  22.   } 
  23.  
  24. class CSVParser extends DataParser { 
  25.   doParsing() { 
  26.     this.out = this.data.split(','); 
  27.   } 
  28.  
  29. class MarkupParser extends DataParser { 
  30.   doParsing() { 
  31.     this.out = this.data.match(/<\w+>.*<\/\w+>/gim); 
  32.   } 

8.2 使用示例

  1. const csvPath = './data.csv'
  2. const mdPath = './design-pattern.md'
  3.  
  4. new CSVParser().parse(csvPath); 
  5. new MarkupParser().parse(mdPath); 

8.3 应用场景

算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。

 

当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。

 

责任编辑:武晓燕 来源: 全栈修仙之路
相关推荐

2024-11-08 16:08:28

2009-06-29 18:11:40

JSP设计模式

2020-09-11 10:36:24

设计模式代码

2020-04-10 20:32:44

网络安全黑客隐私

2022-02-13 22:42:52

设计模式策略

2022-03-25 11:01:28

Golang装饰模式Go 语言

2021-04-20 22:09:13

Python编程语言

2021-10-27 10:03:16

风险管理企业ERM

2010-06-12 16:16:47

UML设计

2020-06-28 10:15:39

架构模式软件

2017-09-14 09:30:38

软件架构模式

2021-02-19 14:07:03

JavaScript编程开发

2022-07-27 20:37:45

主流企业架构

2022-06-07 08:55:04

Golang单例模式语言

2021-08-02 07:57:03

设计系统客户端

2020-10-14 13:58:14

23种设计模式速记

2015-09-14 09:31:44

结对设计

2021-10-29 09:40:21

设计模式软件

2021-08-26 08:00:00

Django开发框架

2019-01-30 15:23:02

数据库MySQLMongoDB
点赞
收藏

51CTO技术栈公众号