介绍前端框架中的设计模式,其优缺点以及使用案例 | 青训营

132 阅读16分钟

设计模式是一套被广泛使用的、经过验证的、可供参考的解决方案,用于解决软件系统中的常见问题。

单例模式

单例设计模式是一种创建对象的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

使用案例

单例设计模式用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这在需要限制一个类的实例化次数时非常有用,例如数据库连接、配置对象等情况。

以下是一个单例设计模式的使用案例:

// 单例对象 - 配置管理器
class ConfigurationManager {
  constructor() {
    if (ConfigurationManager.instance) {
      return ConfigurationManager.instance;
    }

    this.config = {}; // 配置数据

    ConfigurationManager.instance = this;
  }

  // 设置配置项
  setConfig(key, value) {
    this.config[key] = value;
  }

  // 获取配置项
  getConfig(key) {
    return this.config[key];
  }
}

// 创建单例实例
const configManager1 = new ConfigurationManager();
const configManager2 = new ConfigurationManager();

// 向配置管理器设置配置项
configManager1.setConfig('server', 'localhost');
configManager2.setConfig('port', 8080);

// 获取配置项
console.log(configManager1.getConfig('server')); // 输出: localhost
console.log(configManager2.getConfig('port'));   // 输出: 8080

在上述案例中,ConfigurationManager 类是一个单例类,它的构造函数通过检查是否已经存在实例来保证只有一个实例。如果已经存在实例,构造函数会返回现有的实例,否则会创建一个新实例。

可以看到,尽管创建了两个不同的变量 configManager1configManager2 来实例化 ConfigurationManager 类,但实际上它们引用的是同一个实例,因为单例模式确保了只有一个实例存在。

通过单例设计模式,可以确保全局范围内只有一个配置管理器实例,避免了重复创建实例和数据共享的问题。这在需要共享资源或数据的情况下非常有用。


优缺点

优点:

  1. 独一无二的实例:单例模式确保一个类只有一个实例存在,这样可以避免多个对象的创建,节省系统资源。
  2. 全局访问点:通过单例模式,可以在整个应用程序中方便地访问该实例,避免了传递对象的麻烦。
  3. 延迟实例化:单例模式可以延迟对象的实例化,只有在第一次访问时才会创建对象,提高了系统的性能。

缺点:

  1. 难以扩展:由于单例模式只允许一个实例存在,因此扩展该类的实例数量非常困难。如果需要更多的实例,就需要修改原有的代码。

  2. 高耦合性:单例模式将对象的创建和使用进行了紧耦合,使得代码的可测试性和可维护性降低。

  3. 破坏单一职责原则:单例模式往往承担了过多的职责,既充当了对象的创建者,又充当了全局访问点,导致职责不单一。


观察者模式

观察者模式,用于在对象之间建立一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会收到通知并自动更新。

实现这个模式需要定义两个角色:发布者和订阅者。发布者负责管理订阅者列表,并在状态发生变化时通知订阅者。订阅者则负责接收通知并根据需要进行相应的处理。

具体实现时,可以使用接口或抽象类来定义发布者和订阅者的方法,使得它们能够灵活地扩展和适应不同的需求。

使用案例

一个常见的观察者设计模式的使用案例是图形界面(GUI)中的事件处理。在图形界面中,用户的交互操作(例如点击按钮、输入文本等)会触发各种事件,而这些事件需要被相应的组件或对象捕获和处理。

以下是一个简单的使用案例:

// 主题对象 - 按钮
class Button {
  constructor() {
    this.clickHandlers = [];
  }

  // 添加点击事件处理程序
  addClickHandler(handler) {
    this.clickHandlers.push(handler);
  }

  // 触发点击事件
  onClick() {
    this.clickHandlers.forEach(handler => handler());
  }
}

// 观察者对象 - 文本框
class TextBox {
  constructor() {
    this.text = '';
  }

  // 更新文本内容
  updateText(newText) {
    this.text = newText;
    console.log('TextBox updated: ' + this.text);
  }
}

// 创建主题对象 - 按钮实例
const button = new Button();

// 创建观察者对象 - 文本框实例
const textBox = new TextBox();

// 添加观察者对象到主题对象的观察者列表中
button.addClickHandler(() => textBox.updateText('New Text'));

// 模拟用户点击按钮
button.onClick(); // 输出: TextBox updated: New Text

