从前端视角看设计模式之创建型模式篇

设计模式简介

"设计模式"源于GOF(四人帮)合著出版的《设计模式:可复用的面向对象软件元素》,该书第一次完整科普了软件开发中设计模式的概念,他们提出的设计模式主要是基于以下的面向对象设计原则:

  • 对接口编程而不是对实现编程
  • 优先使用对象组合而不是继承

根据该书所提到的,总共有 23 种设计模式,这些模式可以分为以下三大类:

1)创建型模式

这些模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象,这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活

包括:工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式

2)结构型模式

这些模式关注对象之间的组合和关系,旨在解决如何构建灵活且可复用的类和对象结构

包括:适配器模式、桥接模式、过滤器模式、组合模式、装饰器模式、外观模式、享元模式、代理模式

3)行为型模式

这些模式关注对象之间的通信和交互,旨在解决对象之间的责任分配和算法的封装

包括:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、空对象模式、策略模式、模板模式、访问者模式

下图描述设计模式之间的关系:

设计模式的六大原则

1)开闭原则

开闭原则是指:对扩展开发,对修改关闭。在程序需要进行扩展的时候,不能去修改原有的代码,想要达到这样的效果,我们需要使用接口和抽象类

2)里氏代换原则

里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。该原则其实是对开闭原则的补充,实现开闭原则的关键步骤是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范

3)依赖倒转原则

这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体

4)接口隔离原则

接口隔离原则是指:使用多个隔离的接口,比使用单个接口要好,还有个意思是:降低类之间的耦合度

5)迪米特原则(又称:最少知道原则)

最少知道原则是指:一个实体尽量少地与其他实体发生相互作用,使得系统功能模块相互独立

6)合成复用原则

合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承

工厂模式

工厂模式提供了一种创建对象的方式,而无需指定要创建的具体类。通过使用工厂模式,可以将对象的创建逻辑封装到一个工厂类里,而不是在客户端代码中直接实例化对象

工厂模式主要有以下几种类型:

1)简单工厂模式

简单工厂模式不是一个正式的设计模式,但它是工厂模式的基础,它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象

2)工厂方法模式

工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪个类,工厂方法将对象的创建延迟到子类

3)抽象工厂模式

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


当我们需要在不同条件下创建不同实例时可以使用工厂模式,它的使用场景主要有以下几个:

1)日志记录:日志可能记录到本地硬盘、远程服务器等,用户可以选择记录日志的位置

2)数据库访问:当用户不知道最终系统使用哪种数据库,或者数据库可能变化时

PS:工厂模式适用于生成复杂对象的场景,如果对象较为简单,通过 new 即可完成创建,而不必使用工厂模式,使用工厂模式会引入一个工厂类,增加系统复杂度


它的优缺点:

1)优点

  • 调用者只需要知道对象的名称即可创建对象
  • 扩展性高:如果需要增加新产品,只需扩展一个工厂类即可
  • 屏蔽了产品的具体实现,调用者只关心产品的接口

2)缺点

每次增加一个产品时,都需要增加一个具体类和对应的工厂,使系统中类的数量成倍增加,增加了系统的复杂度和具体类的依赖


工厂模式包含以下几个主要角色:

1)抽象产品

定义了产品的共同接口或抽象类,它可以是具体产品类的父类或接口,规定了产品对象的共同方法

2)具体产品

实现了抽象产品接口,定义了具体产品的特定行为和属性

3)抽象工厂

声明了创建产品的抽象方法,可以是接口或抽象类,它可以有多个方法用于创建不同类型的产品

4)具体工厂

实现了抽象工厂接口,负责实际创建具体产品的对象


我们通过以下一个简单的实例来展示工厂模式的实现,假设我们有不同种类的button需要创建,每个按钮有不同的实现

1)定义抽象产品和具体产品

Button类是一个抽象类(接口),它有两个方法:render()用于渲染按钮,onClick()用于按钮点击后的行为

WindowsButtonMacButton分别实现了Button接口,提供具体的渲染和点击行为

复制代码
// 抽象产品:button
class Button {
    render() {
      throw new Error('方法 "render()" 被实现')
    }
    onClick() {
      throw new Error('方法 "onClick()" 被实现')
    }
}
  
// 具体产品:Windows 按钮
class WindowsButton extends Button {
    render() {
        console.log('渲染 Windows 按钮')
    }
    onClick() {
        console.log('点击了 Windows 按钮')
    }
}

// 具体产品:Mac 按钮
class MacButton extends Button {
    render() {
        console.log('渲染 Mac 按钮')
    }
    onClick() {
        console.log('点击了 Mac 按钮')
    }
}

