[Pattern] 设计模式

设计模式

  • 设计模式基本介绍
  • 单例模式
  • 工厂模式

基本介绍

“模式”是由模式之父 Christopher Alexander(克里斯托弗·亚历山大)提出。

image-20210803160008527

1991 年-1992 年之间,由 GoF:

  • Erich Gamma
  • Richard Helm
  • Ralph Johnson
  • John Vlissides

他们四个出版了一本书《设计模式:可复用面向对象软件的基础》,在书里面一共提出了 23 种设计模式:

  • 创建型模式:提供一种创建对象的机制
  • 结构型模式:对象和对象之间,以及对象和类之间如何进行组装
  • 行为型模式:对象之间如何进行沟通和职责分配。

23 种设计模式如下表:

image-20210803160250301

民间有一种偏见:像 JS 这种非标准的面向对象语言,使用设计模式有一点画蛇添足,这其实是不太准确的,无论什么范式的语言,要解决的问题都是相似,只是不同的语言,因为特性不一样,会使用到的设计模式有所不同。

单例模式

定义:保证一个类,仅有一个实例。

  • 后端:线程池、缓冲池、全局缓存
  • 前端:登录窗口,无论用户点击多少次,只有有一个窗口、浏览器的 window 对象

使用 JS 实现单例模式

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log("Hello");
  }
}

// 书写一个辅助函数
function getSingle(fn) {
  let instance = null; // 用于存储第一个实例对象
  // 这里使用到了闭包来保存
  return function (...args) {
    if (instance !== null) {
      // 如果实例对象已经存在,则直接返回
      return instance;
    }
    // 代码走到这里,说明是第一次实例化
    instance = new fn(...args);
    return instance;
  };
}

const SinglePerson = getSingle(Person);

const p1 = new SinglePerson("张三", 18);
const p2 = new SinglePerson("李四", 20);

console.log(p1 === p2); // true

使用 TS 来实现单例模式:

class Person {
  private static instance: Person;
  private name: string;
  private age: number;

  // 要让该类是一个单例类,一定要将构造函数私有化
  private constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public static getInstance(name: string, age: number): Person {
    if (!Person.instance) {
      Person.instance = new Person(name, age);
    }
    return Person.instance;
  }
}

const p1 = Person.getInstance("zhangsan", 18);
const p2 = Person.getInstance("lisi", 20);
console.log(p1 === p2); // true

利用Typescript实现的时候,注意constructor必须是私有的,这样才能防止调用

工厂模式

工厂模式属于创建型模式:

在创建对象的时候,不会对客户端暴露创建的逻辑,客户端通过工厂所提供的统一接口来创建对象。

工厂模式里面有两个概念:

  • 产品:具体创建出来的对象。
  • 工厂:负责生产产品的。(创建对象的)

工厂模式如果要细分:

  • 简单工厂
  • 标准的工厂模式
  • 抽象工厂

简单工厂

在简单工厂模式中,存在一个单独的工厂类,统一通过工厂类来创建对象。客户端通过传递不同的参数来指定对象的类型。

首先是不使用工厂模式:

// 不用工厂模式
interface IProduct {
  use(): void;
}

// 产品A
class ProductA implements IProduct {
  use() {
    console.log("ProductA");
  }
}

// 产品B
export class ProductB implements IProduct {
  use() {
    console.log("ProductB");
  }
}

// 对应的客户端代码,实际的去和具体的产品打交道
// 创建具体的产品
const p1 = new ProductA();
const p2 = new ProductB();

p1.use();
p2.use();

在上面的代码模式中,具体到客户端,创建产品的逻辑和使用产品的逻辑是耦合到一起的,并且客户端能够清楚的知道创建产品的逻辑。

简单工厂模式如下:

interface IProduct {
  use(): void;
}

// 产品A
class ProductA implements IProduct {
  use() {
    console.log("ProductA");
  }
}

// 产品B
export class ProductB implements IProduct {
  use() {
    console.log("ProductB");
  }
}

export class SimpleFactory {
  static createProduct(type: string): IProduct {
    switch (type) {
      case "A": {
        return new ProductA();
        break;
      }
      case "B": {
        return new ProductB();
        break;
      }
      default: {
        throw new Error("type error");
      }
    }
  }
}

// 对应的客户端代码,实际的去和具体的产品打交道
// 创建具体的产品
const p1 = SimpleFactory.createProduct("A");
const p2 = SimpleFactory.createProduct("B");

p1.use();
p2.use();

这样的好处在于,创建产品的逻辑和使用产品的逻辑分离开了。

  • 创建产品:由工厂来创建,具体产品是如何创建出来的,对客户端也讲也是一个黑盒。
  • 客户端通过工厂提供的方法来得到产品,然后使用产品

简单工厂模式会存在的问题 ?

目前我们将创建产品的逻辑全部放入到了一个工厂,这里会导致如果存在要新增产品,那么需要动工厂相关的代码。

工厂方法模式

工厂方法模式和前面的简单工厂模式的区别在于:

一个产品就对应一个工厂。假设有产品 A、B、C,那就就会存在对应的工厂 A、B、C,现在假设要新增一个产品 D,那么前面的工厂是不会受到任何影响的,只需要新增工厂 D 即可。

来看一下工厂方法的类图:

image-20240322104208915

  1. 产品接口:规范各种产品的。
  2. 具体的产品:无论产品是什么,反正需要实现 Product 接口。
  3. 工厂的接口:因为现在工厂有多个,所以需要对这些工厂进行统一的规范。
  4. 具体的工厂:需要实现工厂接口。

假设有一个日志系统,需要根据不同的环境来记录日志,如果使用前面介绍的简单工厂模式:

// 产品的接口
interface Logger {
  log(message: string): void;
}

