在 GoF 的《设计模式》一书中,工厂模式被分为了三种:简单工厂、工厂方法和抽象工厂。(不过,在书中作者将简单工厂模式看作是工厂方法模式的一种特例。)在实际工作中,用得比较多的就是工厂方法模式和抽象工厂模式这两类。本篇文章,我们就先看一下抽象工场模式。
学习抽象工厂模式真正的重点和难点在于:如何找到正确的抽象。虽然抽象工厂模式很容易实现,但更重要的是我们要能意识到“正确的抽象往往都很简单也很底层”,比如,数据库的增删改查操作,日志的 debug、info、warn、error 级别,JVM 内存模型,等等。其实,抽象工厂模式想要告诉我们的不只是在写代码时随便建个抽象类就够了,而是当我们自己在对一类功能进行抽象分析时有没有找到足够简单而又通用的正确抽象。
一、模式原理分析
抽象工厂模式的原始定义是:提供了一个用于创建相关或相关对象族的接口,而无须指定其具体类。
实际上,这句话是给使用者说的。因为从使用者的角度来看,他有时可能只关心某一个抽象的大类,就好比你去租车时,你对店员说,你想要租一辆小型轿车,但具体品牌和型号你并不在意。而站在创建者的角度看,他需要关注的是如何找到这个正确的抽象大类,就好比在上面的租车场景中,你需要从普通的汽车消费者变成汽车厂的厂长一样,你必须关注最后具体的型号该怎么落地。
所以说,当我们在创建抽象工厂模式时,最终还是会涉及指定具体的实现类。换句话说,定义只是说了抽象工厂模式应该要朝着分析共性规律的方向走,而具体操作时我们还得仔细分析具体实现类该怎么实现才行。
我们再来看看抽象工厂模式原始的 UML 图:
从这个 UML 图中,我们能看出抽象工厂模式中其实包含了四个关键角色。
-
抽象工厂;
-
抽象产品(通用的一类对象或接口);
-
具体工厂;
-
具体产品(继承通用对象或接口后扩展特有属性)。
为了便于更好理解,我们这里打个比方:比如说,抽象工厂生产的抽象产品是椅子、桌子、沙发一类的家具,那具体工厂可能就在生产具体的产品:椅子设计成现代简约风格或欧洲宫廷风格,使用的材质有木质或铝制,等等。本质上椅子的特性没有发生重大改变,但在外观上,不同的具体工厂生产的椅子尺寸、材质、外观各不相同。
其中最为关键的角色并不是抽象工厂本身,而是抽象产品。抽象产品的好坏才是直接决定了抽象工厂和具体工厂能否发挥最大作用的关键所在。这也是我们在前面原则模块和思维模块里多次提到的“找到正确的抽象很重要”的原因。
明白了这个道理后,再来看下面 UML 图的代码实现,会发现思路特别清晰。
代码实现了两个不同国家的家具制造工厂。其中,AbsractFactory 是抽象工厂,创建的椅子、沙发和桌子是抽象产品;中国工厂和美国工厂是具体工厂,通过中国工厂或美国工厂制作的家具是具体产品。当我们只想买椅子、沙发和桌子时,只需要告诉抽象工厂就行,可能得到的是中国生产的,也可能是美国生产的。
从上面代码实现中我们可以看出,抽象工厂模式向使用(客户)方隐藏了下列变化:
-
程序所支持的实例集合(具体工厂)的数目;
-
当前是使用的实例集合中的哪一个实例;
-
在任意给定时刻被实例化的具体类型;
-
实例集变化的依据。
这些变化也给了我们一些启示,对于软件使用者来说,他们其实更关心某一组产品的某些共性功能,至于这些功能具体的实现他们并不在意。反过来,对于软件创建者来说,他们要找到正确的共性功能,并尽可能隐藏具体的实现细节,始终围绕着提供符合共性功能的软件。比如,Spring 框架就是始终围绕着如何正确地管理(创建、使用、销毁)Java 对象生命周期这个共性功能。
所以说,在理解抽象工厂模式原理时,一定要牢牢记住“如何找到某一个类产品的正确共性功能”这个重点。
二、使用场景分析
实际上,抽象工厂模式在现实中有很多应用。
比如,当我们需要在一个应用程序中支持多个操作系统时,就会用到像抽象工厂模式这样的机制,需要为目前应用程序所使用的操作系统(Windows、Mac、Linux)选择正确的硬件驱动程序集合(包括磁盘驱动程序、显示驱动程序、IO 外设驱动程序等)。
再比如,在电商系统中,国内电商和海外电商都需要使用类似商品、订单、物流等系统,但是不同地区的政策条件不同、购买习惯不同,即便是同样的线上购物流程,也会存在不同的具体代码实现。这种情况下使用抽象工厂模式就是一个很好的方式,不仅能提高代码的可移植性,还能找到不同地区的差异性。
简单来说,在软件开发中,抽象工厂模式的使用场景主要就是解决跨平台兼容性的问题。
这里我们还是通过一个例子来帮助你理解抽象工厂模式的使用场景。在 Spring 框架中的 BeanFactory 就是最早实现抽象工厂模式的代码,如下所示:
BeanFactory 在 Spring 中是实现 IoC 容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。实现这个接口的抽象工厂类有很多,比如,AbstractBeanFactory 等。
而围绕 AbstractBeanFactory 的具体实现类,有 DefaultListableBeanFactory、XmlBeanFactory 等。这里,BeanFactory 就是抽象产品,AbstractBeanFactory 是抽象工厂,XmlBeanFactory 是具体工厂,通过 XML 注入的 Bean 实例就是最终通过 XmlBeanFactory 具体实现的产品。
总结来说,在实际的代码实现中,抽象工厂模式体现为定义一个抽象工厂类,多个不同的具体工厂继承这个抽象工厂类后,再各自实现相同的抽象功能,进而实现代码上的多态性。
三、为什么要使用工厂模式
分析完抽象工厂模式的原理和使用场景后,我们再来说说使用抽象工厂模式的原因,主要有以下三点。
第一点,对于不同产品系列有比较多共性特征时,可以使用抽象工厂模式,有助于提升组件的复用性。 比如,不同的数据库产品,JDBC 就是对于数据库增删改查建立的抽象工厂模式,无论使用什么类型的数据库,只要具体的数据库组件能够支持 JDBC,就能对数据库进行读写操作,这极大地提高了我们对不同数据库组件的复用性。
第二点,当需要提升代码的扩展性并降低维护成本时,把对象的创建和使用过程分开,能有效地将代码统一到一个级别上。比如,你需要创建统一的日志监控,但不同应用使用的日志收集代理可能各不相同,这时如果有一个统一的日志收集工厂定义抽象的日志收集功能,那么不同的代理只需要按照各自的实现方式提供统一的日志收集功能即可,这样即便以后新增了一些代理,也不会影响旧的功能,提升扩展性的同时也能提升维护性。
第三点,解决跨平台带来的兼容性问题。 抽象工厂模式提供了一种解决跨平台问题的思路,也就是我们的后台服务应该尽可能地使用更高层级的统一的抽象功能,然后通过不同客户端的适配程序来实现统一的功能交付。比如,同一个地区里,安卓或 iOS 的客户端 App 通过 API 网关访问商品数据时,应该是先获取统一的抽象数据对象,然后经过安卓或 iOS 的客户端适配器程序的适配转换和传输,而不是针对具体型号的手机(华为、小米、苹果等)来单独进行适配,安卓和 iOS 这里就被看作是不同手机型号的抽象工厂模式。
四、抽象工厂模式有什么优缺点?
那使用抽象工厂模式我们能收获什么呢?也就是抽象工厂模式的优点有哪些呢?
-
符合开闭原则。 当我们需要添加新的工厂类时,只用新继承一个类,不用修改抽象工厂和其他具体类。
-
可以保证同一工厂生成的产品符合预期。 通过抽象工厂定义了统一的抽象产品功能,只要是继承了这个抽象工厂,本质的抽象产品功能是不会发生改变的。
-
将使用和创建的代码进行解耦。 在具体的代码实现中,使用者只用关心如何使用具体的功能,而不再需要关心这个对象是怎么创建的。这样对象与对象之间的耦合关系变得更单一,降低了过多应用带来的耦合风险。
-
满足单一职责原则。 由于我们将产品的实现代码放到同一层级里,并继承同一个抽象工厂类,所以说,即便具体的代码风格有所不同,也不影响最终提供功能的统一性,使得代码的可维护性大大提高。
-
容易增加新的产品系列。 因为有了抽象工厂作为参考模板,那么再新增新的具体工厂时非常容易,不需要修改其他具体工厂,并且各自的工厂可以朝着自己的演化方向发展。
同样,除了以上优点外,抽象工厂模式也有一些缺点。
-
增加代码量。 虽然抽象工厂模式很好地进行了职责分离,但因此也增加了更多的类文件和代码行数,使得开发时间变长,并且随着实现子类越来越多,可能当一个抽象工厂想要增加抽象时,影响的代码范围会很大。
-
增加学习成本。 抽象工厂模式是自顶向下式的设计,无论是入门级的程序员还是资深程序员,一开始就找到正确的抽象是一件非常困难的事情。这可能需要学习大量的实践案例,并不断总结归纳才有可能做得更好。
-
变更产品的结构困难。 抽象工厂模式最大的缺点在于,一旦定义了某种产品结构后,要想修改就得修改所有的具体工厂和抽象工厂。比如,家具工厂需要再生产“门”这个家具,那么不管是已有的中国工厂还是美国工厂,都需要新增生产“门”的家具,势必会引入风险。其实,这本质上就是继承带来的问题。
文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发。