2)定义抽象工厂和具体工厂

ButtonFactory是工厂接口,定义了createButton()方法,用来创建具体的按钮

WindowsButtonFactoryMacButtonFactory分别实现了createButton()方法,创建不同操作系统的按钮

复制代码
// 工厂接口
class ButtonFactory {
    createButton() {
      throw new Error('方法 "createButton()" 被实现')
    }
}
  
// 具体工厂:Windows 按钮工厂
class WindowsButtonFactory extends ButtonFactory {
    createButton() {
        console.log('创建 Windows 按钮')
        return new WindowsButton()
    }
}

// 具体工厂:Mac 按钮工厂
class MacButtonFactory extends ButtonFactory {
    createButton() {
        console.log('创建 Mac 按钮')
        return new MacButton()
    }
}

3)使用工厂创建对象

renderButton() 是客户端方法,通过传入工厂对象来创建并使用按钮

复制代码
// 客户端代码:根据需要选择不同的工厂来创建不同的按钮
function renderButton(factory) {
    const button = factory.createButton()
    button.render()                        
    button.onClick()                       
}

// 模拟渲染不同操作系统的按钮
console.log('Windows 按钮:')
renderButton(new WindowsButtonFactory())

console.log('Mac 按钮:')
renderButton(new MacButtonFactory())

执行上述代码,控制台打印出:

抽象工厂模式

抽象工厂模式是工厂模式的扩展,它提供一个接口,用于创建一系列相关或相互依赖的对象 ,而无需指定具体的类,换句话说,抽象工厂模式为一组产品提供了一个接口,这组产品可能有多个不同的具体实现,而每一个具体工厂类都负责创建这些产品的不同实现


它的使用场景主要有以下几个:

1)系统需要多个产品族的产品在一起工作,但不需要指定具体类时

2)系统需要独立于产品的创建、组合和表示时


它的优缺点:

1)优点

  • 确保同一产品族的对象一起工作
  • 客户端不需要知道每个对象的具体类,简化了代码

2)缺点

增加一个新的产品族需要修改抽象工厂和所有具体工厂的代码


假设我们需要在多个操作系统上创建不同风格的按钮和文本框,将使用抽象工厂模式来创建这些 UI 元素,以便在不同操作系统上使用相应的样式

1)定义抽象产品接口

定义ButtonTextBox作为抽象产品,它们是各个操作系统上的 UI 元素

复制代码
// 抽象产品:Button
class Button {
    render() {
      throw new Error('方法 "render()" 被实现')
    }
    onClick() {
      throw new Error('方法 "onClick()" 被实现')
    }
} 
// 抽象产品:TextBox
class TextBox {
    render() {
      throw new Error('方法 "render()" 被实现')
    }
    onFocus() {
      throw new Error('方法 "onFocus()" 被实现')
    }
}

2)定义具体产品

定义具体产品类:WindowsButtonMacButtonWindowsTextBoxMacTextBox,它们实现了上述的抽象产品接口

复制代码
// 具体产品:Windows 按钮
class WindowsButton extends Button {
    render() {
      console.log('渲染 Windows 按钮')
    }
    onClick() {
      console.log('点击了 Windows 按钮')
    }
} 
// 具体产品:Mac 按钮
class MacButton extends Button {
    render() {
      console.log('渲染 Mac 按钮')
    }
    onClick() {
      console.log('点击了 Mac 按钮')
    }
}
// 具体产品:Windows 文本框
class WindowsTextBox extends TextBox {
    render() {
      console.log('渲染 Windows 文本框')
    }
    onFocus() {
      console.log('Windows 文本框获得焦点')
    }
}
// 具体产品:Mac 文本框
class MacTextBox extends TextBox {
    render() {
      console.log('渲染 Mac 文本框')
    }
    onFocus() {
      console.log('Mac 文本框获得焦点')
    }
}

3)定义抽象工厂接口

UIFactory是抽象工厂接口,定义了两个方法:createButtoncreateTextBox,用来创建按钮和文本框

复制代码
// 抽象工厂接口:UIFactory
class UIFactory {
    createButton() {
      throw new Error('方法 "createButton()" 必须被实现')
    }
    createTextBox() {
      throw new Error('方法 "createTextBox()" 必须被实现')
    }
}

4)定义具体工厂

具体工厂WindowsFactoryMacFactory负责实例化具体的 UI 元素(按钮和文本框)

