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 许可证)