设计模式-前端版(持续更新中)

144 阅读16分钟

设计模式是经过验证的解决方案,为特定类型的问题提供了一种结构或框架,使得开发人员可以更快、更高效地解决问题,而不必从头开始设计。

设计模式通常描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心结构,从而使你可以在多种不同的情况下应用这个解决方案。

设计模式一般分为创建型模式、结构型模式、行为型模式。

创建型模式

特点:关注对象的创建机制,确保系统能够以一种灵活的方式创建所需的对象实例。

单例模式

什么是单例模式?

在JavaScript中,单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。在JavaScript中,单例模式可以用于多种场景,特别是当你需要控制对象的创建,确保某个类的对象在整个应用程序中只被创建一次时。

在JavaScript里实现单例模式通常有两种常见的方法:使用闭包和构造函数加静态方法。

使用闭包实现单例

闭包允许你创建私有变量和方法,同时也能保证只创建一次实例。下面是一个使用闭包实现的单例模式示例:

const Singleton = (function() {
    let instance;

    function createInstance() {
        const object = new Object("I am the single instance");
        return object;
    }

    return {
        getInstance: function() {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();

const firstInstance = Singleton.getInstance();
const secondInstance = Singleton.getInstance();

console.log(firstInstance === secondInstance); // true

在这个例子中,createInstance 函数只在首次调用 getInstance 方法时被调用,之后的调用都会返回第一次创建的实例。

使用构造函数和静态方法实现单例

另一种方法是在构造函数中检查是否已经存在一个实例,如果存在则直接返回该实例,否则创建新实例并保存起来。这种方式通常会利用静态方法来提供对单例的访问。

class Singleton {
    constructor() {
        if (!Singleton.instance) {
            Singleton.instance = this;
        }
        return Singleton.instance;
    }

    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }

    someMethod() {
        console.log("This is the only instance of Singleton.");
    }
}

const firstInstance = Singleton.getInstance();
const secondInstance = Singleton.getInstance();

console.log(firstInstance === secondInstance); // true

在这个例子中,getInstance 是一个静态方法,可以直接通过类来调用,而不需要创建一个新的实例。当尝试创建一个新的 Singleton 实例时,构造函数会检查是否存在一个实例,如果存在则返回已有的实例。

示例:创建一个日志记录器单例,这个日志记录器可以用来记录日志信息,并且在整个应用中只需要一个实例。

class Logger{
    constructor(){
        if(Logger.instance){
            throw new Error('Logger instance already exists.')
        }
        // 初始化日志记录器
        this.logs = []
        Logger.instance = this;
    }
    log(message){
        const timestamp = new Date().toISOString()
        this.logs.push({timestamp,message})
        console.log(`${timestamp} - ${message}`)
    }
    
    static getInstance(){
        if(!Logger.instance){
            Logger.instance = new Logger()
        }
        return Logger.instance;
    }
}

// 使用单例
const logger1 = Logger.getInstance();
logger.log('this is the first log entry.')

const logger2 = Logger.getInstance();
logger2.log('this is the second log entry')

// 验证两个实例是否相同
console.log(logger1 === logger2)

// 获取所有日志
console.log(logger1.logs);

代码解释:

  1. 构造函数:
    • 构造函数内部首先检查 Logger.instance 是否已经存在,如果存在,则抛出错误,阻止再次创建新的实例。
    • 如果不存在,则初始化日志数组 this.logs 并将当前实例赋值给 Logger.instance
  2. log 方法:
    • 这个方法接收一条消息作为参数,并添加时间戳后将消息添加到日志数组中。
    • 同时,也将消息输出到控制台。
  3. getInstance 静态方法:
    • 这个静态方法负责检查实例是否存在,如果不存在则创建一个新的实例,并返回这个实例。
    • 由于每次调用 getInstance 都会返回相同的实例,因此实现了单例模式。

使用单例做了哪些操作?

  • 我们首先通过 Logger.getInstance() 创建了一个日志记录器实例,并记录了一条日志。
  • 接着,我们再次通过 Logger.getInstance() 获取了日志记录器实例,并记录了第二条日志。
  • 最后,我们验证了两次获取的实例是否相同,并打印了所有日志。

在前端项目中,有哪些使用场景?

1. 全局配置管理器
  • 需求:
    • 应用程序需要一个统一的地方来管理和读取配置信息(如API端点、默认设置等)。
    • 配置信息一旦加载就不应更改,以确保一致性。
  • 实现:
    • 创建一个单例配置管理器,它在启动时加载配置,并在整个应用中提供一个全局访问点。
2. 数据库连接管理器
  • 需求:
    • 如果你的前端应用需要与后端服务器交互(例如通过WebSockets),确保数据库连接只被创建一次,以避免资源浪费。
  • 实现:
    • 使用单例模式创建一个连接管理器,它负责创建和维护唯一的数据库连接实例。
3. 日志记录器
  • 需求:
    • 应用程序需要记录调试信息、错误和其他重要事件。
    • 日志记录器应能够接受不同级别的日志消息,并在必要时写入文件或发送到远程服务器。
  • 实现:
    • 创建一个单例日志记录器,它能够记录不同级别的日志,并且可以在任何地方被调用。
4. 存储管理器
  • 需求:
    • 应用程序需要一个统一的接口来处理本地存储(如localStorage或sessionStorage)。
    • 存储管理器应提供增删改查的功能,并确保数据的一致性和安全性。
  • 实现:
    • 使用单例模式创建一个存储管理器,它封装了所有与本地存储相关的操作。
5. 状态管理器
  • 需求:
    • 在复杂的前端应用中,状态管理变得非常重要。
    • 状态管理器应该是一个全局可访问的对象,它可以被多个组件使用,以确保数据的一致性。
  • 实现:
    • 创建一个单例状态管理器,它提供了更新和查询状态的方法。

示例代码:状态管理器单例

下面是一个简单的状态管理器单例实现的例子

class StateManager {
  constructor() {
    if (StateManager.instance) {
      throw new Error('State Manager instance already exists.');
    }

    // 初始化状态
    this.state = {};

    StateManager.instance = this;
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
  }

  getState() {
    return this.state;
  }

  static getInstance() {
    if (!StateManager.instance) {
      StateManager.instance = new StateManager();
    }
    return StateManager.instance;
  }
}

// 使用状态管理器
const stateManager1 = StateManager.getInstance();
stateManager1.setState({ user: 'Alice' });

const stateManager2 = StateManager.getInstance();
stateManager2.setState({ theme: 'dark' });

// 获取状态
console.log(stateManager1.getState());  // 输出 { user: 'Alice', theme: 'dark' }
console.log(stateManager2.getState());  // 输出 { user: 'Alice', theme: 'dark' }

解释:

  • 构造函数:
    • 构造函数内部检查 StateManager.instance 是否已经存在,如果存在,则抛出错误,阻止再次创建新的实例。
    • 如果不存在,则初始化状态对象 this.state 并将当前实例赋值给 StateManager.instance
  • setState 和 getState 方法:
    • setState 方法允许更新状态,它接收一个新状态对象并合并到现有状态中。
    • getState 方法返回当前的状态对象。
  • getInstance 静态方法:
    • 这个静态方法负责检查实例是否存在,如果不存在则创建一个新的实例,并返回这个实例。
    • 由于每次调用 getInstance 都会返回相同的实例,因此实现了单例模式。

这种状态管理器单例模式可以用于管理全局状态,确保在多个组件之间共享状态时的一致性。

这两种方法都能有效地实现单例模式,具体选择哪一种取决于你的需求和代码风格。

工厂模式

什么是工厂模式

工厂模式是一种创建型设计模式,它提供了一个创建对象的接口,但允许子类决定实例化哪一个类。使用工厂模式可以确保客户端不必关心具体产品类是如何被创建、组合的。

什么时候考虑使用工厂模式

  1. 当一个类不知道它所必须创建的对象的类的时候
  2. 当一个类希望由它的子类来指定它所创建的对象的时候
  3. 为了实现一个“系列”的相关或相互依赖的对象而无需规范它们具体的类

使用场景

  1. 组件工厂

    • 在构建复杂的UI组件时,可以使用工厂模式来创建不同类型的组件,这样可以根据不同的配置生成不同样式的组件。
  2. 对象池管理

    • 当需要频繁创建和销毁类似对象时(例如游戏中的敌人、子弹等),可以使用工厂模式配合对象池技术来提高性能。
  3. 配置驱动的应用

    • 如果你的应用需要根据不同的配置来创建不同功能的实例,那么可以使用工厂模式来动态地选择合适的构造函数。

原理

工厂模式的核心思想是将对象的创建过程从使用对象的地方解耦出来。

这样做的好处包括:

  1. 代码的解耦:对象的创建逻辑被封装在工厂内,使得客户端代码不必知道具体的创建逻辑,也不必了解对象的确切类型。
  2. 易于扩展:当需要增加新的对象类型时,只需要修改工厂的实现,而不必修改客户端代码。
  3. 更好的代码组织:工厂模式可以帮助管理代码结构,特别是当项目变得庞大且有多个不同类型的对象需要创建时。

分类

工厂模式可以进一步细分为以下几种类型

  • 简单工厂(也称静态工厂):由一个类负责创建所有实例。通常,这个类有一个静态方法,接受参数并决定创建哪个类的实例。简单工厂的缺点是它违反了开闭原则,因为每当需要一个新的对象类型时,都需要修改工厂类。
  • 工厂方法(Factory Method):定义一个创建对象的接口,但允许子类决定实例化哪一个类。工厂方法模式使一个类的实例化延迟到其子类。
  • 抽象工厂(Abstract Factory):提供一个接口来创建一系列相关或相互依赖的对象,而无需指定它们具体的类。抽象工厂模式通常用于创建一组对象,这些对象属于同一主题或系列。

示例

简单工厂
// 简单工厂模式示例

function createShape(type) {
    if (type === 'circle') {
        return new Circle();
    } else if (type === 'square') {
        return new Square();
    }
}

class Circle {
    draw() {
        console.log('Drawing a circle.');
    }
}

class Square {
    draw() {
        console.log('Drawing a square.');
    }
}

// 使用工厂创建形状
const shape1 = createShape('circle');
shape1.draw(); // 输出 "Drawing a circle."

const shape2 = createShape('square');
shape2.draw(); // 输出 "Drawing a square."

在这个例子中,createShape 函数充当了一个工厂,根据传入的类型参数创建不同的形状对象。客户端代码只需调用 createShape 函数并传入适当的参数即可,而不需要知道具体的形状类是如何实现的。

工厂方法模式

首先,我们定义一个抽象基类 Shape,它包含一个抽象方法 draw 和一个工厂方法 createShape。然后,我们创建两个具体的图形类 CircleSquare,它们继承自 Shape 并实现了各自的 draw 方法和工厂方法 createShape

注意:

  1. 当你在一个子类中定义了一个与父类同名的静态方法时,实际上是在子类中覆盖(重写)了父类的静态方法。这是完全可行的,并且与类的实例方法覆盖规则类似,但只限于静态上下文。
  2. 子类覆盖父类的静态方法仅影响该特定子类的行为。其他同样继承自同一个父类的子类不受影响,除非它们也选择覆盖该静态方法。
// 抽象基类 Shape
class Shape {
    // 抽象方法 draw
    draw() {
        throw new Error('Method not implemented.');
    }

    // 工厂方法,具体子类需要实现
    static createShape() {
        throw new Error('Method not implemented.');
    }
}

// 具体类 Circle
class Circle extends Shape {
    draw() {
        console.log('Drawing a circle.');
    }

    // 实现工厂方法
    static createShape() {
        return new Circle();
    }
}

// 具体类 Square
class Square extends Shape {
    draw() {
        console.log('Drawing a square.');
    }

    // 实现工厂方法
    static createShape() {
        return new Square();
    }
}

// 使用工厂方法创建形状
const circle = Circle.createShape();
circle.draw(); // 输出 "Drawing a circle."

const square = Square.createShape();
square.draw(); // 输出 "Drawing a square."

在这个例子中,CircleSquare 类都继承了 Shape 类,并覆盖了 draw 方法和 createShape 静态方法。通过调用各自类的 createShape 方法,我们可以创建出具体的 CircleSquare 对象,而不需要直接使用 new 关键字和构造函数。

这种模式的优点在于,如果我们需要添加新的图形类型,我们只需要创建一个新的图形类,继承 Shape 类并实现相应的工厂方法,而不需要修改现有的代码。这遵循了开闭原则,即软件实体应该对扩展开放,对修改关闭。

抽象工厂

我们将创建一个抽象工厂来生成不同类型的汽车。首先,我们定义一个抽象基类 Car,然后创建具体的汽车子类如 BMWMercedes。接着,我们定义一个 CarFactory 抽象工厂接口,并实现两个具体工厂 LuxuryCarFactorySportsCarFactory

// 抽象产品基类 Car
class Car {
  constructor(brand, model) {
    this.brand = brand;
    this.model = model;
  }
  getInfo() {
    return `${this.brand} ${this.model}`;
  }
}

// 具体产品 BMW
class BMW extends Car {
  constructor(model) {
    super('BMW', model);
  }
}

// 具体产品 Mercedes
class Mercedes extends Car {
  constructor(model) {
    super('Mercedes', model);
  }
}

// 抽象工厂接口 CarFactory
class CarFactory {
  createCar(model) {
    throw new Error('Method "createCar" must be implemented.');
  }
}

// 具体工厂 LuxuryCarFactory
class LuxuryCarFactory extends CarFactory {
  createCar(model) {
    if (model === 'X5') {
      return new BMW('X5');
    } else if (model === 'SClass') {
      return new Mercedes('SClass');
    } else {
      throw new Error('Model not supported by this factory.');
    }
  }
}

// 具体工厂 SportsCarFactory
class SportsCarFactory extends CarFactory {
  createCar(model) {
    if (model === 'M3') {
      return new BMW('M3');
    } else if (model === 'AMG GT') {
      return new Mercedes('AMG GT');
    } else {
      throw new Error('Model not supported by this factory.');
    }
  }
}

// 使用工厂创建产品
const luxuryFactory = new LuxuryCarFactory();
const sportsFactory = new SportsCarFactory();

const bmwX5 = luxuryFactory.createCar('X5');
console.log(bmwX5.getInfo()); // 输出: BMW X5

const mercedesAMGGT = sportsFactory.createCar('AMG GT');
console.log(mercedesAMGGT.getInfo()); // 输出: Mercedes AMG GT

在这个例子中,CarFactory 是抽象工厂接口,LuxuryCarFactorySportsCarFactory 是具体工厂,它们实现了 createCar 方法来生产不同的汽车实例。客户代码通过调用工厂方法来获取产品,而不需要知道具体的产品类。这样就达到了解耦的目的,使得增加新的汽车类型或工厂变得容易。

行为型模式

特点:关注类之间的职责分配和算法的封装,以便于复用和可扩展性

观察者模式

什么是观察者模式

在JavaScript中,观察者模式(Observer Pattern)是一种常用的设计模式,它定义了对象间的一种一对多的依赖关系,允许多个观察者对象同时监听某一个主题对象。当主题对象的状态发生改变时,所有登记过的观察者都会收到通知,并根据需要做出相应的处理。

观察者模式的组成

观察者模式通常包含以下几个关键组件:

  1. Subject(主题)
    • 主题对象维护了一个观察者列表。
    • 当主题的状态发生改变时,它会遍历观察者列表并调用每个观察者的update()方法来通知他们。
  2. Observer(观察者)
    • 观察者对象实现了一个update()方法,用于接收主题的通知。
    • 观察者可以订阅多个主题,并且当主题状态改变时,观察者会自动被通知。

数据传递的方式

观察者模式可以进一步细分为两种类型:

  • Push(推送)

    • 在推送模型中,主题(Subject)会直接将最新的数据发送给观察者(Observers)。这种方式适用于当观察者需要立即获得最新状态更新的情况。
    • 使用场景:
      • 当观察者需要的主题状态信息明确且不变时。
      • 当观察者需要的是简单的数据更新而不需要额外的上下文信息时。
  • Pull(拉取)

    • 在拉取模型中,当状态改变时,主题仅通知观察者状态已改变,但不会发送实际的数据。相反,观察者必须主动向主题请求最新的数据。
    • 使用场景:
      • 当观察者需要的数据可能随时间变化或者比较复杂时。
      • 当观察者需要从多个来源获取数据来构建自己的状态时。
      • 当观察者可能只关心部分数据或需要自己决定何时获取数据时。

示例:

推送模型

class Subject {
    constructor() {
        this.observers = [];
    }

    addObserver(observer) {
        this.observers.push(observer);
    }

    removeObserver(observer) {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers.splice(index, 1);
        }
    }

    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

class Observer {
    update(data) {
        console.log(`Received data: ${data}`);
    }
}

const subject = new Subject();
const observer = new Observer();

subject.addObserver(observer);

// 更新主题状态,并推送给观察者
subject.notify("New data");

拉取模型

class Subject {
    constructor() {
        this.state = null;
        this.observers = [];
    }

    setState(newState) {
        this.state = newState;
        this.notify();
    }

    addObserver(observer) {
        this.observers.push(observer);
    }

    removeObserver(observer) {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
            this.observers.splice(index, 1);
        }
    }

    notify() {
        this.observers.forEach(observer => observer.update());
    }
}

class Observer {
    constructor(subject) {
        this.subject = subject;
        this.subject.addObserver(this);
    }

    update() {
        console.log(`Retrieved state: ${this.subject.state}`);
    }
}

const subject = new Subject();
const observer = new Observer(subject);

// 更新主题状态
subject.setState("New state");

总结来说,选择推送模型还是拉取模型取决于你的具体需求。如果你想要更灵活地控制数据流动并且避免过多的数据耦合,那么拉取模型可能更适合你;如果数据更新相对简单且需要即时推送,那么推送模型则更为适用。

使用场景

  1. 事件驱动的应用程序
    • 使用场景:
      • 当你需要为用户提供交互式体验时,比如按钮点击、表单提交等。
      • 当你希望将UI更新逻辑与业务逻辑分离时。
    • 示例:
      • 在一个表单中,当用户填写完表单并点击提交按钮时,可以触发一个观察者来处理表单数据的验证和提交。
  2. 数据绑定和响应式编程
    • 使用场景:
      • 当你需要实现数据驱动的视图更新时。
      • 当你希望在数据发生变化时自动更新相关的视图元素。
    • 示例:
      • Vue.js 和 Angular 这样的框架使用观察者模式来跟踪数据变化并自动更新DOM。
  3. 状态管理
    • 使用场景:
      • 当你的应用程序具有复杂的共享状态,需要多个组件访问并更新这个状态时。
    • 示例:
      • 使用 Redux 或 MobX 进行状态管理时,当状态发生变化时,相关的组件可以接收到通知并重新渲染。
  4. 模块化和解耦
    • 使用场景:
      • 当你希望降低不同模块之间的耦合度时。
      • 当你需要实现模块间通信时。
    • 示例:
      • 在一个电商网站中,购物车模块可能需要监听商品详情页面的商品添加事件,以便实时更新购物车的数量。
  5. 异步请求
    • 使用场景:
      • 当你执行异步请求(如HTTP请求),并在数据返回后需要通知多个组件更新视图时。
    • 示例:
      • 当一个组件发起API调用以获取数据时,它可以注册为观察者以接收数据更新的通知。
  6. 微前端架构
    • 使用场景:
      • 在微前端架构中,各个子应用可能需要共享某些全局状态或事件。
    • 示例:
      • 当主应用需要与子应用进行通信时,可以使用观察者模式来实现事件的发布和订阅。
  7. 跨组件通信
    • 使用场景:
      • 当你有多个组件需要响应相同的事件时。
      • 当组件间的通信较为复杂,需要减少组件间的直接引用时。
    • 示例:
      • 在一个大型的表格组件中,当某个单元格的数据发生变化时,需要通知其他组件(如过滤器、分页控件)进行相应的更新。
  8. 性能优化
    • 使用场景:
      • 当你需要优化应用性能,减少不必要的重绘或重排时。
    • 示例:
      • 在一些大型的列表视图中,只有可见区域内的元素需要更新,而非整个列表。