在上述示例中,Button 类是主题对象,它维护一个点击事件处理程序的列表,并提供添加和触发点击事件的方法。TextBox 类是观察者对象,它具有一个 updateText 方法用于接收并处理按钮点击事件的通知。

在主题对象中,可以通过 addClickHandler 方法将观察者对象添加到观察者列表中。当按钮被点击时,通过调用 onClick 方法,主题对象会通知所有注册的观察者对象,并触发相应的操作。

在上述示例中,当按钮被点击时,观察者对象 TextBoxupdateText 方法被调用,更新了文本框的内容。

这个案例展示了观察者模式在图形界面事件处理中的应用,通过使用观察者模式,可以实现组件之间的松耦合,使得事件的发送者和接收者之间解耦,提高了代码的可维护性和扩展性。


优缺点

优点:

  1. 解耦性:观察者模式可以将观察者和主题对象解耦,使它们之间的依赖关系变得松散。主题对象并不需要知道观察者的具体实现,只需要知道观察者接口即可。这样可以方便地添加、移除或修改观察者,而不需要修改主题对象的代码。
  2. 可扩展性:通过观察者模式,可以方便地扩展和添加新的观察者,而不会影响到主题对象或其他观察者的代码。这使得系统更加灵活和可扩展。
  3. 通知机制:观察者模式提供了一种简单且可靠的通知机制。当主题对象的状态发生变化时,它会自动通知所有的观察者,观察者可以及时进行相应的处理。

缺点:

  1. 内存泄漏风险:如果观察者没有正确地被移除或管理,可能会导致内存泄漏问题。当观察者注册到主题对象后,如果没有及时移除观察者,即使观察者不再需要接收通知,主题对象仍然会保持对观察者的引用,从而导致观察者无法被垃圾回收。
  2. 顺序依赖:观察者模式中观察者的执行顺序是不确定的。如果观察者之间有顺序依赖关系,可能会导致问题。在某些情况下,观察者的执行顺序是很重要的,但观察者模式本身并不提供对观察者执行顺序的控制。

总体而言,观察者设计模式是一种有用的模式,可以实现对象之间的松耦合和通信。它适用于需要一对多通知机制的场景,同时需要注意管理观察者的生命周期和顺序依赖的问题。


原型模式

原型模式(Prototype Pattern)是一种创建型设计模式,它允许通过复制现有对象来创建新对象,而无需显式地使用构造函数。在原型模式中,首先创建一个原型对象,然后通过复制该原型对象来创建新的对象。

在实现原型模式时,通常需要实现一个原型接口或基类,该接口或基类定义了复制对象的方法。具体的原型对象实现该接口或继承该基类,并实现复制方法以返回新的对象副本。

使用案例

用JavaScript实现原型模式时,可以使用原型链和Object.create()方法来创建对象的副本。以下是一个简单的原型模式案例:

// 创建原型对象
const carPrototype = {
  init: function (brand, model) {
    this.brand = brand;
    this.model = model;
  },
  getInfo: function () {
    console.log(`Brand: ${this.brand}, Model: ${this.model}`);
  }
};

// 创建新对象的工厂函数
function createCar(brand, model) {
  const car = Object.create(carPrototype); // 使用原型对象创建新对象
  car.init(brand, model); // 初始化新对象的属性
  return car;
}

// 创建对象实例
const car1 = createCar("Toyota", "Camry");
const car2 = createCar("Honda", "Civic");

// 调用对象方法
car1.getInfo(); // 输出: Brand: Toyota, Model: Camry
car2.getInfo(); // 输出: Brand: Honda, Model: Civic

在上面的示例中,首先创建了一个原型对象carPrototype,它包含了init()方法用于初始化对象的属性,并且还包含了getInfo()方法用于输出对象信息。

然后,定义了一个工厂函数createCar(),它使用Object.create()方法基于carPrototype创建新的对象,并通过调用init()方法来初始化对象的属性。

最后,使用createCar()函数创建了两个对象实例car1car2,并通过调用getInfo()方法输出了它们的信息。

通过使用原型模式,可以通过复制原型对象来创建新的对象实例,而无需显式地使用构造函数,从而实现对象的快速创建。


优缺点

优点:

  1. 对象的复制:原型模式通过复制现有对象来创建新对象,避免了显式的构造函数调用,使得对象的创建更加方便和高效。
  2. 隐藏对象创建细节:使用原型模式,客户端无需关心对象的创建细节,只需通过复制原型对象来创建新对象,简化了对象的创建过程。
  3. 动态添加和修改对象的属性和方法:原型模式允许在运行时动态地添加和修改对象的属性和方法,通过修改原型对象可以影响到所有基于该原型创建的对象。
  4. 减少子类的创建:原型模式可以通过克隆原型对象来创建新对象,避免了子类的创建,减少了子类的数量,简化了类的继承关系。

