首页 > 技术文章 > 工厂模式·抽象工厂——理解“开放封闭”

zaisanshuiyifang 2021-11-02 12:03 原文

抽象工厂模式(Abstract Factory),提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。——《设计模式:可复用面向对象软件的基础》
一上来就是一个高(bu)端(neng)大(li)气(jie)的定义。抽象工厂这块知识,对入行以来一直写纯 JavaScript 的同学可能不太友好——因为抽象工厂在很长一段时间里,都被认为是 Java/C++ 这类语言的专利。Java/C++ 的特性是什么?它们是强类型的静态语言。用这些语言创建对象时,我们需要时刻关注类型之间的解耦,以便该对象日后可以表现出多态性。但 JavaScript,作为一种弱类型的语言,它具有天然的多态性,好像压根不需要考虑类型耦合问题。而目前的 JavaScript 语法里,也确实不支持抽象类的直接实现,我们只能凭借模拟去还原抽象类。因此有一种言论认为,对于前端来说,抽象工厂就是鸡肋。那么既然是鸡肋,我们要不要去学习和了解它呢,以及为什么会将抽象工厂放在第二篇,文末会给出答案。我们接着上篇文章的例子继续走。

一个不简单的简单工厂引发的命案

在实际的业务中,我们往往面对的复杂度并非几个类、一个工厂可以解决,而是需要动用多个工厂。我们继续看上个小节举出的例子,简单工厂函数最后长这样

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)
}

现在我们通过一个类和一个工厂模式将员工的信息,那么问题来了,我们这只是录入了员工的信息,如果我们还要录入boss这个角色的信息呢,在一般的系统中boss的权限和基层的员工是不一样的,有的同学可能会这么想,在Factory工厂里增加authority判断,然后再User类里面添加power 属性不就好了。但是仔细想一想这么设计的后果吧,公司可能有外包同学,保安大叔,有临时到访游客,各种角色,那么每次添加一次角色就要去修改一下Factory函数体。这十几个角色下来,你有没有发现你已经是公司的核心开发了,因为大家都不敢改也不敢测你这一块代码。。。。

function User(name , age, career, work,power) {
    this.name = name
    this.age = age
    this.career = career 
    this.work = work
    this.power = power
}
function Factory(name, age, career, authority) {
    let work
      if(authority==='admin'){
          switch(career) {
            case 'forntEndcoder':
                work =  ['写代码','写系分', '修Bug'] 
                power= ['111','222','333']
                break
            case 'product manager':
                work = ['订会议室', '写PRD', '催更']
                power= ['222','333','444']
                break
            case 'boss':
                power= ['111','222','333','444']
                work = ['喝茶', '看报', '见客户']
            case 'xxx':
                // 其它工种的职责分配
             // 其他权限分配
              ... 
            return new User(name, age, career, work, power)
          }
      }
    if(authority==='commonUser'){
          switch(career) {
            case 'forntEndcoder':
                work =  ['写代码','写系分', '修Bug'] 
                power= ['111','222','333']
                break
            case 'product manager':
                work = ['订会议室', '写PRD', '催更']
                power= ['222','333','444']
                break
            case 'boss':
                power= ['111','222','333','444']
                work = ['喝茶', '看报', '见客户']
            case 'xxx':
                // 其它工种的职责分配
             // 其他权限分配
              ... 
            return new User(name, age, career, work, power)
          }
      }
    }

那么以上的问题是怎么产生的呢,以及如何去设计我们的代码模式呢?
答:这一切悲剧的根源只有一个——没有遵守开放封闭原则。我们再复习一下开放封闭原则的内容:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。楼上这波操作错就错在我们不是在拓展,而是在疯狂地修改。

抽象工厂模式

上面这段可能仍有部分同学觉得抽象,也没关系。这里咱们先不急着理解透彻这个干巴巴的概念,先来看这么一个示例:

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

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

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

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

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

这里我们在提供安卓系统的时候,调用了两个构造函数:AndroidOS 和 QualcommHardWare,它们分别用于生成具体的操作系统和硬件实例。像这种被我们拿来用于 new 出具体对象的类,叫做具体产品类(ConcreteProduct)。具体产品类往往不会孤立存在,不同的具体产品类往往有着共同的功能,比如安卓系统类和苹果系统类,它们都是操作系统,都有着可以操控手机硬件系统这样一个最基本的功能。因此我们可以用一个抽象产品(AbstractProduct)类来声明这一类产品应该具有的基本功能(众:什么抽象产品???要这些玩意儿干啥?老夫写代码就是一把梭,为啥不让我老老实实一个一个写具体类???大家稍安勿躁,先把例子看完,下文会有解释)

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

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

class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用

推荐阅读