复制代码
// 具体工厂:Windows 工厂
class WindowsFactory extends UIFactory {
    createButton() {
      console.log('创建 Windows 按钮')
      return new WindowsButton()
    }
    createTextBox() {
      console.log('创建 Windows 文本框')
      return new WindowsTextBox()
    }
}
// 具体工厂:Mac 工厂
class MacFactory extends UIFactory {
    createButton() {
      console.log('创建 Mac 按钮')
      return new MacButton()
    }
    createTextBox() {
      console.log('创建 Mac 文本框')
      return new MacTextBox()
    }
}

5)客户端使用工厂

根据需求选择不同的工厂来创建产品(按钮和文本框),而不关心具体的产品实现

复制代码
// 客户端代码:根据工厂创建 UI 元素
function renderUI(factory) {
    const button = factory.createButton()  
    button.render()                        
    button.onClick()                       
  
    const textBox = factory.createTextBox()  
    textBox.render()                        
    textBox.onFocus()                       
}

console.log('渲染 Windows 风格的 UI:')
renderUI(new WindowsFactory())

console.log('渲染 Mac 风格的 UI:')
renderUI(new MacFactory())

执行上述代码,控制台打印出:

单例模式

单例模式确保一个类只有一个实例,并提供了一个全局访问点来访问该实例,需要注意的是:

1)单例类只能有一个实例

2)单例类必须自己创建自己的唯一实例

3)单例类必须给其他对象提供这一实例


当需要控制实例数量,节省系统资源时可以使用单例模式,它的使用场景主要有以下几个:

1)生成唯一序列号

2)创建消耗资源过多的对象,比如:I/O、数据库连接等

3)如果某个计算的结果是全局唯一且不希望重复计算,可以使用单例模式缓存该结果,确保第一次计算后复用该结果


它的缺点:

1)没有接口,不能继承

2)与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心实例化方式


单例模式可以使用以下不同的方法来实现:

1)懒汉式

首次调用时创建实例,适合资源消耗较大的情况

2)饿汉式

类加载时创建实例,适合在程序启动时就需要用到实例的情况

懒汉式

懒汉式的单例模式是在需要实例的时候才创建实例,实例是延迟加载的,只有在第一次访问时才会创建

第一次创建instance1时,Singleton的构造函数会初始化实例并存储在Singleton.instance中;第二次创建instance2时,由于Singleton.instance已经存在,构造函数直接返回已有的实例

复制代码
class Singleton {
    constructor() {
    // 如果实例已经存在,直接返回该实例
        if (Singleton.instance) {
            return Singleton.instance
        }
        // 否则就初始化实例
        this.value = Math.random()
        Singleton.instance = this
    }

    getValue() {
        return this.value
    }
}
  
// 测试
const instance1 = new Singleton()
console.log(instance1.getValue())

const instance2 = new Singleton()
console.log(instance2.getValue())

console.log(instance1 === instance2)  // 输出 true,说明是同一个实例

在 JavaScript 中,单线程模型通常不会遇到并发问题,但是在多线程环境中,如果多个线程同时调用new Singleton(),可能会创建多个实例,违背了单例模式的原则

为了避免多个线程并发访问时创建多个实例,我们可以通过加锁来确保同一时刻只有一个线程能创建实例,使用 synchronized 来进行加锁

在 JavaScript 中实现"上锁"的概念时,通常使用一个标志变量(如lock)来模拟锁的功能

通过Singleton.lock来确保只有一个线程能执行实例创建的代码,其他线程在此期间会被阻塞,直到锁被释放;在try...finally结构中,确保即使发生异常,也能释放锁

复制代码
class Singleton {
    constructor() {
      // 如果实例已经存在,直接返回该实例
      if (Singleton.instance) {
        return Singleton.instance
      }
  
      // 模拟线程安全的加锁机制
      if (!Singleton.lock) {
        Singleton.lock = true  // 上锁
        try {
          // 如果没有实例,则创建一个
          this.value = Math.random()
          Singleton.instance = this
        } finally {
          Singleton.lock = false  // 解锁
        }
      }
    }
  
    getValue() {
      return this.value
    }
}
  
// 测试
const instance1 = new Singleton()
console.log(instance1.getValue())

const instance2 = new Singleton()
console.log(instance2.getValue())

console.log(instance1 === instance2)  // 输出 true,说明是同一个实例

饿汉式

饿汉式的单例模式与懒汉式不同,它在类加载时就创建好实例,饿汉式实现不需要考虑线程安全问题,因为实例在类加载时就创建好了,只有一个线程能访问类加载阶段