缺点:

  1. 深拷贝问题:在原型模式中,对象的复制是通过浅拷贝实现的,即只复制对象的引用而不复制对象本身。如果原型对象中包含引用类型的属性,那么复制的对象和原型对象会共享这些属性,可能会导致意外的修改。
  2. 对象状态的处理:原型模式创建的对象是通过复制原型对象得到的,新对象和原型对象是相互独立的,但是如果对象的状态发生变化,需要在每个对象实例中进行处理,可能会增加代码的复杂性。
  3. 需要配合其他模式使用:原型模式通常需要和其他模式一起使用,如原型管理器模式,以便更好地管理和使用原型对象。

总体而言,原型模式在对象的创建和复制方面具有一定的优势,但在处理对象状态和深拷贝等方面需要额外的注意和处理。在具体应用时,需要根据实际需求和场景来评估是否适合使用原型模式。


代理模式

代理模式是一种结构型设计模式,它允许通过创建一个代理对象来控制对另一个对象的访问。代理对象充当了客户端和目标对象之间的中介,通过代理对象可以间接地访问目标对象,从而实现对目标对象的控制和管理。

代理模式的主要参与者包括:

  1. 抽象主题(Subject):定义了目标对象和代理对象的共同接口,这样代理对象可以通过实现该接口来替代目标对象。
  2. 目标对象(Real Subject):是代理对象所代表的真实对象,它定义了代理对象所具有的真实业务逻辑。
  3. 代理对象(Proxy):实现了抽象主题接口,它包含一个引用,可以访问真实对象,并在必要时对真实对象的方法进行增强或附加额外的操作。

使用案例

这个示例,将创建一个代理对象来控制对敏感信息的访问。

// 目标对象:包含敏感信息的类
class SensitiveInfo {
  constructor() {
    this.info = "This is sensitive information.";
  }
  
  getInfo() {
    return this.info;
  }
}

// 代理对象:控制对目标对象的访问
class Proxy {
  constructor() {
    this.target = new SensitiveInfo();
    this.accessGranted = false; // 访问权限,默认为未授权
  }
  
  // 授权访问
  grantAccess() {
    this.accessGranted = true;
  }
  
  // 检查访问权限并获取信息
  getInfo() {
    if (this.accessGranted) {
      return this.target.getInfo();
    } else {
      return "Access denied. You need to grant access first.";
    }
  }
}

// 示例
const proxy = new Proxy();
console.log(proxy.getInfo()); // 输出:"Access denied. You need to grant access first."

proxy.grantAccess();
console.log(proxy.getInfo()); // 输出:"This is sensitive information."

在上面的示例中,有一个目标对象 SensitiveInfo,它包含了敏感信息。然后,创建了一个代理对象 Proxy,它控制对目标对象的访问。代理对象在默认情况下未授权访问,只有通过调用 grantAccess() 方法授权后,才能获取目标对象的信息。通过调用 getInfo() 方法,代理对象会检查访问权限并返回相应的结果。


优缺点

优点:

  1. 代理模式可以实现对目标对象的访问控制。代理对象可以充当一个中介,通过控制对目标对象的访问,可以实现权限验证、安全检查等功能,保护目标对象的安全性。
  2. 代理模式可以增加额外的功能。代理对象可以在调用目标对象的方法前后执行一些额外的操作,例如记录日志、缓存数据、延迟加载等,从而对目标对象进行增强。
  3. 代理模式可以实现远程访问。通过使用远程代理,可以在不同的地址空间中代表一个对象,使得可以通过网络进行远程访问,实现分布式系统的通信和协作。
  4. 代理模式可以实现对象的延迟加载。通过使用虚拟代理,可以在真正需要时才创建目标对象,从而提高系统的性能和资源利用率。

缺点:

  1. 增加了系统的复杂性。引入代理对象会增加额外的类和对象,使系统变得更加复杂,增加了代码的维护成本。
  2. 可能会降低系统的性能。由于代理对象需要在目标对象的访问前后执行额外的操作,可能会引入一定的性能开销,特别是在频繁访问目标对象时。
  3. 可能会引入单点故障。在使用远程代理时,如果代理对象出现故障或不可用,将导致无法访问目标对象,可能会影响系统的可用性。
  4. 可能会导致代码复杂化。如果代理模式的使用不当,可能会导致代码的复杂化和混乱,增加系统的理解和调试难度。

