一篇文章,让你看懂前端需要掌握的基本设计模式

82 阅读9分钟

概述

本篇文章将介绍几种最常见的几种设计模式,包括对这几种设计模式的基本使用、优化、对比。

工厂模式

工厂模式是一种对象创建型设计模式,旨在使代码更易于维护、可扩展和重用。

在工厂模式中,我们通过定义一个共同的接口来创建对象。这个接口可以是一个抽象类或者是一个接口,并包含用于创建对象的方法声明。具体的工厂将负责创建具体的对象,并实现这个接口。客户端将与这个接口进行交互,而不必关心具体的对象是如何创建的。

工厂模式有多种变体:简单工厂模式、工厂方法模式和抽象工厂模式。

  • 简单工厂模式:定义一个工厂类,它可以根据客户端请求返回不同的产品对象。
  • 工厂方法模式:定义一个创建对象的接口,但将具体对象的创建推迟到子类中,以此实现解耦合。
  • 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。

例子

// 定义 Shape 工厂
function ShapeFactory() {}
​
// 向工厂原型添加 create 方法
ShapeFactory.prototype.create = function(type) {
  let shape;
​
  switch (type) {
    case "circle":
      shape = new Circle();
      break;
    case "square":
      shape = new Square();
      break;
    case "triangle":
      shape = new Triangle();
      break;
    default:
      throw new Error("Invalid shape type.");
  }
​
  return shape;
}
​
// 定义形状构造函数
function Circle() {
  this.type = "circle";
}
​
function Square() {
  this.type = "square";
}
​
function Triangle() {
  this.type = "triangle";
}
​
// 使用 Shape 工厂创建不同类型的形状实例
const shapeFactory = new ShapeFactory();
​
const circle = shapeFactory.create("circle");
console.log(circle.type); // 输出 "circle"const square = shapeFactory.create("square");
console.log(square.type); // 输出 "square"const triangle = shapeFactory.create("triangle");
console.log(triangle.type); // 输出 "triangle"

该代码使用了简单工厂模式,它允许我们通过向工厂传入不同的参数来创建不同的对象实例。在这个示例中,我们定义了一个 ShapeFactory 构造函数作为工厂,并向其原型添加了一个 create 方法,该方法接收一个字符串参数并返回相应的不同类型的形状实例。

形状实例由不同的构造函数(例如 CircleSquareTriangle)创建,在这些函数中,我们为每个实例添加了一个 type 属性,以便我们可以在控制台中打印出不同形状的类型。

单例模式

单例模式是一种创建型设计模式,它保证该类只有一个实例存在,并提供一个全局访问点。

单例模式通常用于控制某些系统内部资源的唯一访问权,例如数据库连接、日志记录器等。由于这些资源在系统中只需要存在一个实例,为了避免多个实例之间的冲突,我们可以使用单例模式来确保只有一个实例。

