Skip to content

创建型-抽象工厂

该小节需要理解 开放封闭

在实际的业务中,我们往往面对的复杂度并非数个类、一个工厂可以解决,而是需要动用多个工厂。

在上述的场景模拟中,最终的代码实现方案是这样的:

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

乍一看没什么问题,但是经不起推敲。首先映入眼帘的 Bug,是我们把 Boss 这个角色和普通员工塞进了一个工厂。大家知道,Boss 和基层员工在职能上差别还是挺大的,具体在员工系统里怎么表现呢?首先他的权限就跟咱们不一样。有一些系统,比如员工绩效评估的打分入口,就只有 Boss 点得进去,对不对?除此之外还有许多操作,是只有管理层可以执行的,因此我们需要对这个群体的对象进行单独的逻辑处理。

单从功能实现上来说,没问题。但是这样实现却是在挖坑——因为公司不仅仅只有这两类人,除此之外还有外包同学、还有保安,他们的权限、职能都存在着质的差别。如果延续这个思路,每考虑到一个新的员工群体,就回去修改一次 Factory 的函数体。这样做糟糕透了——首先,是Factory会变得异常庞大,庞大到你每次添加的时候都不敢下手,生怕自己万一写出一个Bug,就会导致整个Factory的崩坏,进而摧毁整个系统;其次,你坑死了你的队友:Factory 的逻辑过于繁杂和混乱,没人敢维护它;最后,你还连带坑了隔壁的测试同学:你每次新加一个工种,他都不得不对整个Factory 的逻辑进行回归——谁让你的改变是在 Factory 内部原地发生的呢!这一切悲剧的根源只有一个——没有遵守开放封闭原则

我们再复习一下开放封闭原则的内容:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。楼上这波操作错就错在我们不是在拓展,而是在疯狂地修改。

抽象工厂模式

以下面的示例做理解,来理解抽象工厂的概念和作用:

一部智能手机的基本组成是操作系统(Operating System,我们下面缩写作 OS)硬件(HardWare)组成。所以说如果我要开一个山寨手机工厂,那我这个工厂里必须是既准备好了操作系统,也准备好了硬件,才能实现手机的量产。考虑到操作系统和硬件这两样东西背后也存在不同的厂商,而我现在并不知道我下一个生产线到底具体想生产一台什么样的手机,我只知道手机必须有这两部分组成,所以我先来一个抽象类来约定住这台手机的基本组成:

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

楼上这个类,除了约定手机流水线的通用能力之外,啥也不干。如果你尝试让它干点啥,比如 new 一个 MobilePhoneFactory 实例,并尝试调用它的实例方法。它还会给你报错,提醒你“我不是让你拿去new一个实例的,我就是个定规矩的”。在抽象工厂模式里,楼上这个类就是我们食物链顶端最大的 Boss——AbstractFactory(抽象工厂)。

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

js
// 具体工厂继承自抽象工厂
class FakeStar extends MobilePhoneFactory {
  createOS() {
    // 提供Android系统
    return new AndroidOS();
  }
  createHardWare() {
    // 提供高通硬件
    return new HighPassHardWare();
  }
}

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

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

// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
  controlHardWare() {
    console.log('我会用安卓的方式去操作硬件') 
  }
}

class appleOS extends OS {
  controlHardWare() {
    console.log('我会用苹果的方式去操作硬件') 
  }
}

硬件也同理:

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

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

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

所以,现在我们要生产一台FakeStar手机时,我们只需要这样做:

js
const myPhone = new FakeStar()

// 让它拥有操作系统
const myOS = myPhone.createOS();

// 让它拥有硬件
const myHardWare = myPhone.createHardWare()

// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()

// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()

关键的来了,如果想产出一个新手机投入市场,就可以不需要对抽象工厂MobilePhoneFactory做任何修改,只需要拓展它的种类:

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

对比一下抽象工厂和简单工厂的思路,思考一下:它们之间有哪些异同?

它们的共同点,在于都尝试去分离一个系统中变与不变的部分。它们的不同在于场景的复杂度

在简单工厂的使用场景里,处理的对象是类,并且是一些非常好对付的类——它们的共性容易抽离,同时因为逻辑本身比较简单,故而不苛求代码可扩展性。

抽象工厂本质上处理的其实也是类,但是是一帮非常棘手、繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着千变万化的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:

  • 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里,抽象工厂可以有多个(大家可以想象我们的手机厂后来被一个更大的厂收购了,这个厂里除了手机抽象类,还有平板、游戏机抽象类等等),每一个抽象工厂对应的这一类的产品,被称为“产品族”。

  • 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品的类。

  • 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如操作系统、硬件等),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。

  • 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 比如我们上文中具体的一种操作系统、或具体的一种硬件等。

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