设计模式 | 工厂方法 (Factory Method)
定义
工厂方法是一种创建式设计模式,它提供了用于在超类中创建对象的接口,但允许子类改变将要创建的对象的类型。
问题
想象一下,你正在创建一个物流管理应用程序。 您的应用程序的第一个版本只能处理卡车运输,因此大部分代码都在 Truck 类中。
过了一段时间,您的应用程序变得非常流行,以至于您会收到大量包含海运的请求。
好消息,对吧?! 但是代码如何? 它看起来大部分代码都与 Truck 类相关联。 添加船只需要更改整个代码库。 此外,如果您决定为应用程序添加其他类型的交通工具,则可能需要再次进行所有这些更改。
你最终会遇到一些令人讨厌的代码,它们会根据传输对象的类别选择行为。
方案
工厂方法设计模式建议使用一个叫” 工厂” 的方法来替代直接通过 new 来创建对象。构造函数调用应该在该方法内移动。 工厂方法返回的对象通常被称为 “产品”。
咋一看上去,这一举动貌似毫无意义。但是你可以在子类中重写工厂方法,并且改变将要创建的类对象。让我们看看它是如何工作的:
当然,有一些小小的限制:所有的产品都必须有一个通用的接口(比如 Transport)。 基类中的 Factory 方法应该返回这个通用接口。
只要这些产品具有共同的基类或接口(例如卡车和船舶实现传输接口),子类就可以返回不同的具体产品。
工厂方法的客户端并不关心它们接收到的具体产品类型,它们通过一个共同的产品接口来与所有的具体产品协同工作。
构造
伪代码
创建产品接口及其实现类
运输接口:
1 | package one.wangwei.designpatterns.factorymethod; |
卡车运输子类:
1 | package one.wangwei.designpatterns.factorymethod; |
水路运输子类:
1 | package one.wangwei.designpatterns.factorymethod; |
创建工厂及其子类
物流抽象类:
1 | package one.wangwei.designpatterns.factorymethod; |
道路运输,采用卡车
1 | package one.wangwei.designpatterns.factorymethod; |
海运,采用船
1 | package one.wangwei.designpatterns.factorymethod; |
调用
1 | package one.wangwei.designpatterns.factorymethod; |
使用场景
问题:当你不知道对象的确切类型和依赖关系时,你的代码需要保证可以正常工作。
例如,从多个数据源去读和写入数据 —— 文件系统、数据库和网络。这些资源都有不同的类型,依赖关系和初始化代码。
方案
工厂方法可以一个产品的具体实现细节。 为了支持新的产品类型,您只需要创建一个新的子类并覆盖其中的工厂方法即可。
问题:当你想要为你的类库或框架的用户提供一种方式,好让他们能够扩展这些库或框架的内部组件。
方案
用户可以轻松地对某些特定组件进行子类化。 但是,框架将如何识别该子类并使用它而不是标准组件呢? 用户必须重写每个创建标准组件实例的方法,并将其更改为创建自定义子类的对象。 这很尴尬,不是吗?
更好的解决方案不仅是给用户提供扩展特定类的手段,而且还将生产组件的代码减少到单个创建方法中。 换句话说,提供工厂方法。
让我们看一下它是如何工作的。想象一下,你正在使用一个开源的 UI 框架开发 APP,你的 APP 需要一个圆型按钮,但是这个框架只提供的方形按钮。
你要做的第一件事情就是实现
RoundButton
类。但是现在你需要告诉UIFramework
类去使用新创建的对象,而不是默认对象。为了达到这个目的,你需要创建一个子类
UIWithRoundButtons
并且重写createButton
方法。这个方法仍然返回Button
类,但是你的新子类将会提供RoundButton
对象。现在,在你的 App 中,你需要使用UIWithRoundButtons
而不是UIFramework
来初始化这个框架。问题:当你想要保存系统资源并且重用已经存在的对象,而不是重新创建一个新的对象。
例如,当处理大的或者资源密集性的对象,例如数据库连接,等等。
方案:
想象一下,重复使用现有对象需要做多少工作:
- 首先,您需要创建一个池来保留现有的对象。
- 当有人请求一个对象时,你会在该池内寻找一个空闲对象。
- … 并将其返回给客户端代码。
- 只有在没有空闲对象时,才会创建一个新对象(并将其添加到池中)。
此代码必须放置在某处。 最方便的地方是一个构造函数。 这样,只要有人试图创建一个对象,所有这些检查都会被执行。 但是,唉,构造函数必须按照定义返回新对象,所以它们不能返回现有的实例。
如何做
提取所有产品的通用接口。 这个接口应该声明对每个产品都有意义的方法。
在创建者类中添加一个空的工厂方法。 其方法签名应返回产品接口类型。
查看创建者的代码并查找对产品构造函数的所有引用。 一个接一个地将它们替换为对工厂方法的调用,但将产品创建代码提取到工厂方法。
您可能需要向工厂方法添加一个临时参数,该参数将用于控制将创建哪个产品。
在这一点上,工厂方法的代码可能看起来很丑陋。 它可能有一个大的开关操作员,可以选择要实例化的产品类别。 但别担心,我们会马上修复它。
现在,在子类中覆盖工厂方法,并在 base 方法中从 switch 操作符中移出相应的条件。
在基础创建者类中用到的控制参数也能够用到子类当中
例如,您可能拥有一个创建者的层次结构,其中包含基类
Mail
以及Air
和Ground
类以及产品类:Plane
,Truck
和Train
。Air
与Plane
相匹配,同时Ground
与Truck
和Train
同时匹配。 您可以创建一个新的子类来处理这两种情况,但还有另一种选择。 客户端代码可以将参数传递给 Ground 类的工厂方法,以控制它接收的产品。如果基础工厂方法在完成代码的迁移后变成了空壳,则可以将其抽象化。
优缺点
Pros
- 遵循了开闭原则。
- 避免了具体产品和使用它们的代码之间的紧密耦合。
- 由于将所有创建代码移动到一个地方,因此简化了代码。
- 向程序添加一个新的产品变得非常方便。
Cons
- 需要额外的子类。
JDK 中的运用
java.util.Calendar#getInstance()
java.util.ResourceBundle#getBundle()
java.text.NumberFormat#getInstance()
java.nio.charset.Charset#forName()
java.net.URLStreamHandlerFactory#createURLStreamHandler(String)
(Returns different singleton objects, depending on a protocol)java.util.EnumSet#of()
javax.xml.bind.JAXBContext#createMarshaller()
and other similar methods.