let singleton = (function() {
  let instance;
​
  function createInstance() {
    // 在此处编写需要实例化的代码...
    let obj = new Object("I am the instance");
    return obj;
  }
​
  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();
​
// Example usage:
let instance1 = singleton.getInstance();
let instance2 = singleton.getInstance();
​
console.log(instance1 === instance2); // Output: true

在此示例中,我们通过立即调用函数表达式来创建了一个闭包。闭包内部定义了 createInstance 函数用于创建实例对象,同时定义了一个变量 instance,它保存了已经创建的实例对象。

getInstance 方法用于获取单例模式的唯一实例。当第一次请求实例对象时,instance 将是 null,因此我们会创建一个新的实例对象并将其存储在变量 instance 中。在随后的请求中,我们直接返回存储在 instance 中的对象。这样就保证了系统中只存在一个实例对象,并且可以在任何需要该对象的地方获得相同的实例。

最后,在应用程序中可以像上面的示例中那样使用单例模式,即通过 getInstance 方法获取单个实例对象。

饿汉式

class Singleton {
  constructor() {
    if (typeof Singleton.instance === 'object') {
      return Singleton.instance;
    }
    this.property1 = 'value1';
    this.property2 = 'value2';
    Singleton.instance = this;
    return this;
  }
}
​
const instance1 = new Singleton();
const instance2 = new Singleton();
​
console.log(instance1 === instance2); // true

这种方式是使用一个立即执行的闭包创建一个私有静态变量来存储类的唯一实例。当第一次创建实例时,将类的实例存储到静态变量中,并在以后的每个调用中返回它。

懒汉式

class Singleton {
  constructor() {
    if (!Singleton.instance) {
      this.property1 = 'value1';
      this.property2 = 'value2';
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
}
​
const instance1 = new Singleton();
const instance2 = new Singleton();
​
console.log(instance1 === instance2); // true

这种方式是在创建实例时不立即初始化它,而是在第一次调用实例时才进行初始化。如果之前已经创建了一个实例,那么直接返回该实例。

特性饿汉式单例模式懒汉式单例模式
线程安全
性能较差(无论是否需要用到该实例,都会创建)好(只在需要的时候才会创建)
实现难度简单复杂

饿汉式单例模式是线程安全的,因为它在类加载阶段就已经创建了实例,因此,在多线程环境下,为确保线程安全,建议使用饿汉式单例模式;对于性能敏感的场景,则更适合使用懒汉式单例模式

观察者模式

观察者模式是一种行为型设计模式,它允许在对象之间建立一种一对多的依赖关系,使得当一个对象状态发生改变时,所有依赖于它的对象都会自动收到通知并作出相应的响应。观察者模式的优点在于可以使得对象之间的耦合度降低,被观察者和观察者之间的交互方式灵活自由。但是,观察者模式会导致每个具体观察者实例都需要注册到被观察者实例上,这样会导致程序的运行效率降低。

结构

  • Subject(主题):维护一个观察者列表,提供方法用于添加或删除观察者。当主题状态发生改变时,负责通知观察者。
  • Observer(观察者):定义一个更新接口,使得主题能够向它发送通知。
  • ConcreteSubject(具体主题):将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有注册过的观察者发出通知。
  • ConcreteObserver(具体观察者):存储一个具体主题和观察者状态,实现 Observer 的更新接口以使自身状态与主题的状态保持一致。

例子

例如,在一个新闻站点中,有多个用户订阅了各自感兴趣的新闻类型。每当有相关新闻,该新闻站点将根据新闻类型通知相应的订阅用户。这里,新闻站点就是主题,订阅用户就是观察者,而不同的新闻类型就是具体主题。具体观察者则代表了订阅用户的信息。

// 创建主题对象
function Subject() {
  this.observers = []; // 观察者数组
}
​
// 添加观察者
Subject.prototype.addObserver = function(observer) {
  this.observers.push(observer);
};
​
// 删除观察者
Subject.prototype.removeObserver = function(observer) {
  var index = this.observers.indexOf(observer);
  if (index > -1) {
    this.observers.splice(index, 1);
  }
};
​
// 通知所有的观察者更新状态
Subject.prototype.notifyObservers = function() {
  for (var i = 0; i < this.observers.length; i++) {
    this.observers[i].update();
  }
};
​
// 创建观察者对象
function Observer(name, subject) {
  this.name = name;
  this.subject = subject;
}
​
// 观察者更新操作
Observer.prototype.update = function() {
  console.log(this.name + " has been notified.");
};
​
// 测试代码
var subject = new Subject();
​
var observerA = new Observer("A", subject);
var observerB = new Observer("B", subject);
var observerC = new Observer("C", subject);
​
subject.addObserver(observerA);
subject.addObserver(observerB);
subject.addObserver(observerC);
​
subject.notifyObservers(); // 输出:A has been notified. B has been notified. C has been notified.
​
subject.removeObserver(observerA);
​
subject.notifyObservers(); // 输出:B has been notified. C has been notified.

以上代码中,我们定义了主题对象Subject和观察者对象Observer。在Subject中,我们定义了添加、删除、通知观察者的方法,并维护一个观察者数组。在Observer中,我们定义了更新状态的操作,并在构造函数中传入主题对象,以便更新时能够从主题获取到最新的状态。

通过创建SubjectObserver对象,并调用相应的方法,我们可以实现观察者模式的基本功能,即在主题状态变化时通知所有观察者进行更新操作。

发布/订阅模式

发布/订阅模式是一种信息传递机制。它的核心原理是:一个被称为“发布者”(publisher)的组件向多个被称为“订阅者”(subscriber)的组件发送事件,这些订阅者将在接收到事件后执行相应的操作。

在这种模式中,发布者和订阅者之间是低耦合的,它们可以进行交互。发布者不需要知道订阅者的具体实现方式,而只需要向订阅者发布事件即可;而订阅者也不需要知道发布者是如何生成事件的,只需要在接收到事件后响应做出相应的处理即可。

发布/订阅模式可以应用于很多场景,例如:

  • 视图和数据模型之间的通信
  • 系统中各个组件之间的协作
  • 异步编程中的事件监听等

例子

JavaScript中实现发布订阅模式可以使用观察者模式,通过将subscriber添加到events队列中,并在event被触发时通知所有订阅者。

// 发布订阅事件管理器 
class EventManager {
  constructor() {
    // 创建一个events对象来存储订阅事件和回调函数
    this.events = {};
  }
​
  // 订阅事件
  subscribe(eventName, callback) {
    // 如果事件不存在,则创建
    if(!this.events[eventName]) {
      this.events[eventName] = [];
    }
    // 添加回调函数到事件列表
    this.events[eventName].push(callback);
  }
​
  // 发布事件
  publish(eventName, data) {    
    // 如果事件不存在,则返回
    if (!this.events[eventName]) return;
  
    // 迭代事件列表并执行回调函数
    this.events[eventName].forEach(function(callback) {
      callback(data);
    });
  }
​
  // 取消订阅事件
  unsubscribe(eventName, callback) {
    // 如果事件不存在,则返回
    if (!this.events[eventName]) return;
​
    //查找回调函数并移除
    const index = this.events[eventName].indexOf(callback);
    if (index > -1) {
      this.events[eventName].splice(index, 1);
    }
  }
}
​
const eventManager = new EventManager();
​
//创建订阅事件 'message'
eventManager.subscribe('message', function(data) {
  console.log(`接收消息:${data}`);
});
​
// 发布事件 'message'
eventManager.publish('message', '你好,欢迎加入我们的聊天室!');
​
// 取消订阅事件 'message'
eventManager.unsubscribe('message');
​

在上面的代码中,我们使用一个EventManager类来存储订阅事件和回调函数。通过subscribe方法添加订阅者到事件列表中,然后通过publish方法触发事件并通知所有订阅者执行回调函数。如果需要取消事件订阅,则可以使用unsubscribe方法移除回调函数。

区别

观察者模式发布-订阅模式是两个相关但有所不同的设计模式。

观察者模式(observer pattern)定义了一种一对多的关系,即一个被观察者对象的状态发生改变时,会自动通知所有它所依赖的观察者对象,并且观察者对象将根据被观察者对象传过来的参数进行相应的处理。这个模式非常适用于需要实时监测某些对象状态并及时做出响应的场景。

而发布-订阅模式(publish/subscribe pattern)则使用了一个中间件,即消息代理(message broker),来解除了订阅者与发布者的直接耦合关系。发布者发布消息到消息代理(或者叫发布中心),订阅者从消息代理订阅消息,消息代理将消息传递给所有订阅者。在这个模式中发布者和订阅者之间并没有直接联系,而是通过消息代理来进行数据交换。这个模式非常适用于大量数据发布和订阅、异步处理以及松散耦合的场景。

虽然观察者模式和发布-订阅模式都可以用来实现对象间的低耦合,但本质上还是有所不同的。观察者模式看重的是被观察者和观察者之间的直接联系,而发布-订阅模式则更注重于消息代理在其中扮演的角色。

策略模式

策略模式是一种行为型设计模式,它允许你在运行时选择算法的行为。这个模式定义了一系列算法类,将每一个算法封装起来并且使它们可以相互替换。程序运行时使用哪种方式由用户选择,根据用户选择执行不同的逻辑。

实现策略模式的步骤

策略模式的实现包括以下步骤:

  1. 定义一个抽象策略(strategy)接口或者抽象类,声明公共方法,该方法将由各个具体策略实现。
  2. 定义具体策略(concretestrategy)类,继承抽象策略类,并实现其方法。
  3. 创建一个环境类(context),它持有一个策略对象的引用,可动态指定使用哪一个具体策略对象。
  4. 在环境类中定义一个设置具体策略的方法,以便动态地更改对象所使用的具体策略。

适用场景

当你需要在不同情况下使用不同算法时,可以使用策略模式。例如,用户可以选择多种支付方式时,每种支付方式对应一种接口;当你需要根据不同的文件格式去解析不同的文件时,就可以考虑使用策略模式,定义多个解析策略类,客户端通过设置不同的解析策略来实现对不同文件格式的解析。此外,当你需要避免使用多重条件转移语句时,也可以考虑使用策略模式。