【设计模式】工厂模式
封面来源:工厂方法设计模式 (refactoringguru.cn),如有侵权,请联系删除。
Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)
知乎问答:工厂模式(factory Method)的本质是什么?为什么引入工厂模式?
0. 工厂模式简介
工厂模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
也就是说: 当创建对象的过程比较复杂、不想对使用者暴露创建逻辑时,就可以使用工厂模式。
工厂模式的定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
工厂模式有三种,分别是简单工厂模式(Simple Factory Pattern)、工厂方法模式(Factory Method Pattern)和抽象工程模式(Abstract Factory Pattern),因此本文将逐一介绍它们的定义、优缺点和使用方式。
1. 简单工厂模式
如果把被创建的对象称为“产品”,把创建产品的对象称为“工厂”。当需要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。
在简单工厂模式中创建实例的方法通常为静态(static)方法,因此简单工厂模式(Simple Factory Pattern)又叫作静态工厂方法模式(Static Factory Method Pattern)。
简单来说,简单工厂模式有一个具体的工厂类,可以生成多个不同的产品,属于创建型设计模式。 简单工厂模式不在 GoF 23 种设计模式之列。
简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
1.1 优点与缺点
优点
1、工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
2、客户端无需知道所创建具体产品的类名,只需知道参数即可。
3、也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。
缺点
1、简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则。
2、使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
3、系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
4、简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。
应用场景
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。比如:
1、日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
2、数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。就像 Hibernate 换数据库只需换方言和驱动就可以。
3、设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。
1.2 模式的结构
【简单工厂模式】的主要角色如下:
1、简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
2、抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
3、具体产品(ConcreteProduct):是简单工厂模式的创建目标。
【简单工厂模式】结构图:
1.3 代码实现
根据上面的结构图,我们可以很容易完成代码实现。
首先需要一个抽象产品(一个接口):
1 | public interface Product { |
让具体的产品实现抽象产品:
1 | public class ConcreteProduct1 implements Product { |
1 | public class ConcreteProduct2 implements Product { |
然后再编写一个简单工厂,根据不同的产品编号,生产出不同的产品:
1 | public final class SimpleFactory { |
最后可以编写一个测试类来测试一下,除了像下述代码一样使用断言 Assert
,还可以直接将消息打印在控制台上:
1 | public class SimpleFactoryTest { |
2. 工厂方法模式
前文说到的【简单工厂模式】违背了开闭原则,而【工厂方法模式】是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。
那什么是【工厂方法模式】呢?与【简单工厂模式】又有什么区别呢?
【工厂方法模式】会定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类当中。核心工厂类不再负责产品的创建,使得核心类成为一个抽象工厂角色,仅负责具体工厂子类必须实现的接口,这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。
简单来说就是在【简单工厂模式】上 对工厂进行了抽象,让具体的工厂必须实现抽象工厂类。这样的话,当我们添加新的产品时,不需要修改修改原工厂的代码,而是添加一个新的工厂去生成新的产品(当然,这样的话类的数量将明显增加)。
【工厂方法模式】又有那些优缺点呢?
2.1 优点与缺点
优点
1、用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
2、灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
3、典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。
缺点
1、类的个数容易过多,增加复杂度
2、增加了系统的抽象性和理解难度
3、抽象产品只能生产一种产品,此弊端可使用“抽象工厂模式”解决(先预告下,后文介绍)。
应用场景
1、客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
2、创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
3、客户不关心创建产品的细节,只关心产品的品牌
2.2 模式的结构
【工厂方法模式】的主要角色如下:
1、抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct()
来创建产品。
2、具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
3、抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
4、具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
【工厂方法模式】结构图:
2.3 代码实现
既然【工厂方法模式】相较于【简单工厂模式】只是对工厂进行了进一步抽象,那我们就在【简单工厂模式】的代码实现上进一步完善。
首先是引入抽象工厂(一个接口):
1 | public interface AbstractFactory { |
然后针对不同的产品使用不同的工厂进行实现,我们前面有两件产品,因此需要两个具体的工厂(具体的工厂实现抽象工厂):
1 | public class ConcreteFactory1 implements AbstractFactory { |
1 | public class ConcreteFactory2 implements AbstractFactory { |
还是跟前面一样,编写测试方法来测试一下(除了使用断言,也可以在控制台上打印):
1 |
|
3. 抽象工厂模式
从【工厂方法模式】来看,似乎已经很棒了,但我们仔细观察很容易发现其中的劣势:过于阔绰。
一个工厂只生产一件产品,一件产品就对应一个工厂,也就是一个类,如果产品的种类激增,那么也会导致类的数量激增。
为了解决这个问题,我们可以使用“抽象工厂模式”。
那什么是【抽象工厂模式】?
抽象工厂模式的定义:一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
简单来说就是套娃,就是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
3.1 产品等级与产品族
我们需要先引入两个概念:产品等级、产品族。
咱们看张图:
从这张图中可以看到:同一个牌子属于同一个产品族,同一个类型属于同一个产品等级。
那这两个概念有啥用呢?
【工厂方法模式】只考虑生产同等级的产品,而【抽象工厂模式】将考虑生产多等级的产品。
也就是说,【抽象工厂模式】是【工厂方法模式】的升级版。
但也并不是说直接上【抽象工厂模式】就完事了,使用【抽象工厂模式】一般需要满足以下条件: 系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
3.2 应用场景与拓展
应用场景
1、当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
2、系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
3、系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
拓展
【抽象工厂模式】的扩展有一定的“开闭原则”倾斜性:
1、当增加一个新的产品族时只需增加一个新的具体工厂,不需要修改原代码,满足开闭原则。
2、当产品族中需要增加一个新种类的产品时,则所有的工厂类都需要进行修改,不满足开闭原则。
另一方面,当系统中只存在一个等级结构的产品时,抽象工厂模式将退化到工厂方法模式。
3.3 优点与缺点
优点
【抽象工厂模式】除了有【工厂方法模式】的优点以外,它还具有以下优点:
1、可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
2、当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
3、抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。
缺点
【抽象工厂模式】也不是无懈可击的,它依然存在缺点:
1、当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
简单来说就是:产品族难扩展,产品等级易扩展。
3.4 模式的结构
【抽象工厂模式】的主要角色如下:
1、抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
2、具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
3、抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
4、具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
【抽象工厂模式】结构图:
3.5 代码实现
根据上述的结构图,很容易完成代码的实现。首先需要两个抽象产品(表示两种产品等级、两种类型的产品):
1 | public interface Product1 { |
1 | public interface Product2 { |
然后针对每种抽象产品又有不同的实现,比如 Product1
有两种实现:
1 | public class ConcreteProduct11 implements Product1 { |
1 | public class ConcreteProduct12 implements Product1 { |
Product2
也有两种实现:
1 | public class ConcreteProduct21 implements Product2 { |
1 | public class ConcreteProduct22 implements Product2{ |
然后我们需要一个抽象工厂,这个工厂可以生成不同产品等级的产品,也就是说可以生产处于同一个产品族(同一个牌子下的产品):
1 | public interface AbstractFactory { |
针对上述四件产品,需要两个工厂,每个工厂生产两种类型的产品,这两种产品属于同一个产品族,或者说属于同一个牌子:
1 | public class ConcreteFactory1 implements AbstractFactory { |
1 | public class ConcreteFactory2 implements AbstractFactory { |
最后还是老规矩,编写测试方法测试一下(除了使用断言,也可以在控制台上打印):
1 |
|
3.6 进一步优化
抽象工厂的实现可能会有很多,对于第一次接触系统的人,难以知晓究竟改用哪个实现类,此时可以将每个实现类与一个枚举项进行映射,用户在使用时只需要传入一个枚举项,就能得到一个工厂实例。
如果使用 JDK21 中“转正”的模式匹配,整个实现将更加完善且优雅:
1 | public class FactoryMaker { |
3.7 Factory Kit
在某些情况下可能会有这样的需求:
- 工厂也不知道究竟该创建出什么类型的对象
- 工厂实例不是全局的,而是谁用谁创建
这和抽象工厂很类似,因为也是定义创建工厂的方式;但它和抽象工厂又有点区别,抽象工厂能创建出的对象类型往往是固定,而现在是不固定的,只有在创建工厂实例时才知道这个工厂究竟能创建出什么对象。
首先定义一个工厂的构造器,它定义了构造出的工厂能够创建什么对象:
1 | public enum ProductType { |
使用构造出的工厂来创建对象时,同样是通过对象的类型进行匹配,此时需要一个 FactoryKit
,它定义了对象类型与枚举项的映射关系:
1 | public interface ProductFactoryKit { |
1 |
|
4. 为什么要引入工厂模式?
在最开始的时候就说了,在工厂模式中创建对象时,不会对客户端暴露创建逻辑。
那为什么要不暴露创建逻辑呢?
那肯定是创建过程比较复杂呗。
举几个“复杂的创建过程”的例子:
第一颗栗子
创建的对象可能是 pool (池)中的,并不是每次都需要去创建一个新的,但是 pool 的参数可以用其他的逻辑去控制,比如连接池、线程池。
第二颗栗子
代码的作者希望隐藏对象真实的类型,但使用构造方法就必须使用真实的类型,这时就可以利用工厂模式。比如提供一个抽象类:
1 | abstract class Foo { |
但真正的实现类是:
1 | public class FooImplV1 extends Foo { |
作者并不想让使用者知道 FooImplV1
的存在(万一作者哪天心情不好,改成了 FooImplV666
),只想让你知道 Foo
。那么他就可以提供这种方式让你使用:
1 | Foo foo = FooCreator.create(); |
第三颗栗子
对象的创建由很多个参数来决定。比如有一份数据被保存在文件中,要让这个文件的数据可以变成一个对象,那么可以这么做:
1 | Foo foo = FooCreator.fromFile("/path/to/the/data-file.ext"); |
当然实际场景可能复杂得多,有大量的配置参数。比如:
1 | Foo foo = FooCreator.fromFile("....", param1, param2, param3, ...); |
如果有必要,可以把这些 param 弄成一个 Config 对象。那这个 Config 对象也很复杂呢?可以再给 Config 弄个 Factory。那又如果这个 Factory 也很复杂呢?你可以弄个Factory的Factory。
嗯,尽情的套娃,只要你愿意。
第四颗栗子
在“第三颗栗子”中,我们看到对象的创建可能由多个参数决定。作者这时候发现使用者很喜欢创建某种对象,那么作者又提供了一个方法,让使用这可以直接使用它。比如:
1 | public static Foo getAFoo() { |
那么在使用时就可以:
1 | Foo foo = FooUtil.getAFoo(); |
线程池的创建 API 就和这很类似,比如:
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
第五颗栗子
创建的对象有复杂的依赖关系。比如 Foo 对象的创建依赖 A,A 又依赖 B,B 又依赖 C …
于是创建的过程是一组对象的创建和注入,而这手写太麻烦了,所以要维护好创建过程。
对的,Spring 的 IoC 就是这么干的。
第六颗栗子
我知道怎么创建一个对象,但不知道什么创建。这是就需要把“如何创建”的代码塞给“什么时候创建”的代码,后者会在适当的时机回调创建的函数。
在函数是一等公民的编程语言中,直接让函数作为方法的参数即可。而在 Java 中,就得搞个 Factory 的类再去传。Spring IoC 也利用了这个机制(FactoryBean
)。
备注:Java 8 中方法和 Lambda 作为一等公民。
第七颗栗子
避免在构造方法中抛出异常。在构造方法中最好不要抛出异常,因为这会加重上层代码编写者的负担,但业务要求必须抛一个异常怎么办?就像上面的 Foo
的创建需要从文件中读出数据。当文件不存在或者磁盘有问题读不出来时应当抛异常,这时可以用 FooCreator.fromFile
来搞定。
总结
当对象的创建过程比较复杂时,都需要写一个 createXXXX
的方法来帮我们实现对象的创建。再拓展一下范围,哪怕创建的不是对象,而是任何资源,也都得这么干。一句话:
不管用什么语言,创建什么资源。当为“创建”本身写代码的时候,就是在使用“工厂模式”了。