JavaScript设计模式-抽象工厂模式

系统结
• 阅读 2771
工厂模式定义:“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();

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

总结

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

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

  • 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里抽象工厂可以有多个,每一个抽象工厂对应的这一类产品,被称为“产品族”。
  • 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的方法,用于创建具体的产品的类。
  • 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如借记卡、信用卡),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
  • 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 文中具体的一张借记卡或信用卡或者组装的主机里的一个内存条、一块硬盘等。
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
3年前
List的Select 和Select().tolist()
List<PersondelpnewList<Person{newPerson{Id1,Name"小明1",Age11,Sign0},newPerson{Id2,Name"小明2",Age12,
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
00_设计模式之语言选择
设计模式之语言选择设计模式简介背景设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。设计模式(Designpattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
系统结
系统结
Lv1
世间无限丹青手,一片伤心画不成。
文章
2
粉丝
0
获赞
0