在下面的实现中,Singleton.instance是类加载时就创建好的静态属性,因此无论多少次访问,都返回相同的实例

复制代码
class Singleton {
  static instance = new Singleton()
  
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance
    }
    this.value = Math.random() 
  }

  getValue() {
    return this.value
  }
}
  
// 测试
const instance1 = Singleton.instance
console.log(instance1.getValue())

const instance2 = Singleton.instance
console.log(instance2.getValue())

console.log(instance1 === instance2)  // 输出 true,说明是同一个实例

建造者模式

建造者模式将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的对象


它的使用场景主要有以下:

1)当一个对象的构建过程很复杂,可能涉及多个步骤,且这些步骤可以独立进行

2)需要支持产品的多种表现形式(不同配置、不同组合)

PS:与工厂模式的区别是:建造者模式更加关注于零件装配的顺序


它的优缺点:

1)优点

  • 分离构建过程和表示,可以构建不同的表示
  • 代码复用性高,可以在不同的构建过程中重复使用相同的建造者

2)缺点

  • 如果产品属性较少,该模式会导致代码冗余
  • 增加了系统的类和对象数量

建造者模式包含以下几个主要角色:

1)产品

要构建的复杂对象,产品类通常包含多个部分或属性

2)抽象建造者

定义了构建产品的抽象接口,包括构建产品的各个部分的方法

3)具体建造者

实现抽象建造者接口,具体确定如何构建产品的各个部分,并负责返回最终构建的产品

4)指导者

负责调用建造者的方法来构建产品,指导者并不了解具体的构建过程,只关心产品的构建顺序和方式


假设我们需要构建一辆汽车,汽车有多个部件,不同配置的汽车有不同的部件组合,下面通过建造者模式来组织这个构建过程

1)定义产品类

构建一个汽车产品类,它有多个部件:引擎、轮胎、车窗

复制代码
class Car {
  constructor() {
    this.engine = null
    this.tires = null
    this.windows = null
  }
  show() {
    console.log(`汽车配置:引擎: ${this.engine}, 轮胎: ${this.tires}, 车窗: ${this.windows}`)
  }
}

2)定义抽象建造者

建造者类定义了构建汽车的各个步骤,但不包含具体的实现

复制代码
//抽象建造者:CarBuilder
class CarBuilder {
    buildEngine() {
      throw new Error('子类实现 buildEngine 方法')
    }
    buildTires() {
      throw new Error('子类实现 buildTires 方法')
    }
    buildWindows() {
      throw new Error('子类实现 buildWindows 方法')
    }
    getCar() {
      throw new Error('子类实现 getCar 方法')
    }
}

3)定义具体建造者

具体建造者类继承CarBuilder,并实现了具体的构建过程,这里定义了两种不同配置的汽车:普通汽车和豪华汽车

复制代码
//具体建造者:普通汽车 NormalCarBuilder
class NormalCarBuilder extends CarBuilder {
    constructor() {
      super()
      this.car = new Car()
    }
    buildEngine() {
      this.car.engine = '普通引擎'
    }
    buildTires() {
      this.car.tires = '普通轮胎'
    }
    buildWindows() {
      this.car.windows = '普通车窗'
    }
    getCar() {
      return this.car
    }
}
//具体建造者:豪华汽车 LuxuryCarBuilder
class LuxuryCarBuilder extends CarBuilder {
    constructor() {
      super()
      this.car = new Car()
    }
    buildEngine() {
      this.car.engine = '豪华引擎'
    }
    buildTires() {
      this.car.tires = '豪华轮胎'
    }
    buildWindows() {
      this.car.windows = '豪华车窗'
    }
    getCar() {
      return this.car
    }
}

4)定义指导者

指导者负责按照一定的顺序,调用建造者类的方法,来创建汽车

复制代码
//指导者:Director
class Director {
    constructor(builder) {
      this.builder = builder
    }
    construct() {
      this.builder.buildEngine()
      this.builder.buildTires()
      this.builder.buildWindows()
    }
}

5)客户端

客户端通过指导者来控制建造过程,最终得到一个构建好的汽车对象

复制代码
// 客户端代码
const normalCarBuilder = new NormalCarBuilder()
const normalDirector = new Director(normalCarBuilder)

normalDirector.construct()
const normalCar = normalCarBuilder.getCar()
normalCar.show() // 输出: 汽车配置:引擎: 普通引擎, 轮胎: 普通轮胎, 车窗: 普通车窗

const luxuryCarBuilder = new LuxuryCarBuilder()
const luxuryDirector = new Director(luxuryCarBuilder)

