工厂模式定义:“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();
这种方式对原有的系统不会造成任何潜在影响,所谓的“对扩展开放,对修改封闭”就比较圆满的实现了。
总结
大家现在回头对比一下抽象工厂和简单工厂的思路,思考一下:它们之间有哪些异同?
它们的共同点,在于都尝试去分离一个系统中变与不变的部分。它们的不同在于场景的复杂度。
在简单工厂的使用场景里,处理的对象是类,并且是一些相对简单的类——它们的共性容易抽离,同时因为逻辑本身比较简单,因而不期许代码很高的可扩展性。
抽象工厂本质上处理的也是类,但是是相对更加繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着很高的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:
- 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里抽象工厂可以有多个,每一个抽象工厂对应的这一类产品,被称为“产品族”。
- 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的方法,用于创建具体的产品的类。
- 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如借记卡、信用卡),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
- 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 文中具体的一张借记卡或信用卡或者组装的主机里的一个内存条、一块硬盘等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。