01 意图
工厂方法是一种创建型设计模式,它提供了在超类中创建对象的接口,但允许子类更改将要创建的对象的类型。
02 问题
想象一下,您正在创建一个物流管理应用程序。您的应用程序的第一个版本只能处理卡车运输,因此您的大部分代码都存在于Truck
类中。
一段时间后,您的应用程序变得非常流行。每天,您都会收到来自海上运输公司的数十个请求,要求将海上物流整合到应用程序中。
好消息,对吧?但是代码呢?目前,您的大部分代码都与Truck
类耦合。添加Ships
到应用程序需要对整个代码库进行更改。此外,如果稍后您决定向应用程序添加另一种交通工具,您可能需要再次进行所有这些更改。
结果,您将得到非常讨厌的代码,其中充斥着根据运输对象的类别切换应用程序行为的条件。
03 解决方案
工厂方法模式建议您将直接对象构造调用(使用new
运算符)替换为对特殊工厂方法的调用。别担心:对象仍然是通过new
操作符创建的,但它是从工厂方法中调用的。工厂方法返回的对象通常称为产品。
乍一看,这种更改可能看起来毫无意义:我们只是将构造函数调用从程序的一部分移到了另一部分。但是,请考虑一下:现在您可以在子类中覆盖工厂方法并更改由该方法创建的产品类。
但是有一点限制:只有当这些产品具有共同的基类或接口时,子类才可能返回不同类型的产品。此外,基类中的工厂方法应将其返回类型声明为此接口。
例如,Truck和Ship类都应该实现Transport接口,该接口声明了一个名为deliver. 每个类都以不同的方式实现此方法:卡车通过陆路运送货物,轮船通过海路运送货物。类中的工厂方法RoadLogistics返回卡车对象,而SeaLogistics类中的工厂方法返回船舶。
使用工厂方法的代码(通常称为客户端代码)看不到各个子类返回的实际产品之间的差异。客户将所有产品视为抽象的Transport。客户端知道所有传输对象都应该具有该deliver方法,但它的确切工作方式对客户端并不重要。
04 结构实现
此示例说明如何使用工厂方法创建跨平台 UI 元素,而无需将客户端代码耦合到具体的 UI 类。
基础对话框类使用不同的 UI 元素来呈现其窗口。在各种操作系统下,这些元素可能看起来有些不同,但它们的行为应该仍然一致。Windows 中的按钮仍然是 Linux 中的按钮。
当工厂方法发挥作用时,您无需为每个操作系统重写对话框的逻辑。如果我们声明一个在对话框基类中生成按钮的工厂方法,我们稍后可以创建一个对话框子类,它从工厂方法返回 Windows 样式的按钮。然后子类从基类继承大部分对话框的代码,但是,由于工厂方法,可以在屏幕上呈现类似于 Windows 的按钮。
要使这种模式起作用,基本对话框类必须使用抽象按钮:所有具体按钮都遵循的基类或接口。这样,无论它使用哪种类型的按钮,对话框的代码都可以正常工作。
当然,您也可以将此方法应用于其他 UI 元素。但是,随着您添加到对话框中的每个新工厂方法,您会更接近抽象工厂模式。不要害怕,我们稍后会讨论这种模式。
// The creator class declares the factory method that must
// return an object of a product class. The creator's subclasses
// usually provide the implementation of this method.
class Dialog is
// The creator may also provide some default implementation
// of the factory method.
abstract method createButton():Button
// Note that, despite its name, the creator's primary
// responsibility isn't creating products. It usually
// 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.
method render() is
// Call the factory method to create a product object.
Button okButton = createButton()
// Now use the product.
okButton.onClick(closeDialog)
okButton.render()
// Concrete creators override the factory method to change the
// resulting product's type.
class WindowsDialog extends Dialog is
method createButton():Button is
return new WindowsButton()
class WebDialog extends Dialog is
method createButton():Button is
return new HTMLButton()
// The product interface declares the operations that all
// concrete products must implement.
interface Button is
method render()
method onClick(f)
// Concrete products provide various implementations of the
// product interface.
class WindowsButton implements Button is
method render(a, b) is
// Render a button in Windows style.
method onClick(f) is
// Bind a native OS click event.
class HTMLButton implements Button is
method render(a, b) is
// Return an HTML representation of a button.
method onClick(f) is
// Bind a web browser click event.
class Application is
field dialog: Dialog
// The application picks a creator's type depending on the
// current configuration or environment settings.
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.")
// 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.
method main() is
this.initialize()
dialog.render()
05 适用场景
-
当您事先不知道代码应该使用的对象的确切类型和依赖关系时,请使用工厂方法。
工厂方法将产品构造代码与实际使用产品的代码分开。因此,独立于代码的其余部分扩展产品构造代码会更容易。
例如,要向应用程序添加新的产品类型,您只需要创建一个新的创建者子类并覆盖其中的工厂方法。
-
当您想为您的库或框架的用户提供一种扩展其内部组件的方法时,请使用工厂方法。
继承可能是扩展库或框架的默认行为的最简单方法。但是框架如何识别应该使用您的子类而不是标准组件?
解决方案是将跨框架构建组件的代码减少到单个工厂方法中,并让任何人在扩展组件本身之外覆盖此方法。
让我们看看它是如何工作的。想象一下,您使用开源 UI 框架编写应用程序。你的应用应该有圆形按钮,但框架只提供方形按钮。Button
你用一个光荣的子类扩展了标准类RoundButton
。但是现在你需要告诉主UIFramework
类使用新的按钮子类而不是默认的。
要实现这一点,您需要UIWithRoundButtons
从基础框架类创建一个子类并覆盖其createButton
方法。虽然此方法返回Button
基类中的对象,但您让子类返回RoundButton
对象。现在使用UIWithRoundButtons
类而不是UIFramework
. 就是这样!
当您想通过重用现有对象而不是每次都重新构建它们来节省系统资源时,请使用工厂方法。
在处理大型资源密集型对象(例如数据库连接、文件系统和网络资源)时,您经常会遇到这种需求。
让我们考虑一下要重用现有对象需要做什么:
首先,您需要创建一些存储来跟踪所有创建的对象。
当有人请求一个对象时,程序应该在该池中寻找一个空闲对象。
…然后将其返回给客户端代码。
如果没有空闲对象,程序应该创建一个新对象(并将其添加到池中)。
那是很多代码!并且它必须全部放在一个地方,这样你就不会用重复的代码污染程序。
可以放置此代码的最明显和最方便的地方可能是我们试图重用其对象的类的构造函数。但是,构造函数必须始终按定义返回新对象。它无法返回现有实例。
因此,您需要一个能够创建新对象以及重用现有对象的常规方法。这听起来很像工厂方法。
06 如何实施
使所有产品遵循相同的界面。这个接口应该声明对每个产品都有意义的方法。
在创建者类中添加一个空的工厂方法。方法的返回类型应与常见的产品接口匹配。
-
在创建者的代码中找到对产品构造函数的所有引用。一一替换为对工厂方法的调用,同时将产品创建代码提取到工厂方法中。
您可能需要在工厂方法中添加一个临时参数来控制返回产品的类型。
此时,工厂方法的代码可能看起来很丑陋。它可能有一个大型
switch
运算符来选择要实例化的产品类。但别担心,我们会尽快修复它。 现在,为工厂方法中列出的每种产品类型创建一组创建者子类。覆盖子类中的工厂方法并从基方法中提取适当的构造代码位。
-
如果产品类型太多,为所有产品创建子类没有意义,您可以在子类中重用基类中的控制参数。
例如,假设您有以下类的层次结构:
Mail
带有几个子类的基类:AirMail
和GroundMail
;Transport
类Plane
是Truck
和Train
。_ 虽然AirMail
该类仅使用Plane
对象,GroundMail
但可以同时使用Truck
和Train
对象。您可以创建一个新的子类(比如TrainMail
)来处理这两种情况,但还有另一种选择。客户端代码可以将参数传递给类的工厂方法GroundMail
来控制它想要接收的产品。 如果在所有提取之后,基础工厂方法变为空,您可以将其抽象化。如果还有一些东西,您可以将其设置为该方法的默认行为。
07 优缺点