luxuryDirector.construct()
const luxuryCar = luxuryCarBuilder.getCar()
luxuryCar.show() // 输出: 汽车配置:引擎: 豪华引擎, 轮胎: 豪华轮胎, 车窗: 豪华车窗

原型模式

原型模式通过复制现有的对象来创建新对象,而不是通过直接构造新的对象,避免了重复初始化复杂对象的开销


它的使用场景主要有以下几个:

1)类初始化需要消耗大量资源,如数据、硬件资源

2)通过 new 创建对象需要复杂的数据准备或访问权限时

3)一个对象需要多个修改者

PS:与直接实例化类创建新对象不同,原型模式通过拷贝现有对象生成新对象


它的优缺点:

1)优点

  • 性能提高
  • 避免构造函数的约束

2)缺点

配置克隆方法需要全面考虑类的功能,对已有类可能较难实现,特别是处理不支持串行化的间接对象或含有循环结构的引用时


原型模式包含以下几个主要角色:

1)原型接口

定义一个用于克隆自身的接口,通常包含一个 clone() 方法

2)具体原型类

实现圆形接口的具体类,负责实际的克隆操作,通常使用浅拷贝或深拷贝来复制自身

  • 浅拷贝:复制对象时,原始对象的属性值会被复制到新对象中,但如果属性是引用类型,则会复制引用而不是实际的对象
  • 深拷贝:不仅复制对象本身的属性,还会递归地复制对象中引用类型的属性,从而确保新对象完全独立于原始对象

3)客户端

使用原型实例来创建新的对象,客户端调用原型对象的 clone() 方法来创建新的对象,而不是直接使用构造函数


假设我们需要复制一个游戏角色,这个角色有多个属性,如名字、血量、装备等,通过克隆原始角色来创建一个新的角色

1)创建一个GameCharacter

复制代码
// 原型类:游戏角色
class GameCharacter {
    constructor(name, health, weapons) {
      this.name = name      
      this.health = health 
      this.weapons = weapons  
    }
    // 显示角色信息
    display() {
      console.log(`角色:${this.name}, 血量:${this.health}, 武器:${this.weapons.join(', ')}`)
    }
    // 克隆方法 - 浅拷贝
    clone() {
      return new GameCharacter(this.name, this.health, [...this.weapons])
    }
}

2)创建一个简单的角色对象

复制代码
// 创建一个角色
const originalCharacter = new GameCharacter("战士", 100, ["剑", "盾"])
originalCharacter.display()

3)克隆角色

复制代码
// 克隆角色
const clonedCharacter = originalCharacter.clone()
clonedCharacter.display()

4)修改克隆后的对象

修改克隆后的角色,看看它是否独立于原始对象:

复制代码
// 修改克隆后的角色
clonedCharacter.name = "弓箭手"
clonedCharacter.health = 80
clonedCharacter.weapons.push("弓")

// 显示修改后的角色信息
clonedCharacter.display()

// 显示原始角色信息
originalCharacter.display()

从以下输出中可以看出,原始角色和克隆后的角色是独立的,修改克隆角色的属性不会影响原始角色

当前的 clone 方法是浅拷贝,它会直接复制对象的引用类型属性,如 weapons 数组,如果修改了克隆角色的weapons,会影响到原始角色

为了避免这个问题,我们可以在 clone 方法中使用深拷贝,保证即使 weapons 是引用类型,也会完全复制

复制代码
// 深拷贝:确保复制所有引用类型的内容
clone() {
  return new GameCharacter(this.name, this.health, JSON.parse(JSON.stringify(this.weapons)))
}
相关推荐
文心快码BaiduComate10 分钟前
WAVE SUMMIT深度学习开发者大会2025举行 文心大模型X1.1发布
前端·后端·程序员
babytiger10 分钟前
python 通过selenium调用chrome浏览器
前端·chrome
passer98117 分钟前
基于webpack的场景解决
前端·webpack
华科云商xiao徐24 分钟前
Java并发编程常见“坑”与填坑指南
javascript·数据库·爬虫
奶昔不会射手30 分钟前
css3之grid布局
前端·css·css3
举个栗子dhy34 分钟前
解决在父元素上同时使用 onMouseEnter和 onMouseLeave时导致下拉菜单无法正常展开或者提前收起问题
前端·javascript·react.js
Coding_Doggy40 分钟前
苍穹外卖前端Day1 | vue基础、Axios、路由vue-router、状态管理vuex、TypeScript
前端
前端与小赵40 分钟前
vue3和vue2生命周期的区别
前端·javascript·vue.js