在23种经典设计模式中,最开始的设计模式——工厂方法模式,抽象工厂模式和未被归纳到设计模式中但经常被OOP开发所采用的简单工厂方法模式之间有着巨大的共性,和具体应用层面的区别。
本文试图把这些工厂类设计模式对开发有影响的共性和区别通过比较的方式阐述清楚,帮助读者掌握这三种设计模式的应用。
开闭原则
在甄别这几种设计模式前我们先理解一个原则:开闭原则
“软件中的对象应该对于扩展是开放的,但是对于修改是封闭的” ——维基定义
开闭原则是五大设计原则(SOLID)中最为抽象的一个,因此它的描述也极为简单易懂。它的目的单一,就是为了迎合改变(ready for change)。满足这个原则的程序在出现需求更改,维护变动的时候可以将代码改变量减小到最小,从而很容易地实现产品的升级迭代,大大增强软件的“生命力”。
了解开闭原则以及目的后我们看看简单工厂方法。
简单工厂方法
简单工厂方法也称静态工厂方法,其实质就是在用户直接调用所需应用类构造器生成应用类的过程中加一堵墙,使得用户与应用类分离开,耦合度下降。
Track track = new ConcreteTrack();
变成了
Track track = new TrackFactory().createTrack();//返回的是ConcreteTrack实例
这样用户将获取具体应用类的过程变成了获取工厂,调用工厂方法的过程。用户与具体应用类的直接联系就被工厂这堵墙隔绝了,减少了具体类的暴露,从而客户不会对具体类进行过度依赖,开发者日后修改具体类也就不会对滥用具体应用类的客户端造成致命打击。这是防暴露层面简单工厂的好处,而这一好处是所有工厂方法所共有的。
顺便问一下,简单工厂为什么也叫静态工厂呢?
直观解释往往是”因为简单工厂提供的生成产品的方法是静态的“。但是实际上这不是必要的,你也可以把这个生成产品的方法写成非静态方法,所以不要误认为简单工厂生成产品的方法都是静态的。
我们接下来称工厂生成产品的方法为产品方法。那么简单工厂或静态工厂方法的核心就是其产品方法的“单一”属性。
这个单一有两个层面的解释。
第一,方法数量的单一。往往一个静态工厂中只有一个产品方法。但是问题是,这个工厂却需要具备生成多个产品的能力,因此开发者需要把生成多个产品的逻辑全塞在这一个方法中。于是,开发者就无法通过继承等OOP手段让这个工厂的设计符合开闭原则,因为在扩展升级中,你不能通过继承去扩充父类方法的具体逻辑,你只能直接修改父类产品方法的逻辑去实现扩充。所以采取纯静态工厂方法,你就面临每次增加新产品需求就必须修改原工厂产品方法代码的窘境。然而修改产品方法的逻辑显然违背对修改封闭的原则。
所以静态方法的致命缺陷就显露了出来,它是这三个设计模式中唯一不满足开闭原则的设计模式,它不具备很好的后期扩展性。但是由于它是最简单的工厂类设计模式,它直白地展示出工厂类设计模式的优点(隔离客户与具体类),并且容易被理解应用,实现代价也小,所以在产品种类少,变化考虑非首要的场景下它经常被使用。
第二,架构层次单一。静态方法不需要人们为它特意准备一个接口,它可以直接根据开发者的需要去创建具体类,所以在接口,抽象类,实现类三个架构层次中它只需要实现类这一个层次。这样,采用简单工厂方法设计的整体架构就变的很简洁。
工厂方法模式
工厂方法可以理解为静态方法的进化,具体来讲就是“单一”属性进化成“多个”。
在架构层次上我们扩充工厂的接口层,并把重点放到接口层而非实现类。我们定义一个工厂接口,其中规定好产品方法的signature,用产品的统一接口来指定返回值的类型。这样工厂就具备了至少两个层次,工厂接口层次和具体工厂实现类层次。
说到这里提一下产品的架构层次,很简单,同一类产品应该实现同一个接口(如汽车类产品应实现汽车接口),所以产品是有接口层的,又抽象类层不是必要的,所以产品也至少有两个层次。
这里我们规定实现同一个接口的产品的集合被称为一个产品等级结构,为后面介绍抽象工厂做准备。
说回工厂方法的架构层次数,它相对静态共产方法就是“多个”。而这样做带来了巨大的好处,我们可以通过实现接口来实现对新工厂和对应产品的拓展,完美地满足了开闭原则。每当我们需要扩展新的产品时,我们不再是对一个具体实现类添加生成产品的逻辑,而是新建一个实现类,在里面实现产品方法。关闭对已有代码的修改,而打开了实现接口进行拓展的大门。
另一个层面上,在实际开发中随着产品种类的增加,工厂实现类的数量也增加了,从单一变成多个。而这也就引出了工厂方法的缺点,随着产品种类的数量扩充,工厂实现类的数量也一同扩充,工厂类的数量有时会发生暴涨,带来巨大的代码代价。因此,实际开发中可以采取静态工厂方法与工厂方法结合的策略减少过多的工厂类。
抽象工厂方法
抽象工厂方法又可以理解成是对工厂方法的进化,原则也可以理解成是“单一”属性变成“多个”。
那是什么属性从单一变成了多个了呢?
一. 产品等级结构单一变成了多个。
简单的说就是开发产品的种类变多了,以前只要造汽车的引擎,现在连汽车的玻璃,轮子也要一起造。
这里我们再定义产品族的概念:搭配起来一起使用的不同类产品(来自不同产品等级结构的产品)的集合理解为一个产品族。
例子:造一个行星系统,那么恒星和行星就是不同类的产品,太阳和地球就是这两类产品的具体类,如果我们要造太阳系,那么这两个产品就会被搭配起来使用,这时太阳和地球就变成了一个产品族。
二. 工厂的产品方法从单一变成了多个。
无论是接口还是具体工厂实例,同一个工厂不再受限制于生成单一的产品,它开始具备生成一整个产品族(多个不同类产品)的能力。
例子:我们要造一个行星系统,我们调用“宇宙工厂”的产品方法生成恒星,行星,轨道,这三种产品。再造原子系统,调用“原子工厂”又会生成原子核,电子,能量轨道。而宇宙工厂和原子工厂是实现同一个接口的工厂实现类,他们分别满足我们对不同场景对多个不同产品的需求。
抽象工厂是实际应用中最广泛的工厂类方法,因为它考虑的情况最复杂最接近实际开发。它是在工厂方法上进化过来的,它通过把工厂方法集成带来便利的同时却也带来了弊端。
开闭原则的倾斜性
“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的。对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:
1. 增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。
2. 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,不能很好地支持“开闭原则”。
抽象工厂模式的这种性质称为“开闭原则”的倾斜性,抽象工厂模式以一种倾斜的方式支持增加新的产品,它为新产品族的增加提供方便,但不能为新的产品等级结构的增加提供这样的方便。所以当出现新的产品等级结构需求时,它就变的和静态工厂方法一样无法适应开闭原则。
总结
简单工厂方法,工厂方法,抽象工厂是一个层层进化的过程。每次进化都是前一个设计模式中某个“单一”层面变成“多个”从而将考虑情况变的复杂,适用场景变的广泛。
简单工厂方法将所有产品生成的逻辑塞在同一个产品方法中,不适合大规模变化拓展,但是实现简单。
工厂方法通过实现接口来拓展新的工厂,提供新的产品方法,满足开闭原则。它适合不确定的产品类的生成,但是容易因产品过多而产生繁琐的工厂类和代码。
抽象工厂方法把工厂的产品方法变多了,这样一个工厂可以产生多个产品,产品成体系地被工厂产生。适用于复杂的产品需求环境,同时又因为开闭原则倾斜性,产品的大类不适合拓展。
在实际开发中切记他们适用的开发场景,以减小他们的弊端,提高价值。面对复杂的场景,应试图在这些模式上组合拓展,而不是死板地套用单个设计模式。