一文读懂工厂方法模式

01 意图

工厂方法是一种创建型设计模式,它提供了在超类中创建对象的接口,但允许子类更改将要创建的对象的类型。

image

02 问题

想象一下,您正在创建一个物流管理应用程序。您的应用程序的第一个版本只能处理卡车运输,因此您的大部分代码都存在于Truck类中。

一段时间后,您的应用程序变得非常流行。每天,您都会收到来自海上运输公司的数十个请求,要求将海上物流整合到应用程序中。

image

好消息,对吧?但是代码呢?目前,您的大部分代码都与Truck类耦合。添加Ships到应用程序需要对整个代码库进行更改。此外,如果稍后您决定向应用程序添加另一种交通工具,您可能需要再次进行所有这些更改。

结果,您将得到非常讨厌的代码,其中充斥着根据运输对象的类别切换应用程序行为的条件。

03 解决方案

工厂方法模式建议您将直接对象构造调用(使用new运算符)替换为对特殊工厂方法的调用。别担心:对象仍然是通过new操作符创建的,但它是从工厂方法中调用的。工厂方法返回的对象通常称为产品。

image

乍一看,这种更改可能看起来毫无意义:我们只是将构造函数调用从程序的一部分移到了另一部分。但是,请考虑一下:现在您可以在子类中覆盖工厂方法并更改由该方法创建的产品类。

但是有一点限制:只有当这些产品具有共同的基类或接口时,子类才可能返回不同类型的产品。此外,基类中的工厂方法应将其返回类型声明为此接口。

image

例如,Truck和Ship类都应该实现Transport接口,该接口声明了一个名为deliver. 每个类都以不同的方式实现此方法:卡车通过陆路运送货物,轮船通过海路运送货物。类中的工厂方法RoadLogistics返回卡车对象,而SeaLogistics类中的工厂方法返回船舶。

image

使用工厂方法的代码(通常称为客户端代码)看不到各个子类返回的实际产品之间的差异。客户将所有产品视为抽象的Transport。客户端知道所有传输对象都应该具有该deliver方法,但它的确切工作方式对客户端并不重要。

04 结构实现

image

此示例说明如何使用工厂方法创建跨平台 UI 元素,而无需将客户端代码耦合到具体的 UI 类。

image

基础对话框类使用不同的 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. 就是这样!

当您想通过重用现有对象而不是每次都重新构建它们来节省系统资源时,请使用工厂方法。

在处理大型资源密集型对象(例如数据库连接、文件系统和网络资源)时,您经常会遇到这种需求。

让我们考虑一下要重用现有对象需要做什么:

  1. 首先,您需要创建一些存储来跟踪所有创建的对象。

  2. 当有人请求一个对象时,程序应该在该池中寻找一个空闲对象。

  3. …然后将其返回给客户端代码。

  4. 如果没有空闲对象,程序应该创建一个新对象(并将其添加到池中)。

那是很多代码!并且它必须全部放在一个地方,这样你就不会用重复的代码污染程序。

可以放置此代码的最明显和最方便的地方可能是我们试图重用其对象的类的构造函数。但是,构造函数必须始终按定义返回新对象。它无法返回现有实例。

因此,您需要一个能够创建新对象以及重用现有对象的常规方法。这听起来很像工厂方法。

06 如何实施

  1. 使所有产品遵循相同的界面。这个接口应该声明对每个产品都有意义的方法。

  2. 在创建者类中添加一个空的工厂方法。方法的返回类型应与常见的产品接口匹配。

  3. 在创建者的代码中找到对产品构造函数的所有引用。一一替换为对工厂方法的调用,同时将产品创建代码提取到工厂方法中。

    您可能需要在工厂方法中添加一个临时参数来控制返回产品的类型。

    此时,工厂方法的代码可能看起来很丑陋。它可能有一个大型switch运算符来选择要实例化的产品类。但别担心,我们会尽快修复它。

  4. 现在,为工厂方法中列出的每种产品类型创建一组创建者子类。覆盖子类中的工厂方法并从基方法中提取适当的构造代码位。

  5. 如果产品类型太多,为所有产品创建子类没有意义,您可以在子类中重用基类中的控制参数。

    例如,假设您有以下类的层次结构:Mail带有几个子类的基类:AirMailGroundMail; TransportPlaneTruckTrain。_ 虽然AirMail该类仅使用Plane对象,GroundMail但可以同时使用TruckTrain对象。您可以创建一个新的子类(比如TrainMail)来处理这两种情况,但还有另一种选择。客户端代码可以将参数传递给类的工厂方法GroundMail来控制它想要接收的产品。

  6. 如果在所有提取之后,基础工厂方法变为空,您可以将其抽象化。如果还有一些东西,您可以将其设置为该方法的默认行为。

07 优缺点

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容