精读《设计模式 - Factory Method 工厂方法》

Factory Method(工厂方法)

Factory Method(工厂方法)属于创建型模式,利用工厂方法创建对象实例而不是直接用 New 关键字实例化。

理解如何写出工厂方法很简单,但理解为什么要用工厂方法就需要动动脑子了。工厂方法看似简单的将 New 替换为一个函数,其实是体现了面向接口编程的思路,它创建的对象其实是一个符合通用接口的通用对象,这个对象的具体实现可以随意替换,以达到通用性目的。

意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。

举例子

如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。

换灯泡

我自己在家换过灯泡,以前我家里灯坏掉的时候,我看着这个奇形怪状的灯管,心里想,这种灯泡和这个灯座应该是一体的,市场上估计很难买到适配我这个灯座的灯泡了。结果等我把灯泡拧下来,跑到门口的五金店去换的时候,店员随便给了我一个灯泡,我回去随便拧了一下居然就能用了。

我买这个灯泡的过程就用到了工厂模式,而正是得益于这种模式,让我可以方便在家门口就买到可以用的灯泡。

卡牌对战游戏

卡牌对战中,卡牌有一些基本属性,比如攻防、生命值,也符合一些通用约定,比如一回合出击一起等等,那么对于战斗系统来说,应该怎样实例化卡牌呢?如何批量操作卡牌,而不是通用功能也要拿到每个卡牌的实例才能调用?另外每个卡牌有特殊能力,这些特殊能力又应该如何拓展呢?

实现任意图形拖拽系统

一个可以被交互操作的图形,它可以用鼠标进行拉伸、旋转或者移动,不同图形实现这些操作可能并不相同,要存储的数据也不一样,这些数据应该独立于图形存储,我们的系统如果要对接任意多的图形,具备强大拓展能力,对象关系应该如何设计呢?

意图解释

在使用工厂方法之前,我们就要创建一个 用于创建对象的接口,这个接口具备通用性,所以我们可以忽略不同的实现来做一些通用的事情

换灯泡的例子来说,我去门口五金店买灯泡,而不是拿到灯泡材料自己 New 一个出来,就是因为五金店这个 “工厂” 提供给我的灯泡符合国家接口标准,而我家里的灯座也符合这个标准,所以灯座不需要知道对接的灯泡是具体哪个实例,什么颜色,什么形状,这些都无所谓,只要灯泡符合国家标准接口,就可以对接上。

对卡牌对战的系统来说,所有卡牌都应该实现同一种接口,所以卡牌对战系统拿到的卡牌应该就是简单的 Card 类型,这种类型具备基本的卡片操作交互能力,系统就调用这些能力完成基本流程就好了,如果系统直接实例化具体的卡片,那不同的卡片类型会导致系统难以维护,卡片间操作也无法抽象化。

正式这种模式,使得我们可以在卡牌的具体实现上做一些特殊功能,比如修改卡片攻击时效果,修改卡牌销毁时效果。

对图形拖拽系统来说,用到了 “连接平行的类层次” 这个特性,所谓连接平行的类层次,就是指一个图形,与其对应的操作类是一个平行抽象类,而一个具体的图形与具体的操作类则是另一个平行关系,系统只要关注最抽象的 “通用图形类” 与 “通用操作类” 即可,操作时,底层可能是某个具体的 “圆类” 与 “圆操作类” 结合使用,具体的类有不同的实现,但都符合同一种接口,因此操作系统才可以把它们一视同仁,统一操作。

意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。

所以接口是非常重要的,工厂方法第一句话就是 “定义一个用于创建对象的接口”,这个接口就是 Creator,让子类,也就是具体的创建类(ConcreteCreator)决定要实例化哪个类(ConcreteProduct)。

所谓使一个类的实例化延迟到其子类,是因为抽象类不知道要实例化哪个具体类,所以实例化动作只能由具体的子类去做,这样绕一圈的好处是,我们可以将任意多对象看作是同一类事物,做统一的处理,比如 无论何种灯泡实例都满足通用的灯座接口所有工厂实例化的卡牌都具备玩一局卡牌游戏的基本功能任何图形与交互类都满足特定功能关系,这种思想让生活和设计得到了大幅简化。

结构图

Creator 就是工厂方法,ConcreteCreator 是实现了 Creator 的具体工厂方法,每一个具体工厂方法生产一个具体的产品 ConcreteProduct,每个具体的产品都实现通用产品的特性 Product

代码例子

下面例子使用 typescript 编写。

// 产品接口
interface Product {
  save: () => void;
}

// 工厂接口
interface Creator {
  createProduct: () => Product;
}

// 具体产品
class ConcreteProduct implements Product {
  save = () => {};
}

// 具体工厂
class ConcreteCreator implements Creator {
  createProduct = () => {
    return new ConcreteProduct();
  };
}

创建一个 Product 的子类 ConcreteCreator,并返回一个实现了 Product 的具体实例 ConcreteProduct,这样我们就可以方便使用这个工厂了。

工厂方法并不是直接调用 new ConcreteCreator().createProduct 那么简单,这样体现不出任何抽象性,真正的场景是,在一个创建产品的流程中,我们只知道拿到的工厂是 Creator

function main(anyCreator: Creator) {
  const product = anyCreator.createProduct()
}

在外面调用 main 函数时,实际传进去的是一个具体工厂,比如 myCreator,但关键是 main 函数不用关心到底是哪一个具体工厂,只要知道是个工厂就行了,具体对象创建过程交给了其子类。

你也许也发现了,这就是抽象工厂中其中的一步,所以抽象工厂使用了工厂方法。

弊端

工厂方法中,每创建一种具体的子类,就要写一个对应的 ConcreteCreate,这相对比较笨重,但有意思的是,如果将创建多个对象放到一个 ConcreteCreate 中,就变成了 简单工厂模式,新增产品要修改已有类不符合开闭模式,反而推荐写成本文说的这种模式。

彼之毒药吾之蜜糖,要知道没有一种设计模式解决所有问题,没有一种设计模式没有弊端,而这个弊端不代表这个设计模式不好,一个弊端的出现可能是为了解决另一个痛点。 要接受不完美的存在,这么多种设计模式就是对应了不同的业务场景,为合适的场景选择一种能将优势发扬光大,以至于能掩盖弊端,就算进行了合理的架构设计

总结

工厂方法并不是简单把 New 的过程换成了函数,而是抽象出一套面向接口的设计模式:

你看,我要做灯泡,可以直接做具体的灯泡,也可以定一个灯泡接口,通过灯泡工厂拿到具体灯泡,灯泡工厂对待所有灯泡的只做流程都是一样的,不管是中世纪风灯泡,还是复古灯泡,还是普通白织灯,都是一模一样的制作流程,具体怎么做由具体的子类去实现,这样我们可以统一管理 “灯泡” 这一个通用概念,而忽略不同灯泡之间不太重要的差别,程序的可维护性得到了大幅提升。

讨论地址是:精读《设计模式 - Factory Method 工厂方法》· Issue #274 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证
阅读 248

推荐阅读
前端精读专栏
用户专栏

精读前端业界好文,每周更新

6502 人关注
108 篇文章
专栏主页