总体而言,代理模式在许多场景下都是很有用的,可以实现对目标对象的控制、增强和保护。然而,在使用代理模式时需要权衡其优点和缺点,根据具体的应用场景和需求进行设计和实现。


迭代器模式

迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供一种顺序访问聚合对象(例如列表、集合、数组等)元素的方法,而不需要暴露其内部实现细节。

迭代器模式的主要目标是将遍历聚合对象的责任与聚合对象本身分离,从而使得聚合对象可以独立变化,而不影响遍历的方式。

以下是迭代器模式的主要参与者:

  1. 迭代器(Iterator):定义了访问和遍历聚合对象元素的接口,包括获取下一个元素、判断是否还有元素等方法。
  2. 具体迭代器(Concrete Iterator):实现迭代器接口,负责实现具体的遍历算法,追踪当前遍历位置,并返回正确的元素。
  3. 聚合对象(Aggregate):定义创建迭代器的接口,可以是一个集合或容器对象。
  4. 具体聚合对象(Concrete Aggregate):实现聚合对象接口,创建具体的迭代器对象。

使用案例

这里利用ES6中的生成器函数(Generator Function)来创建迭代器。下面是一个简单的示例,演示如何使用迭代器模式遍历一个数组:

// 定义一个迭代器对象
function Iterator(array) {
  let nextIndex = 0;

  // 使用生成器函数创建迭代器
  return {
    next: function() {
      return nextIndex < array.length ?
        { value: array[nextIndex++], done: false } :
        { done: true };
    }
  };
}

// 创建一个数组
const arr = [1, 2, 3, 4, 5];

// 使用迭代器遍历数组
const iterator = Iterator(arr);
let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}

在上述示例中,定义了一个Iterator函数,它接受一个数组作为参数,并返回一个迭代器对象。迭代器对象内部使用生成器函数来实现next方法,该方法返回一个包含valuedone属性的对象,表示当前遍历的元素和遍历是否结束。

然后,创建一个数组arr,并使用Iterator函数创建一个迭代器对象iterator。接下来,使用while循环来遍历迭代器对象,每次调用next方法获取下一个元素,并在控制台打印出其值。循环终止的条件是迭代器的done属性为true,表示遍历结束。

通过以上实现,成功地使用迭代器模式遍历了一个数组。你可以根据需要进行扩展和调整,以适应不同的迭代需求和数据结构。


优缺点

优点:

  1. 分离了聚合对象和遍历逻辑:迭代器模式将遍历算法封装在迭代器中,使得聚合对象可以专注于自身的核心功能,而不需要关注遍历的实现细节。这样可以提高代码的可维护性和可扩展性。
  2. 支持多种遍历方式:通过定义不同的迭代器实现类,可以支持不同的遍历方式,例如顺序遍历、逆序遍历、按条件过滤等。这样可以灵活地适应不同的遍历需求,提高了代码的灵活性和复用性。
  3. 统一的遍历接口:迭代器模式提供了统一的遍历接口,使得客户端代码可以以统一的方式访问不同类型的聚合对象。这样可以简化客户端代码,减少了对具体聚合对象的依赖。

缺点:

  1. 增加了代码复杂性:引入迭代器模式会增加一些额外的类和接口,增加了代码的复杂性和理解难度。对于简单的遍历需求,使用迭代器模式可能会显得过于繁琐。
  2. 不适用于频繁修改聚合对象:如果聚合对象在遍历过程中频繁地发生修改,可能会导致迭代器失效或出现意外行为。因此,在需要频繁修改聚合对象的情况下,迭代器模式可能不是一个合适的选择。
  3. 遍历顺序固定:迭代器模式遍历聚合对象的顺序是固定的,无法灵活地改变遍历顺序。如果需要灵活地改变遍历顺序,可能需要修改迭代器的实现,增加一些额外的逻辑。

总结而言,迭代器模式通过将遍历算法封装在迭代器中,分离了聚合对象和遍历逻辑,提供了统一的遍历接口和灵活的遍历方式。它适用于遍历复杂的聚合对象,并且可以提高代码的可维护性和可扩展性。然而,它也会增加代码的复杂性,并且不适用于频繁修改聚合对象的情况。在使用迭代器模式时,需要根据具体的需求权衡其优缺点。