工厂模式定义:“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();
这种方式 对原有的系统不会造成任何潜在影响
,所谓的“对扩展开放,对修改封闭”就比较圆满的实现了。
总结
大家现在回头对比一下抽象工厂和简单工厂的思路,思考一下:它们之间有哪些异同?
它们的共同点,在于都尝试去分离一个系统中变与不变的部分。它们的不同在于场景的复杂度。
在简单工厂的使用场景里,处理的对象是类,并且是一些相对简单的类——它们的共性容易抽离,同时因为逻辑本身比较简单,因而不期许代码很高的可扩展性。
抽象工厂本质上处理的也是类,但是是相对更加繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着很高的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:
-
抽象工厂(抽象类,它不能被用于生成具体实例)
: 用于声明最终目标产品的共性。在一个系统里抽象工厂可以有多个,每一个抽象工厂对应的这一类产品,被称为“产品族”。 -
具体工厂(用于生成产品族里的一个具体的产品)
: 继承自抽象工厂、实现了抽象工厂里声明的方法,用于创建具体的产品的类。 -
抽象产品(抽象类,它不能被用于生成具体实例)
: 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如借记卡、信用卡),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。 -
具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品)
: 文中具体的一张借记卡或信用卡或者组装的主机里的一个内存条、一块硬盘等。
请您自行验证核实并承担相关的风险与后果!
CoLaBug.com遵循[CC BY-SA 4.0]分享并保持客观立场,本站不承担此类作品侵权行为的直接责任及连带责任。
如您有版权、意见、投诉等问题,请通过[eMail]联系我们处理,如需商业授权请联系原作者/原网站。