Skip to content

创建型模式 #7

Description

@edwineo

构造器模式

使用构造函数去初始化对象,就是应用了构造器模式

初始化两个用户:

const A = {
    name: 'AAA',
    age: 25,
    career: 'coder',
}

const B = {
    name: 'BBB',
    age: 24,
    career: 'product manager'
}

使用构造函数方式:

function User (name , age, career) {
  this.name = name
  this.age = age
  this.career = career 
}
const user = new User('lilei', 25, 'coder')

在创建一个user过程中,谁变了,谁不变?

很明显,变的是每个user的姓名、年龄、工种这些值,这是用户的个性,不变的是每个员工都具备姓名、年龄、工种这些属性,这是用户的共性

工厂模式

在使用构造器模式的时候,我们本质上是去抽象了每个对象实例的变与不变。那么使用工厂模式时,我们要做的就是去抽象不同构造函数(类)之间的变与不变

例如,我们创建的不同职位的用户之间,需要添加一个 work 标签,代表每个职位所做的工作:

function Coder (name , age) {
    this.name = name
    this.age = age
    this.career = 'coder' 
    this.work = ['写代码','写系分', '修Bug']
}

function ProductManager (name, age) {
    this.name = name 
    this.age = age
    this.career = 'product manager'
    this.work = ['订会议室', '写PRD', '催更']
}
...

现在我们有两个类(后面可能还会有更多的类),我们每次从数据库拿到一条数据,都要人工判断一下这个员工的工种,然后手动给它分配构造器吗?

不行,这也是一个“变”,我们把这个“变”交给一个函数去处理:

function Factory (name, age, career) {
  switch(career) {
    case 'coder':
      return new Coder(name, age) 
      break
    case 'product manager':
      return new ProductManager(name, age)
      break
      ...
}

但整个公司上下有数十个工种,难道要手写数十个类、数十行 switch 吗?我们需要将上述两段代码结合,进一步提取“变”与“不变”。

不同工种的员工,共同点是:都拥有 name、age、career、work 这四个属性这样的共性。

它们之间的区别是:

  1. 每个字段取值的不同
  2. work 字段需要随 career 字段取值的不同而改变。

接下来我们将共性和个性分离,将共性封装:

function User (name , age, career, work) {
    this.name = name
    this.age = age
    this.career = career 
    this.work = work
}

function Factory (name, age, career) {
    let work
    switch(career) {
        case 'coder':
            work =  ['写代码','写系分', '修Bug'] 
            break
        case 'product manager':
            work = ['订会议室', '写PRD', '催更']
            break
        case 'boss':
            work = ['喝茶', '看报', '见客户']
        case 'xxx':
            // 其它工种的职责分配
            ...
    }
            
    return new User(name, age, career, work)
}

总结一下,工厂模式其实就是将创建对象的过程单独封装。它很像我们去餐馆点菜:比如说点一份西红柿炒蛋,我们不用关心西红柿怎么切、怎么打鸡蛋这些菜品制作过程中的问题,我们只关心摆上桌那道菜。在工厂模式里,我传参这个过程就是点菜,工厂函数里面运转的逻辑就相当于炒菜的厨师和上桌的服务员做掉的那部分工作——这部分工作我们同样不用关心,我们只要能拿到工厂交付给我们的实例结果就行了。工厂模式的目的,就是为了实现无脑传参

抽象工厂模式

抽象工厂在很长一段时间里,都被认为是 Java/C++ 这类语言的专利。因为在强类型的静态语言中,用这些语言创建对象时,我们需要时刻关注类型之间的解耦,以便该对象日后可以表现出多态性。

但 JavaScript,作为一种弱类型的语言,它具有天然的多态性,好像压根不需要考虑类型耦合问题。而目前的 JavaScript 语法里,也确实不支持抽象类的直接实现,我们只能凭借模拟去还原抽象类。但抽象工厂的这种设计思想,是值得我们去学习的。

抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂

其中有四个关键角色:

  1. 抽象工厂(抽象类,它不能被用于生成具体实例)
  2. 具体工厂(用于生成产品族里的一个具体的产品)
  3. 抽象产品(抽象类,不能被用于生成具体实例)
  4. 具体产品(用于生成产品族里的一个具体的产品的更细粒度的产品)

如果我们需要一个工厂来生产手机,那么我们首先需要一个抽象类来约定住这台手机的基本组成

// 抽象工厂
class MobilePhoneFactory {
    // 提供操作系统的接口
    createOS(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
}

抽象工厂不干活,具体工厂来干活。

当我们明确了生产方案,明确我们要生产什么样的手机了之后,就可以化抽象为具体,比如我现在想要一个专门生产 Android 系统 + 高通硬件的手机的生产线,我给这类手机型号起名叫 FakeStar,那我就可以为 FakeStar 定制一个具体工厂:

// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
    createOS() {
        // 提供安卓系统实例
        return new AndroidOS() // 具体产品类
    }
    createHardWare() {
        // 提供高通硬件实例
        return new QualcommHardWare() // 具体产品类
    }
}

这里我们调用了两个构造函数:AndroidOS 和 QualcommHardWare,它们分别用于生成具体的操作系统和硬件实例。像这种被我们拿来用于 new 出具体对象的类,叫做具体产品类。但具体产品类往往不会孤立存在,不同的具体产品类往往有着共同的功能,比如安卓系统类和苹果系统类,它们都是操作系统,都有着可以操控手机硬件系统这样一个最基本的功能。因此我们可以用一个抽象产品类来声明这一类产品应该具有的基本功能。

// 抽象产品类
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 具体产品类继承自抽象产品类
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操作硬件')
    }
}
class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用苹果的方式去操作硬件')
    }
}
...