// 具体的产品,实现了 Logger 接口
// 以文件的信息记录日志
class FileLogger implements Logger {
  log(message: string) {
    console.log(`File log: ${message}`);
  }
}

// 也是一个具体的产品,实现了 Logger 接口
// 以数据库的形式来记录日志
class DatabaseLogger implements Logger {
  log(message: string) {
    console.log(`Database log: ${message}`);
  }
}

// 简单工厂
class LoggerFactory {
  static createLogger(type: string): Logger {
    switch (type) {
      case "file":
        return new FileLogger();
      case "database":
        return new DatabaseLogger();
      default:
        throw new Error("Unknown logger type");
    }
  }
}

const logger = LoggerFactory.createLogger("file");
logger.log("This is a message.");

types.ts

export interface ILogger {
  log(message: string): void;
}

export interface ILoggerFactory {
    createLogger(): ILogger;
}

FileLogger.ts

import { ILogger } from "./ILogger";

export class FileLogger implements ILogger {
  log(message: string) {
    console.log("log to file: ", message);
  }
}

DatabaseLogger.ts

import { ILogger } from "./ILogger";

export class DatabaseLogger implements ILogger {
  log(message: string) {
    console.log("DatabaseLogger to file: ", message);
  }
}

FileFactory.ts

import { ILogger, ILoggerFactory } from "./ILogger";
import { FileLogger } from "./FileLogger";

// FileFactory 这种工厂专门负责生产 FileLogger
export class FileFactory implements ILoggerFactory {
  createLogger(): ILogger {
    return new FileLogger();
  }
}

DatabaseFactory.ts

import { ILogger, ILoggerFactory } from "./ILogger";
import { DatabaseLogger } from "./DatabaseLogger";

// DatabaseFactory 这种工厂专门负责生产 DatabaseLogger
export class DatabaseFactory implements ILoggerFactory {
  createLogger(): ILogger {
    return new DatabaseLogger();
  }
}

Client.ts

import { FileFactory } from './FileFactory';

const fileFactory = new FileFactory();

const fileLogger = fileFactory.createLogger();

const dbFactory = new DatabaseFactory();

const dbLogger = dbFactory.createLogger();

观察者模式

观察者模式的定义:

允许你定义一种订阅机制,之后在事件发生的时候,通知多个观察该对象的观察者,多个观察者自发的进行一些行为。

例如生活中的一个例子:

假如你有两种类型的对象: 顾客和商店 。顾客对某个特定品牌的产品非常感兴趣(例如最新型号的手机),而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。但如果商品尚未到货时,绝大多数来到商店的顾客都会空手而归。另一方面,每次新产品到货时,商店可以向所有顾客发送邮件(可能会被视为垃圾邮件)。这样,部分顾客就无需反复前往商店了,但也可能会惹恼对新产品没有兴趣的其他顾客。

image-20240322154631735

这种问题在生活中也非常常见。在生活中一般采取的做法是由用户主动留下联系方式,当用户留下联系方式之后,该用户就变成了一个观察者,当商店开始出售新的产品的时候,就通知所有的观察者,所有的观察者开始采取之前定义好的行为。

观察者模式的类图结构:

image-20240322155150797
  1. 发布者:像所有的观察者发布信息,一般发布者内部会有一个类似于 subscribers 的数组,该数组用于存储所有的观察者。
  2. 当新的事件发生的时候,发布者会去遍历 subscribers 数组,通知所有的观察者做相应的事情
  3. 观察者基本的一个接口,定义观察者统一的行为
  4. 具体的观察者
  5. 所有的观察者根据 Subscriber 接口的定义,都会有一个 update 方法
  6. 客户端会分别创建发布者和订阅者,执行相应的逻辑

types.ts

// 观察者的统一接口
export interface IObserver {
  update(message: string): void;
}

Publisher.ts

import { IObserver } from "./IObserver";

// 发布者
export class Publisher {
  // 该数组用于存储观察者
  private observers: IObserver[] = [];

  // 添加观察者
  public addObserver(observer: IObserver): void {
    this.observers.push(observer);
  }

  // 删除观察者
  public removeObserver(observer: IObserver): void {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  // 通知所有的观察者
  public notifyObservers(message: string): void {
    this.observers.forEach((observer) => {
      observer.update(message);
    });
  }
}

log.ts

import { IObserver } from "./IObserver";

export class LogNotificationListener implements IObserver {
  public update(message: string): void {
    console.log(`Logging notification: ${message}`);
  }
}

email.ts

import { IObserver } from "./IObserver";

export class EmailNotificationListener implements IObserver {
  public update(message: string): void {
    console.log(`Sending email notification: ${message}`);
  }
}

client.ts

import { Publisher } from "./Publisher";
import { EmailNotificationListener } from "./Email";
import { LogNotificationListener } from "./Log";

// 创建发布中心
const newPublisher = new Publisher();

// 创建两个观察者
const logObserver = new LogNotificationListener();
const emailObserver = new EmailNotificationListener();

// 接下来,我们将观察者添加到发布中心,相当于留下你的联系方式给商家
newPublisher.addObserver(logObserver);
newPublisher.addObserver(emailObserver);

// 最后,我们发布一个消息,相当于商家发布了一条消息
newPublisher.notifyObservers("Hello World!");
posted @   Zhentiw  阅读(2)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2024-02-23 [Rust] Instantiating Classic, Tuple, and Unit structs
2024-02-23 [Rust] Accessing specific values in a tuple by index
2024-02-23 [Rust] Create an array of numbers by using Range
2024-02-23 [Rust Unit testing] test should_panic
2024-02-23 [Rust Unit testing] assert & assert_eq
2024-02-23 [Rust] Implicitly returning values from functions
2024-02-23 [Rust] Constants
点击右上角即可分享
微信分享提示