toBeTheLight.github.io 荒原

阅读:《图解设计模式》2 交给子类


书中将 Template Method 模板方法模式、Factory Method 工厂方法模式归入类的继承相关的设计模式。

  • 模板方法模式:将具体处理交给子类,核心是定义好模板方法,将此方法中调用的某些方法推迟到子类中实现。
  • 工厂方法模式:将实例的生成交给子类,核心是定义好工厂方法,将此方法中生成实例的方法和某些方法推迟到子类中实现。

Template Method 模板方法模式

定义

在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为 Template Method 模式。

角色

在模板方法中只有两个角色。

  • 抽象类:负责实现模板方法(此模板方法是最终产品的 API),同时负责定义组成此方法的抽象方法。
  • 具体类:负责实现上述抽象方法。

JavaScript 实现

// 抽象类
class Login {
  init (options) {
    // 模板方法
    this.initDom(options)
    this.initEvent()
    this.inited()
  }
  initDom () {
    throw new Error('需实现 LoginTemplate 类的 initDom 方法')
  }
  initEvent () {
    throw new Error('需实现 LoginTemplate 类的 initEvent 方法')
  }
  inited () {
    throw new Error('需实现 LoginTemplate 类的 inited 方法')
  }
}
// 具体类
class UsernameLogin extends Login {
  constructor () {
    super()
    this.name = '用户名登录'
    this.elements = {}
  }
  initDom (options) {
    const dom = document.createElement('div')
    dom.innerText = this.name
    const button = document.createElement('button')
    this.elements.loginButton = button
    dom.appendChild(button)
    options.el.appendChild(dom)
  }
  initEvent () {
    this.elements.loginButton.addEventListener('click', () => {
      alert(this.name + '成功')
    })
  }
  inited () {
    alert('初始化成功')
  }
}
// 使用方
const usernameLogin = new UsernameLogin()
usernameLogin.init({
  el: document.querySelector('#usernameLogin')
})

思考

适用于对外 API 固定,API 内流程行为组成固定,但根据分类不同,需要各自实现一部分或全部流程内行为的类。

Factory Method 工厂方法模式

定义

当我们将生成实例的部分交给子类去实现时,就形成了工厂方法模式。

角色

  • 抽象产品类:定义最终产品的 API。
  • 实现产品类:实现抽象产品类的 API 并实现其他必要方法。
  • 抽象工厂类:实现了生成实例的方法(流程),并负责定义组成此方法的抽象方法。
  • 实现工厂类:负责实现抽象工厂类的抽象方法。

其中产品类的实例化在实现工厂类实现的某个抽象方法中。所以实现工厂类对实现产品类是已知的并配套的。

JavaScript 实现

// 抽象产品类
class Product {
  init (options) {
    throw new Error('需实现 Product 类的 init 方法')
  }
}
// 实现产品类
class UsernameLogin extends Product {
  constructor () {
    super()
    this.name = '用户名登录'
    this.elements = {}
  }
  init (options) {
    const dom = document.createElement('div')
    dom.innerText = this.name
    const button = document.createElement('button')
    this.elements.loginButton = button
    dom.appendChild(button)
    options.el.appendChild(dom)
    this.elements.loginButton.addEventListener('click', () => {
      alert(this.name + '成功')
    })
    alert('初始化成功')
  }
}
// 抽象工厂类
class Factory {
  create () {
    // 此处使用了模板方法模式,定义了生成实例的方式,但是此方法细节由子类实现
    const product = this.createProduct()
    this.register(product)
    return product
  }
  createProduct () {
    throw new Error('需实现 Factory 类的 createProduct 方法')
  }
  register () {
    throw new Error('需实现 Factory 类的 register 方法')
  }
}
// 实现工厂类
class UsernameLoginFactory extends Factory {
  constructor () {
    super()
    this.id = 0
    this.manager = {}
  }
  createProduct () {
     return new UsernameLogin() 
  }
  register (instance) {
    this.manager[this.id] = instance
  }
}
// 使用方
const factory = new UsernameLoginFactory()
const usernameLogin = factory.create()
usernameLogin.init({
  el: document.querySelector('#usernameLogin')
})

思考

  • 工厂方法的选择:
    • 为避免过度设计,在仅仅只需要调用 new 就可完成实例生成的情况下,没有必要使用工厂方法模式。
    • 在实例的生成是一套流程时,建议使用。如 JavaScript 实现示例,使用 new 实例化之后,还需完成注册,添加其他参数等固定流程,而这些流程复杂不适合包含在构造函数中(构造函数本身不建议太复杂)。这时将初始化的过程封装在工厂类中,将创建流程对用户屏蔽,那么初始化实现的修改对用户来说就是无感知的。
  • 模板方法模式联系:如果工厂类创建 API 中用到的方法交给具体的子类实现的话,那么就用到了模板方法模式,如实例代码中的 create 接口中调用的 createProduct 和 register 就是由子类实现的。
  • 简单工厂模式:
    • 与工厂方法模式不同的是,简单工厂模式可能在 create 接口中直接封装了多种实例化方式,通过传入的参数条件判断,决定该走哪个流程。那么如果需要添加新的实例,则修改增加新的产品类,同时修改已经实现的 create 接口。对比工厂方法模式,则直接添加新的产品类和实现新的产品类,而不需要修改现有逻辑。

拓展

子类责任:在 JavaScript 中我们通常习惯将共有方法添加到父类(或原型链),这是站在子类(实例)的角度上思考的问题,希望上一层提供哪些方法。那么按照模板方法模式这种,则是站在父类的角度考虑问题,期望子类(或实例)实现哪些方法,也就是说子类具有实现在父类中所声明的抽象方法的责任,这种责任称为“子类责任”。


Content