硬件类产品同理:

// 抽象产品类
class HardWare {
    // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
    operateByOrder() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 具体产品类
class QualcommHardWare extends HardWare {
    operateByOrder() {
        console.log('我会用高通的方式去运转')
    }
}

class MiWare extends HardWare {
    operateByOrder() {
        console.log('我会用小米的方式去运转')
    }
}
...

如此一来,当我们需要生产一台FakeStar手机时,我们只需要这样做:

// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()

// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()

如果我们需要再生产一款新种类的手机时,这时候我们不需要对抽象工厂MobilePhoneFactory做任何修改,只需要拓展它的种类:

class newStarFactory extends MobilePhoneFactory {
    createOS() {
        // 操作系统实现代码
    }
    createHardWare() {
        // 硬件实现代码
    }
}

这样操作,对原有的系统不会造成任何潜在影响。所谓的**“对拓展开放,对修改封闭”**就这么圆满实现了。

前面我们之所以要实现抽象产品类,也是同样的道理。

总结,抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂。只需留意以下三点:

  1. 学会用 ES6 模拟 JAVA 中的抽象类;
  2. 了解抽象工厂模式中四个角色的定位与作用;
  3. 对“开放封闭原则”形成自己的理解,知道它好在哪,知道执行它的必要性。

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。

那么,如何才能保证一个类仅有一个实例?

一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象:

class SingleDog {
    show() {
        console.log('我是一个单例对象')
    }
}

const s1 = new SingleDog()
const s2 = new SingleDog()

s1 === s2 // false

这里的 s1 和 s2 之间没有任何瓜葛,两者是相互独立的对象,各占一块内存空间。

而单例模式想要做到的是,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例

实现方式有两种

通过类的静态方法实现:

class SingleDog {
  static getInstance () {
    if (!SingleDog.instance) {
      SingleDog.instance = new SingleDog()
    }
    return SingleDog.instance
  }
}

const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

s1 === s2 // true

通过闭包实现:

class SingleDog {
  ...
}

const single = (function () {
  let instance = null
  return function () {
    if (!instance) {
      instance = new SingleDog()
    }
    return instance
  }
})()

const s1 = single()
const s2 = single()

s1 === s2 // true

生产实践:Vuex中的单例模式

近年来,基于 Flux 架构的状态管理工具层出不穷,其中应用最广泛的要数 Redux 和 Vuex。无论是 Redux 和 Vuex,它们都实现了一个全局的 Store 用于存储应用的所有状态。这个 Store 的实现,正是单例模式的典型应用。

Vuex如何确保Store的唯一性?

我们先来看看如何在项目中引入 Vuex:

// 安装vuex插件
Vue.use(Vuex)

// 将store注入到Vue实例中
new Vue({
    el: '#app',
    store
})

通过调用Vue.use()方法,我们安装了 Vuex 插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到Vue实例里去。也就是说每 install 一次,都会尝试给 Vue 实例注入一个 Store。

在 install 方法里,有一段和我们上面的 getInstance 非常相似的逻辑:

let Vue // 这个Vue的作用和楼上的instance作用一样
...

export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}

这就是 Vuex 源码中单例模式的实现办法了,套路可以说和我们的getInstance如出一辙。通过这种方式,可以保证一个 Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store。

原型模式

原型模式不仅是一种设计模式,它还是一种编程范式,是 JavaScript 面向对象系统实现的根基。

原型模式是 JavaScript 这门语言面向对象系统的根本。但在其它语言,比如 JAVA 中,类才是它面向对象系统的根本。所以说在 JAVA 中,我们可以选择不使用原型模式 —— 这样一来,所有的实例都必须要从类中来,当我们希望创建两个一模一样的实例时,就只能这样做:

Dog dog = new Dog('旺财', 'male', 3, '柴犬')
Dog dog_copy = new Dog('旺财', 'male', 3, '柴犬')

我们不得不把一模一样的参数传两遍,非常麻烦。而原型模式允许我们通过调用克隆方法的方式达到同样的目的,比较方便,所以 Java 专门针对原型模式设计了一套接口和方法,在必要的场景下会通过原型方法来应用原型模式。当然,在更多的情况下,Java 仍以“实例化类”这种方式来创建对象。

所以说在以类为中心的语言中,原型模式确实不是一个必选项,它只有在特定的场景下才会登场。

在 Java 等强类型语言中,原型模式的出现是为了实现类型之间的解耦。而 JavaScript 本身类型就比较模糊,不存在类型耦合的问题,所以说咱们平时根本不会刻意地去使用原型模式。因此我们此处不必强行把原型模式当作一种设计模式去理解,把它作为一种编程范式来讨论会更合适。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions