工厂模式定义:“Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.”(在基类中定义创建对象的一个接口,让子类决定实例化哪个类。工厂方法让一个类的实例化延迟到子类中进行。)

抽象工厂这块知识,对入行以来一直写纯 JavaScript 的同学可能不太友好——因为抽象工厂在很长一段时间里,都被认为是 Java/C++ 这类语言的专利。

Java/C++ 的特性是什么?它们是强类型的静态语言。用这些语言创建对象时,我们需要时刻关注类型之间的解耦,以便该对象日后可以表现出多态性。但 JavaScript,作为一种弱类型的语言,它具有天然的多态性,好像压根不需要考虑类型耦合问题。而目前的 JavaScript 语法里,也确实不支持抽象类的直接实现,我们只能凭借模拟去还原抽象类。因此有一种言论认为,对于前端来说,抽象工厂就是鸡肋。

但现在,不要看到“抽象”两个字转身就走,鸡肋不鸡肋理解清楚了才有发言权。

简单工厂案例后续

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

我们继续看上个小节举出的例子,简单工厂函数最后长这样:

function Factory(name, age, career) {
    var work;
    switch(career) {
        case 'employees':
            work = ["办存款", "放贷款", "收贷款"];
        case 'president':
            work = ["喝茶", "看报纸", "..."];
        case 'chairman':
            work = ["喝水", "放贷签字", "开会"];
        case xxx:
            // 工种对应职责
        ...
    }
    return new User(name, age, career, work);
}

乍看之下是没什么问题,但仔细看上去首个问题就是我们把行长和普通职工放在了一起。行长和职工在职能上的差别还是很大的:首先,权限不同;其次,对一个系统的操作也不同;再者,......

那怎么办呢?要在工厂方法里加入相关的逻辑判断吗?单从功能实现上是没有问题的。但这么做实则在挖坑,因为银行的工种多着呢,不止有行长、普通职工、还有主任、支行长、分行长等,他们的权限、职能有很大的不同。如果按照这个思路,每出现一个工种就在 Factory 增加相应的逻辑,那首先会造成这个工厂方法异常庞大,大到最终你不敢增加/修改任何地方,生怕导致 Factory 出现 bug 影响现有系统逻辑,也使得其难以维护。其次,每增加一个工种的逻辑就需要测试人员对 Factory 方法整个逻辑进行回归,给测试人员带来额外的工作量。而这一切的源头就是没有遵守软件设计的开放封闭原则。我们再复习一下开放封闭原则的内容:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。

抽象工厂模式

我们先不急于理解具体的概念,先来看下面的例子:

有一天我们来到银行,给大堂经理说我要办一张借记卡、一张信用卡。无论什么卡都有相同的属性,比如都可以存钱(虽然信用卡存钱没有利息)、转账(假装信用卡可以转账)。对于银行也一样,农行可以办卡,工行也可以办卡,那么这两家银行也具备同样的功能。

又有一天我们想组装一台主机,我们知道主机由内存条、硬盘、CPU、电源、显卡等组成,而内存条、硬盘等部件也有很多不同品牌厂家生产,一时之间我们定不好想组装一台什么配置的主机。没关系,我们可以先约定一个抽象主机类,让它具有各种硬件属性,接着在对各硬件进行抽象,这样我们就拥有了抽象工厂类和抽象产品类。

上面的场景是属于抽象工厂的例子,卡类属于抽象产品类,制定产品卡类所具备的属性,而银行和之前的工厂模式一样,负责生产具体产品实例,通过大堂经理就可以拿到卡。其实,银行也可以被抽象为银行类,继承这个类的银行实例都有办卡的功能,这样就完成了抽象类对实例的约束。

示例的代码实现

// 抽象工厂类
class BankFactory {
  constructor() {
    if (new.target === BankFactory) {
      throw new Error("抽象工厂类不能直接实例化!");
    }
  }
  // 抽象方法-办卡
  createBankCard() {
    throw new Error("抽象工厂类不允许直接调用,请重写实现!");
  }
  // 抽象方法-存钱
  saveMoney() {
    throw new Error("抽象工厂类不允许直接调用,请重写实现!");
  }
}
// 具体银行类
class Icbc extends BankFactory {
  createBankCard(type) {
    switch (type) {
      case "debit":
        return new DebitCard();
      case "credit":
        return new CreditCard();
      default:
        throw new Error("暂时没有这个产品!");
    }
  }
}
// 抽象产品类
class Card {
  // 抽象产品方法
  buy() {
    throw new Error("抽象产品方法不允许直接调用,请重新实现!");
  }
  transfer() {
    throw new Error("抽象产品方法不允许直接调用,请重新实现!");
  }
}
// 具体借记卡类
class DebitCard extends Card {
  buy() {
    console.log("您可以使用工行借记卡进行消费了!");
  }
  transfer() {
    console.log("您可以使用工行借记卡进行转账了!");
  }
}
// 具体信用卡类
class CreditCard extends Card {
  buy() {
    console.log("您可以使用工行信用卡进行消费了!");
  }
  transfer() {
    console.log("您可以使用工行信用卡进行转账了!");
  }
}
const myBank = new Icbc();
const myCard = myBank.createBankCard("debit");
myCard.buy();

这种方式对原有的系统不会造成任何潜在影响,所谓的“对扩展开放,对修改封闭”就比较圆满的实现了。

总结

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

它们的共同点,在于都尝试去分离一个系统中变与不变的部分。它们的不同在于场景的复杂度。
在简单工厂的使用场景里,处理的对象是类,并且是一些相对简单的类——它们的共性容易抽离,同时因为逻辑本身比较简单,因而不期许代码很高的可扩展性。
抽象工厂本质上处理的也是类,但是是相对更加繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着很高的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:

  • 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里抽象工厂可以有多个,每一个抽象工厂对应的这一类产品,被称为“产品族”。
  • 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的方法,用于创建具体的产品的类。
  • 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如借记卡、信用卡),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
  • 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 文中具体的一张借记卡或信用卡或者组装的主机里的一个内存条、一块硬盘等。

杜子李_
0 声望0 粉丝