类的工厂
工厂模式提供了一种创建对象的方式,而无需指定要创建的具体类。
通过使用工厂模式,可以将对象的创建逻辑封装在一个工厂类中,而不是在客户端代码中直接实例化对象,这样可以提高代码的可维护性和可扩展性。
工厂模式的类型与简介:
- 简单工厂模式(Simple Factory):它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象。
- 它拥有一个包含大量条件语句的构建方法,可根据方法的参数来选择对何种产品进行初始化并将其返回。
- 简单工厂模式不是一个正式的设计模式,但它是工厂方法模式、抽象工厂的中间步骤。
- 它属于创建型模式,但不属于 GoF 23 设计模式。
- 不符合开闭原则,简单工厂中每一次扩展都需要对工厂类进行修改
- 静态工厂模式:将工厂类中的创建对象的功能定义为静态的,就是静态工厂模式。
- 它同样不是一种设计模式。
- 有时候,此模式归类为简单工厂模式的另一种实现方式。
- 工厂方法模式(Factory Method Pattern),又称多态性工厂模式:工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪个类。工厂方法将对象的创建延迟到子类。
- 抽象工厂模式(Abstract Factory Pattern):抽象工厂模式提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。
关于以上分类方法的一些提醒
- 在 GoF 的《设计模式》一书中,作者将简单工厂模式看作是工厂方法模式的一种特例。
- 《图解设计模式》一书中沿用 GoF《设计模式》中的分类方法。书中关于工厂模式介绍的是 Factory Method 模式和 Abstract Factory 模式。
- 有些观点认为上面的简单工厂模式(Simple Factory)和静态工厂模式是「简单工厂模式」的两种实现。
更多相似术语的比较,可见后文的详细章节。
为了内容编排清晰,本文已将案例和理论分节处理。
一个简易计算器的案例
我们先从案例讲起。
通过代码实现一个简单计算器,具有加减乘除的运算功能:
1 | public class OperationFactory { |
如果我们不使用简单工厂模式,我们的代码会是这样的:
1 | public static void main(String[] args) { |
也就是说,简单工厂模式将一个复杂的类的创建过程,抽象为一个工厂类去做这个事情。
大部分工厂类都是以「Factory」这个单词结尾的,但也不是必须的,比如 Java 中的 DateFormat
、Calender
。除此之外,工厂类中创建对象的方法一般都是 create
开头,比如代码中的 createOperation()
,但有的也命名为 getInstance()
、createInstance()
、newInstance()
,有的甚至命名为 valueOf()
(比如 Java String
类的 valueOf()
函数)等等。
在上面的代码中,我们可以发现每次调用 createOperation()
函数都会 new
一个新的实例。如果这个实例可以复用,我们可以将它缓存起来,这就是后面使用到的静态工厂模式。
针对上一小节简单工厂模式的 OperationFactory
,我们可以将其改造为静态工厂模式:
1 | public class OperationFactory { |
在上面的代码中如果我们需要新增操作运算,就必须改动 OperationFactory
的代码,似乎有点违反开闭原则。如果「新增操作运算」这件事情很少发生,上面的代码也是可以接受的。
在这个模式中,我们把 OperationAdd
、OperationSub
这些看成是工厂方法模式中的「具体产品」角色;其抽象类 Operation
即为「产品」角色。每个具体的产品,都有各自对应的具体工厂而创建;各自的具体工厂,都继承着工厂接口:
1 | // 相较于简单工厂模式 OperationFactory 并不直接创建具体产品 |
当需要生成的产品不多且不会增加,一个具体工厂类就可以完成任务时,可删除抽象工厂类。这时工厂方法模式将退化到简单工厂模式。
各种「工厂模式」
简单工厂模式
简单工厂「模式」是对于工厂最基础的应用,但它其实不能算作「工厂模式」,它不是一个设计模式,像是一种编程习惯。
应用层传参数给工厂就行,不需要关心如何创建。但是工厂类的职责相对过重,增加新的产品,需要修改工厂类的判断逻辑,违背了开闭原则。
静态工厂模式
创建类的实例的最常见的方式是用 new
语句调用类的构造方法。在这种情况下,程序可以创建类的任意多个实例,每执行一条 new
语句,都会导致 Java 虚拟机的堆区中产生一个新的对象。假如类需要进一步封装创建自身实例的细节,并且控制自身实例的数目,那么可以提供静态工厂方法。
静态工厂方法的特点:
- 不需要使用创建对象的方法实例化对象
- 不能通过继承来改变创建方法的行为
代码上的特点就是类的构造函数使用 private
修饰。
在有些情况下,一个类可以同时提供
public
的构造方法和静态工厂方法。用户可按需使用获取类的方式。
例子:
Class.forName
Integer.valueOf
Foo.getInstance
静态工厂方法与用 new
语句调用构造方法的区别:
静态工厂模式 | new 语句 | |
---|---|---|
构造方法名字 | 方法名任意,提高代码可读性。但和其他的静态方法没有明显区别,我们可以通过约定命名的方式使用户区分一些静态方法专门负责返回类的示例,比如 valueOf 、getInstance 。 | 必须与类名相同。但不能从名字上区分每个被重载的构造方法。 |
调用时生成的对象数 | 每次调用创建对象数目取决于方法的具体实现。 | 每次调用都会生成一个新的对象。 |
能否返回子类的实例 | 可以。这一特性可以实现松耦合的系统接口。具体详看:工厂(方法)模式 Factory 章节 | 不能,只能返回当前类的实例。 |
静态工厂方法最主要的特点是:每次被调用的时候,不一定要创建一个新的对象。利用这一特点,静态工厂方法可用来创建以下类的实例:
- 站内文章单例(Singleton)类:只有唯一的实例的类。详看:饿汉模式。
- 枚举类:实例的数量有限的类。自定义枚举类而不是简单的定义 0,1 之类的数字在提高代码可读性的同时,还可以引入 Java 编译器的检查。JDK5 开始,有专门的
java.lang.Enum
枚举类型的语法。 - 具有实例缓存的类:能把已经创建的实例暂且存放在缓存中的类。
- 具有实例缓存的不可变类:不可变类的实例一旦创建,其属性值就不会被改变。
静态工厂方法的优缺点
Consider static factory methods instead of constructors.
创建对象时尽量考虑静态工厂方法,从而你可能见到以下场景:
1 | List<String> list = Lists.newArrayList(); // 明明可以写成list = new ArrayList() |
优点:
- 静态工厂方法可以通过方法名字来表示创建了什么对象
- 创建对象时不需要每次都创建一个新的。这也叫做实例控制(instance-controlled),比如
Boolean
的valueOf
方法。 - 静态工厂方法可以返回该类的子类。如果
new
的话就只能返回某个类,但是静态工厂方法可以返回其子类。比如有个静态对象的返回值是Set
,那么方法返回HashSet
。 - 可以返回
private class
的对象。 - 静态工厂方法可以根据参数来调整返回的子类
- 静态工厂方法创建的对象可以暂时不存在。典型的例子就是通过 站内文章反射 来创建对象,给一个对象的全路径为参数(比如
java.uril.ArrayList
)然后通过反射的方式创建之后返回。那么在这种情况下new
肯定是不行了(类都没有根本没办法new
)。
缺点:由于没有 public
的构造函数,那么这个类就没办法被继承,也就没法重写其方法。缺点中的优点就是鼓励程序员用组合而不是继承。
工厂方法模式 Factory Method
将实例的生成交给子类。
如果我们将 站内文章模板方法模式 用于生成实例,他就演变为工厂方法模式。
在模板方法模式中,父类规定处理的流程,子类实现具体的处理;在工厂方法模式中,父类(工厂)决定实例的生成方式,但并不决定所要生成的具体的类,具体处理全部交给子类(工厂)负责。这样可以将生成实例的框架和实际负责生成实例的类解耦。
在工厂模式中,核心的工厂类不再负责所有的产品的创建,而是将具体的工作交给子类去做。该核心类成为一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品应当被实例化这种细节。
抽象类 Creator
中的 create
方法使用 final
关键字修饰,表明不应该重写模板方法,这是模板方法模式的一个特点。
例子:
1 | package framework; |
登场角色:
Product
(产品):框架,抽象类,定义该模式下生成实例所持有的接口。具体的处理由ConcreteProduct
角色决定。Creator
(创建者):框架,负责生成Product
角色的抽象类。不用new
关键字来生成实例,而是调用生成实例的专用方法来生成实例,这样就可以防止父类与其他具体类耦合。ConcreteProduct
(具体的产品):加工方,决定具体的产品。ConcreteCreator
(具体的创建者):加工方,负责生成具体的产品。
拓展思路:
- 如果要创建其他产品,直接引入
framework
(框架)包。 - 生成实例的方法,
Creator
里的create
方法,可以有三种实现方式:
1 | // 方式1:在Creator里指定create为抽象方法 |
优点 | 缺点 |
---|---|
解耦。创建者和具体产品之间的解耦。 | 应用该模式时需要引入许多新的子类。最好的情况是将该模式引入创建者类的现有层次结构中。 |
单一职责原则。产品创建的代码放在程序的单一位置。 | 每新增一个新产品时就需要增加两个类(具体的产品类和与之对应的具体工厂)。 |
开闭原则。不更改原有代码,直接引入新的产品类型。 |
与其他设计模式的关系
工厂方法模式和简单工厂模式的区别:
- 简单工厂把有关实例化的全部事情,都在工厂类中处理(没有抽象工厂类),然而工厂方法模式创建了一个框架,让工厂子类决定要如何实现。
- 克服了简单工厂违背“开放 - 封闭原则”的缺点,又保持了封装对象创建过程的优点,对简单工厂的进一步抽象和推广
工厂方法模式和抽象工厂模式:
- 抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。
相关的设计模式:
- 站内文章单例模式:在多数情况下我们都可以将单例模式用于扮演
Creator
角色(或是ConcreteCreator
角色)的类。这是因为在程序中没有必要存在多个Creator
角色(或是 ConcreteCreator 角色)的实例。 - 站内文章组合模式:有时可以将组合模式用于
Product
角色(或是ConcreteProduct
角色)。 - 站内文章模板方法模式:工厂方法模式是模板方法的典型应用,是模板方法的特殊形式。在示例程序中,
create
方法就是模板方法。同时,工厂方法可以作为一个大型模板方法中的一个步骤。 - 站内文章迭代器模式:有时,在迭代器模式模式中使用
iterator
方法生成Iterator
的实例时会使用工厂方法模式。这可以让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。 - 站内文章原型模式:原型并不基于继承, 因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法基于继承, 但是它不需要初始化步骤。
抽象工厂 Abstract Factory
将关联零件组装成产品。
抽象工厂模式(Abstract Factory Pattern)的定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。抽象工厂的工作是将「抽象零件」组装为「抽象产品」。
抽象:不考虑具体实现,而是仅关注接口(API)的状态。
依赖倒置原则:要依赖抽象,不要依赖具体的类。
登场角色:
AbstractProduct
(抽象产品):定义AbstractFactory
角色所生成的抽象零件和产品接口(API)。AbstractFactory
(抽象工厂):定义用于生成抽象产品的接口(API)。ConcreteProduct
(具体产品):负责实现AbstractProduct
角色的接口(API)。是抽象产品的多种不同类型实现。ConcreteFactory
(具体工厂):负责实现AbstractFactory
角色的接口。Client
(委托者):调用AbstractFactory
角色和AbstractProduct
角色的接口(API)来进行工作,对于具体的零件、产品和工厂一无所知。比如示例程序中的 Main,根据 Factory 接口构造一篇 HTML。
使用抽象工厂模式一般要满足以下条件:
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用。
优点 | 缺点 |
---|---|
确保同一工厂生成的产品相互匹配,将一个系列的产品族统一到一起创建。 | 采用该模式需要向应用中引入众多接口和类。 |
解耦。客户端代码和具体产品代码解耦。 | 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口。 |
单一职责原则。 | 增加了系统的抽象性和理解难度。 |
「开闭原则」倾斜性。增加一个新的产品族容易。 | 「开闭原则」倾斜性。增加一个新种类的产品困难。 |
抽象工厂模式的扩展有一定的「开闭原则」倾斜性:
- 当增加一个新的产品族时只需增加一个新的具体工厂,不需要修改原代码,满足开闭原则。
- 当产品族中需要增加一个新种类的产品时,则所有的工厂类都需要进行修改,不满足开闭原则。
与其他设计模式对比
抽象工厂模式和工厂方法模式的异同:
- 创建对象时:
- 工厂方法模式:继承。通过工厂子类创建对象,客户端只需要知道具体的抽象类型,由工厂子类决定具体类型,只负责将客户从具体类型中解耦。
- 抽象工厂模式:对象的组合。提供用来创建一个产品家族的抽象类,这个类型的子类定义产品被创建的方法,使用时通过实例化具体工厂类,并将工厂类传入针对抽象类型写的代码中,把客户从所使用的实际具体产品中解耦。
- 抽象工厂集合了一群相关的产品类,而工厂方法只需要创建一个产品。抽象工厂中的每一个方法创建一个具体类,实际是利用工厂方法实现。可以使用 站内文章原型模式 来生成这些类的方法。
另一方面,当系统中只存在一个等级结构的产品时,抽象工厂模式将退化到工厂方法模式。
相关设计模式:
- 工厂(方法)模式:有时抽象工厂模式中零件和产品的生成会使用到工厂方法模式。
- 站内文章组合模式:有时抽象工厂模式在制作产品时会使用组合模式。
- 站内文章单例模式:有时抽象工厂模式中的具体工厂会使用单例模式。
- 站内文章门面模式:当只需对客户端代码隐藏子系统创建对象的方式时,可以使用抽象工厂来代替外观模式。
- 站内文章桥接模式:如果由桥接定义的抽象只能与特定实现合作,这一模式搭配就非常有用。在这种情况下,抽象工厂可以对这些关系进行封装,并且对客户端代码隐藏其复杂性。
工厂模式比较与各式术语辨析
工厂模式 | 开闭原则 | GoF 23 设计模式 | 简记 |
---|---|---|---|
简单工厂模式 | ❌ | ❌ | 一个具体工厂 |
静态工厂模式 | ✅ | 带缓存的具体工厂 | |
工厂方法模式 | ✅创建型 | 抽象工厂生产抽象产品 | |
抽象工厂模式 | 🟡 | 超级工厂 |
工厂——一个模糊的概念
工厂是一个含义模糊的术语,表示可以创建一些东西的函数、 方法或类。最常见的情况下,工厂创建的是对象。 但是它们也可以创建文件和数据库记录等其他东西。
- 创建程序 GUI 的函数或方法;
- 创建用户的类;
- 以特定方式调用类构造函数的静态方法。
- 一种创建型设计模式。
构建方法——把构造函数封装一下
构建方法在《重构与模式》中被定义为「创建对象的方法」。这意味着每个工厂方法模式的结果都是 「构建方法」,但反过来则并非如此。这也意味着你可以用「构建方法」来替代马丁·福勒在 重构 中使用的「工厂方法」和乔斯华·布洛克在《Effective Java》中使用的「静态工厂方法」。
在实际中,构建方法只是构造函数调用的封装器。它可能只是一个能更好地表达意图的名称。此外, 它可以让你的代码独立于构造函数的改动,甚至还可以包含一些特殊的逻辑,返回已有对象以而不是创建新对象。
许多人会仅仅因为这些方法创建了新对象而称之为「工厂方法」。 其中的逻辑很直接:所有的工厂都会创建对象,而该方法会创建对象,所以显然它是一个工厂方法。当遇到真正的工厂方法时,这自然会造成许多混淆。
下面的示例中 next
是一个构建方法:
1 | class Number { |
静态构建方法——构建方法声明 static
静态构建方法是被声明为 static
的构建方法。 换句话说, 你无需创建对象就能在某个类上调用该方法。当静态构建方法返回一个新对象时,它就成为了构造函数的替代品。
有些资料认为「静态工厂方法」的称呼是坏习惯
原因:
- 「工厂方法」是 GoF 23 设计模式之一,别来沾边!
- 「静态
static
」和「工厂方法」不能搭配,不要碰瓷!工厂方法是一种依赖于继承的设计模式。如果将它设置为static
,就不能在子类中对其进行扩展,这就破坏了该模式的目的。
但其实,不管是静态构建方法、静态工厂方法还是静态工厂模式指的东西都差不多,大家都会知道这是什么。
绝大多数编程语言的构造函数必须都返回一个新的类实例。静态构建方法是应对该限制的变通方法。 在静态方法内部,你的代码会决定是调用构造函数创建一个全新实例,还是返回一个在缓存中已有的对象。
1 | class User { |
简单工厂模式——有很多 if-else
的孤零零工厂
简单工厂模式描述了一个类, 它拥有一个包含大量条件语句的构建方法,可根据方法的参数来选择对何种产品进行初始化并将其返回。
在绝大多数情况下,简单工厂是引入工厂方法或抽象工厂模式时的一个中间步骤。
简单工厂通常没有子类。但当从一个简单工厂中抽取出子类后,它看上去就会更像经典的工厂方法模式了。
abstract
关键字后并不会变成抽象工厂模式!工厂方法模式——GoF 23 设计模式之一
工厂方法是一种创建型设计模式,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。
如果在基类及其扩展的子类中都有一个构建方法的话,那它可能就是工厂方法。
抽象工厂模式——GoF 23 设计模式之一
抽象工厂是一种创建型设计模式,它能创建一系列相关或相互依赖的对象, 而无需指定其具体类。
有这样一组的对象:运输工具 + 引擎 + 控制器。它可能会有几个变体:
- 汽车 + 内燃机 + 方向盘
- 飞机 + 喷气式发动机 + 操纵杆
如果你的程序中并不涉及产品系列的话,那就不需要抽象工厂。
Java 生成实例的方式
new
方法
通过 new
的方法方式可以生成实例,这种方法类名会出现在代码中,形成强耦合关系。
1 | // new关键字 |
在开发过程中,我们可能有「在不指定类名的前提下生成实例」的需求,这时候可以尝试使用 站内文章原型模式。原型模式包含三种实例创建方法:clone
方法、反序列化方法以及反射方法。
clone
方法
我们可以通过 clone
方法,根据现有实例复制出一个新实例。这个过程中不会调用构造函数。
1 | // clone方法 |
使用反射
我们还可以使用 站内文章反射,通过 newInstance
的方法生成实例:
1 | // java.lang.Class类的newInstance方法。通过Class类的实例生成出Class类所表示的类(即Something类)的实例(会调用无参构造函数)。 |
class.loadClass
方法:
1 | ClassLoader cl; |
反序列化
反序列化方式。即通过 I/O 流(包括反序列化),如运用反序列化手段,调用 java.io.ObjectInputStream
对象的 readObject()
方法。
结尾
后续计划
- 补充「抽象工厂模式」的案例学习
- 语言风格需要统一、逻辑需要清晰
本文 PlantUML 归档
工厂方法模式:
1 | package frame{ |
抽象工厂模式:
1 | package factory{ |
本文参考
- 《图解设计模式》
- 本科生课程笔记《程序设计中级实践&设计模式》 - TJU 🍐⚱️
- 极客时间专栏 - 设计模式之美 - 王争
- 设计模式辨析——工厂篇(简单工厂、静态工厂、工厂方法、抽象工厂) - 知乎
- 还在new对象吗?试试静态工厂方法–Effective Java第二章 - 知乎
- 工厂模式 | 菜鸟教程 (runoob.com)
- 设计模式之工厂模式(3种)详解及代码示例 - kosamino - 博客园
- 实例化类的五种方法-CSDN博客
- 万字详解 GoF 23 种设计模式(多图、思维导图、模式对比),让你一文全面理解_gof23-CSDN博客
- 建造者设计模式(生成器模式)