设计模式之工厂方法(Factory Method)
又称: 虚拟构造器
意图
工厂方法是一种创建型设计模式,它为在超类中创建对象提供了一个接口,但允许子类更改将要创建的对象的类型。
问题描述
想象一下,你正在创建一个物流管理应用。你的应用程序的第一个版本只能处理卡车运输,所以你的大部分代码都在卡车类(Truck
)中。
过了一段时间,你的应用变得非常受欢迎。每天,你都会收到数十个来自海运公司的请求,要求将海运物流纳入应用。
好消息,对吧?但是代码呢?目前,你的大部分代码都耦合到 Truck
类。将 Ship
添加到应用中需要对整个代码库进行更改。此外,如果以后你决定在应用程序中添加另一种类型的交通工具,你可能需要在进行所有这些更改。
因此,你最终会得到非常糟糕的代码,其中充满了根据运输对象类别切换应用程序行为的条件。
方案
工厂方法模式建议将直接对象构造调用(使用 new
运算符)替换为对特殊工厂方法的调用。别担心:对象仍然是通过 new
操作符创建的,但它是从工厂方法中调用的。工厂方法返回的对象通常被称为产品(products)。
乍一看,这种更改可能看起来毫无意义:我们只是将构造函数调用从程序的一部分移到了另一部分。然而,考虑一下:现在你可以覆盖子类中的工厂方法,并更改该方法创建的产品类。
不过有一个小小的限制:只有当这些产品有一个公共基类或接口时,子类才能返回不同类型的产品。此外,基类中的工厂方法应该将其返回类型声明为此接口。
例如,Truck
和 Ship
类都应该实现 Transport
接口,该接口声明了一个名为 delivery
的方法。每个类别实现这种方法的方式不同:卡车通过陆路运送货物,船只通过海路运送货物。RoadLogistics
类中的工厂方法返回卡车对象,而 SeaLogistics
类的工厂方法则返回船只。
使用工厂方法的代码(通常称为客户端代码)在不同子类返回的实际产品之间没有区别。客户端将所有产品视为抽象的 Transport
。客户端知道所有传输对象都应该具有 deliver
方法,但它的确切工作方式对客户端来说并不重要。
结构
Product 声明了接口,该接口对于创建者及其子类可以生成的所有对象都是通用的。
Concrete Products 是产品接口的不同实现。
Creator 类声明了返回新产品对象的工厂方法。该方法的返回类型与产品接口相匹配很重要。
你可以将工厂方法声明为抽象方法 abstract
,以强制其所有子类实现自己版本的该方法。作为替代方案,基本工厂方法可以返回一些默认的产品类型。
请注意,尽管名称如此,但产品创建并不是 Creator 的主要责任。通常,Creator 类已经有了一些与产品相关的核心业务逻辑。工厂方法有助于将此逻辑与具体的产品类解耦。这里有一个类比:一家大型软件开发公司可以有一个程序员培训部门。然而,整个公司的主要职能仍然是编写代码,而不是培养程序员。
Concrete Creators 重写了基础工厂方法使之返回不同类型的产品。
请注意该工厂方法并非一定要一直创建新实例。它也可以从缓存、对象池或其他来源返回已有的对象。
伪代码
此示例说明了如何使用工厂方法创建跨平台 UI 元素,而无需将客户端代码耦合到具体的 UI 类。
基础的 Dialog
类使用不同的 UI 元素来渲染其窗口。在不同操作系统下,这些元素可能看起来有些许不同,不过它们的行为应该仍然一致。Windows 中的按钮仍然是 Linux 中的按钮。
当工厂方法发挥作用时,不需要为每个操作系统重写 Dialog
类的逻辑。如果我们声明一个在 Dialog
基类中生成按钮的工厂方法,我们稍后可以创建一个从工厂方法返回 Windows 样式按钮的子类。子类然后继承基类的大部分代码,但由于工厂方法,可以在屏幕上渲染看起来像 Windows 的按钮。
为使该模式生效,基础的 Dialog
必须与抽象按钮协作:一个所有实体按钮必须遵循的基类或接口。这样,无论使用哪种类型的按钮,Dialog
内的代码都可以保持功能。
当然,您也可以将这种方法应用于其他UI元素。但是,对于添加到 Dialog
中的每一个新工厂方法,都会更接近抽象工厂(Abstract Factory)模式。不用担心,我们稍后会讨论这种模式。
// Creator 类声明工厂方法必须返回一个产品类对象
// Creator 的子类通常提供该方法的实现
class Dialog is
// Creator 也可以提供工厂方法的一些默认实现
abstract method createButton():Button
// 请注意,尽管名称如此,但产品创建并不是 Creator 的主要责任。
// 它通常包含一些核心业务逻辑。
// 这些逻辑依赖于工厂方法返回的产品对象。
// 子类可以通过重写工厂方法并从中返回不同类型的产品来间接更改业务逻辑。
method render() is
// 调用工厂方法创建产品对象
Button okButton = createButton()
// 使用产品
okButton.onClick(closeDialog)
okButton.render()
// 具体创建者重写工厂方法,
// 以修改返回的产品类型
class WindowsDialog extends Dialog is
method createButton():Button is
return new WindowsButton()
class WebDialog extends Dialog is
method createButton():Button is
return new HTMLButton()
// 产品接口声明所有具体产品必须实现的操作
interface Button is
method render()
method onClick(f)
// 具体产品提供各种产品接口的实现
class WindowsButton implements Button is
method render(a, b) is
// 以 Windows 样式渲染按钮。
method onClick(f) is
// 绑定原生系统点击事件。
class HTMLButton implements Button is
method render(a, b) is
// 返回按钮的 HTML 表示方式
method onClick(f) is
// 绑定网页浏览器点击事件
class Application is
field dialog: Dialog
// 应用程序根据当前配置或环境设置选择创建者的类型。
method initialize() is
config = readApplicationConfigFile()
if (config.OS == "Windows") then
dialog = new WindowsDialog()
else if (config.OS == "Web") then
dialog = new WebDialog()
else
throw new Exception("Error! Unknown operating system.")
// 客户端代码与具体创建者的实例一起工作,
// 尽管是通过其基本接口。
// 只要客户端通过基本接口继续与创建者合作,
// 你就可以将任何创建者的子类传递给它。
method main() is
this.initialize()
dialog.render()
适用场景
当事先不知道代码应该使用的对象的确切类型和依赖关系时,请使用工厂方法。
工厂方法将产品构造代码与实际使用产品的代码分离。因此,更容易独立于代码的其余部分扩展产品构造代码。
例如,要向应用程序添加新的产品类型,只需要创建一个新的 Creator 子类并覆盖其中的工厂方法。
当你希望为库或框架的用户提供扩展其内部组件的方法时,请使用工厂方法。
继承可能是扩展库或框架默认行为的最简单方法。但是框架如何认识到应该使用你的子类而不是标准组件呢?
解决方案是将跨框架构建组件的代码减少到一个单一的工厂方法中,并允许任何人在扩展组件本身之外,重写该方法。
让我们看看这会如何运作。想象一下,你使用开源 UI 框架编写了一个应用程序。你的应用程序应该有圆形按钮,但框架只提供方形按钮。你编写了一个 RoundButton
子类扩展了标准的 Button
类。但现在您需要告诉主 UIFramework
类使用新按钮子类,而不是默认的按钮子类。
为了实现这一点,你可以从基本框架类创建一个子类 UIWithRoundButtons
,并重写其 createButton
方法。当此方法在基类中返回 Button
对象时,你可以使子类返回 RoundButton
对象。现在使用 UIWithRoundButtons
类而不是 UIFramework
。就这样!
当你希望通过重用现有对象保存系统资源而非每次重新编译时,请使用工厂方法。
在处理大型资源密集型对象(如数据库连接、文件系统和网络资源)时,您经常会遇到这种需求。
让我们思考一下,为了重用现有对象,必须做些什么:
- 首先,需要创建一些内存来跟踪所有创建的对象。
- 当有人请求对象时,程序在对象池中查找空闲对象。
- … 然后将其返回给客户端代码。
- 如果没有空闲对象,程序会创建一个新的(并将其添加到对象池中)。
这有很多代码!而且它必须全部放在一个地方,这样你就不会用重复的代码污染程序。
放置这些代码最明显、最方便的地方可能是我们试图重用其对象的类的构造函数。但是,构造函数必须始终根据定义返回新对象。它无法返回现有实例。
因此,需要有一个能够创建新对象以及重用现有对象的常规方法。这听起来很像工厂的方法。
如何实现
1. 让所有产品遵循同样的接口。这个接口应该声明在每个产品中都有意义的方法。
2. 在 Creator 类中添加一个空的工厂方法。方法的返回类型应与通用产品接口匹配。
3. 在 Creator 的代码中查找对产品构造函数的所有引用。逐个地,将它们替换为对工厂方法的调用,同时将产品创建代码提取到工厂方法中。
你可能需要添加一个临时参数到工厂方法中,以控制返回的产品类型。
此刻,工厂方法的代码可能看起来相当难看。它可能有一个很大的 switch
语句,用于选择要实例化的产品类。但别担心,我们很快就会解决的。
4. 现在,为工厂方法中列出的每个产品类型创建一组 Creator 的子类。在子类中重写工厂方法,并且从基础方法中提取除合适的代码片段。
5. 如果产品类型太多并且为所有产品类型创建子类是没有意义的,那么可以在子类中重用基类中的控制参数。
比如,假设你有以下的类层次结构:基础的 Mail
类有一些子类:AirMail
和 GroudMail
;Transport
类有 Plane
、Truck
和 Train
。而 AirMail
只使用 Plane
对象,GroudMail
只与 Truck
和 Train
对象协作。你可以创建一个新的子类(比如 TrainMail
)来处理这两种情况,不过也有另一种选项。客户端代码将参数传送给 GroundMail
类的工厂方法,以控制它需要的是哪个产品。
6. 如果,在提取之后,基础工厂方法为空了,你可以将其改为抽象方法。如果还有内容,你可以将其设为该方法的默认行为。
优缺点
- ✔️你避免了 Creator 和产品实体之间的紧密耦合。
- ✔️单一职责原则。你可以将产品创建代码移动到程序中的一个位置,使代码更易于支持。
- ✔️开闭原则。你可以在不破坏现有客户端代码的情况下引入新类型的产品到程序中。
- ❌由于需要引入了许多子类以实现该模式,代码可能变得更加复杂。最好的情况是将模式引入 Creator 类的现有层次结构中。
与其他模式的关系
- 许多设计始于工厂方法(不那么复杂,通过子类更可定制)开始,并向抽象工厂、原型或建造者Builder(更灵活,但更复杂)发展。
- 抽象工厂(Abstract Factory)类通常基于一组工厂方法(Factory Method),不过你也可以使用原型(Prototype)在这些类中组合方法。
- 你可以将工厂方法(Factory Method)和迭代器(Iterator)一起使用,让集合子类返回兼容该集合的不同类型迭代器。
- 原型(Prototype)不是基于继承,所以它没有继承的缺点。另一方面,原型(Prototype)需要对克隆的对象进行复杂的初始化。工厂方法(Factory Method)基于继承,但不需要初始化步骤。
- 工厂方法(Factory Method)是模板方法(Template Method)专业化。同时,工厂方法(Factory Method)可以作为大型模板方法(Template Method)中的一个步骤。
代码示例
index.php: 概念示例
<?php
namespace RefactoringGuru\FactoryMethod\Conceptual;
/**
* The Creator class declares the factory method that is supposed to return an
* object of a Product class. The Creator's subclasses usually provide the
* implementation of this method.
*/
abstract class Creator
{
/**
* Note that the Creator may also provide some default implementation of the
* factory method.
*/
abstract public function factoryMethod(): Product;
/**
* Also note that, despite its name, the Creator's primary responsibility is
* not creating products. Usually, it contains some core business logic that
* relies on Product objects, returned by the factory method. Subclasses can
* indirectly change that business logic by overriding the factory method
* and returning a different type of product from it.
*/
public function someOperation(): string
{
// Call the factory method to create a Product object.
$product = $this->factoryMethod();
// Now, use the product.
$result = "Creator: The same creator's code has just worked with " .
$product->operation();
return $result;
}
}
/**
* Concrete Creators override the factory method in order to change the
* resulting product's type.
*/
class ConcreteCreator1 extends Creator
{
/**
* Note that the signature of the method still uses the abstract product
* type, even though the concrete product is actually returned from the
* method. This way the Creator can stay independent of concrete product
* classes.
*/
public function factoryMethod(): Product
{
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator
{
public function factoryMethod(): Product
{
return new ConcreteProduct2();
}
}
/**
* The Product interface declares the operations that all concrete products must
* implement.
*/
interface Product
{
public function operation(): string;
}
/**
* Concrete Products provide various implementations of the Product interface.
*/
class ConcreteProduct1 implements Product
{
public function operation(): string
{
return "{Result of the ConcreteProduct1}";
}
}
class ConcreteProduct2 implements Product
{
public function operation(): string
{
return "{Result of the ConcreteProduct2}";
}
}
/**
* The client code works with an instance of a concrete creator, albeit through
* its base interface. As long as the client keeps working with the creator via
* the base interface, you can pass it any creator's subclass.
*/
function clientCode(Creator $creator)
{
// ...
echo "Client: I'm not aware of the creator's class, but it still works.\n"
. $creator->someOperation();
// ...
}
/**
* The Application picks a creator's type depending on the configuration or
* environment.
*/
echo "App: Launched with the ConcreteCreator1.\n";
clientCode(new ConcreteCreator1());
echo "\n\n";
echo "App: Launched with the ConcreteCreator2.\n";
clientCode(new ConcreteCreator2());
Output.txt: 执行结果
App: Launched with the ConcreteCreator1.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of the ConcreteProduct1}
App: Launched with the ConcreteCreator2.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of the ConcreteProduct2}