4. 工厂方法模式:模式动机与定义、角色与结构、C++代码实现、与简单工厂的对比

工厂方法模式,说白了就是让子类去决定创建哪个对象。我刚开始接触设计模式时,总觉得简单工厂已经够用了,干嘛还要搞这么复杂?直到有一次在项目中踩了坑,才真正理解它的价值。

4.1 模式动机:为什么需要工厂方法?

先说说简单工厂的问题。你想想看,简单工厂把所有对象的创建逻辑都塞到一个函数里。每次新增产品,都得改那个工厂函数。这违反了开闭原则——对扩展开放,对修改关闭。

我在项目中遇到过这样一个场景:一个日志系统,需要支持文件日志、数据库日志、网络日志。一开始用简单工厂,挺爽的。后来需求变了,要加一个加密日志。我不得不去改那个工厂函数。改完之后,测试团队发现原有的文件日志创建逻辑被我无意中改坏了。嗯,这就是耦合的代价。

工厂方法模式的动机很直接:把对象的创建延迟到子类。这样,新增产品时,你只需要新增一个工厂子类,不用动已有的代码。

核心思想:定义一个创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

4.2 模式定义与角色

工厂方法模式涉及四个角色:

角色 名称 职责
Product 抽象产品 定义产品的接口
ConcreteProduct 具体产品 实现产品接口的具体类
Creator 抽象创建者 声明工厂方法,返回Product对象
ConcreteCreator 具体创建者 实现工厂方法,返回具体的ConcreteProduct

这里有个关键点:Creator类里通常还会包含一些业务逻辑,这些逻辑依赖于工厂方法返回的产品对象。说白了,Creator不关心具体是哪个产品,它只关心产品能干什么。

4.3 核心结构图

下面这张图展示了工厂方法模式的结构。我习惯用这种图来跟团队沟通设计思路,一目了然。

工厂方法模式结构图 Creator + factoryMethod(): Product + someOperation(): void // 调用factoryMethod() ConcreteCreatorA + factoryMethod(): Product ConcreteCreatorB + factoryMethod(): Product Product + operation(): void ConcreteProductA + operation(): void ConcreteProductB + operation(): void 依赖 创建 创建

4.4 C++代码实现

来看一个实际的例子。假设我们要开发一个跨平台的UI按钮库。不同操作系统下,按钮的样式不同。我们用工厂方法模式来实现。

// 抽象产品:按钮
class Button {
public:
    virtual ~Button() = default;
    virtual void render() = 0;
    virtual void onClick() = 0;
};

// 具体产品:Windows按钮
class WindowsButton : public Button {
public:
    void render() override {
        std::cout << "渲染Windows风格按钮" << std::endl;
    }
    void onClick() override {
        std::cout << "Windows按钮点击事件" << std::endl;
    }
};

// 具体产品:Linux按钮
class LinuxButton : public Button {
public:
    void render() override {
        std::cout << "渲染Linux风格按钮" << std::endl;
    }
    void onClick() override {
        std::cout << "Linux按钮点击事件" << std::endl;
    }
};

// 抽象创建者
class Dialog {
public:
    virtual ~Dialog() = default;
    
    // 工厂方法
    virtual std::unique_ptr<Button> createButton() = 0;
    
    // 业务逻辑,依赖工厂方法
    void renderWindow() {
        auto btn = createButton();
        btn->render();
        btn->onClick();
    }
};

// 具体创建者:Windows对话框
class WindowsDialog : public Dialog {
public:
    std::unique_ptr<Button> createButton() override {
        return std::make_unique<WindowsButton>();
    }
};

// 具体创建者:Linux对话框
class LinuxDialog : public Dialog {
public:
    std::unique_ptr<Button> createButton() override {
        return std::make_unique<LinuxButton>();
    }
};
使用建议:我个人习惯在工厂方法中返回 std::unique_ptrstd::shared_ptr,避免裸指针带来的内存管理问题。C++11之后,智能指针是标配。

4.5 与简单工厂的对比

简单工厂和工厂方法,到底选哪个?我直接给你列个对比表:

对比维度 简单工厂 工厂方法
开闭原则 违反。新增产品需修改工厂类 符合。新增产品只需新增子类
复杂度 低。一个工厂类搞定 中等。需要定义工厂接口和子类
产品种类 适合产品种类少且稳定的场景 适合产品种类多且频繁扩展的场景
代码耦合 工厂类与所有产品耦合 每个工厂子类只与对应产品耦合
扩展方式 修改已有代码 新增代码,不修改已有代码

简单工厂适合什么场景?我举个例子:你做一个工具类,就生成两三种日志对象,而且确定以后不会再加了。那简单工厂完全够用,别过度设计。

工厂方法适合什么场景?你想想看,如果你在做一个插件系统,第三方开发者要能注册自己的产品。这时候简单工厂就玩不转了。你总不能每次有新的插件,都去改你的核心代码吧?

避坑指南:我曾经在一个项目中,一开始用简单工厂,觉得够用。后来产品线扩张,产品类型从3种变成了15种。那个工厂函数变得又臭又长,每次修改都提心吊胆。重构时改成工厂方法,虽然多了几个类,但每个类的职责清晰了,测试也好写了。所以我的建议是:如果你不确定未来会不会扩展,优先考虑工厂方法。多写几个类,比以后重构要划算得多。

4.6 什么时候用工厂方法?

总结一下,我个人判断是否使用工厂方法的几个标准:

  • 客户端不关心产品创建细节:客户端只需要知道产品能干什么,不需要知道怎么创建的
  • 产品类型可能扩展:未来可能会新增产品类型,而且不希望修改已有代码
  • 产品创建逻辑复杂:创建产品需要读取配置、连接数据库、或者做一些初始化操作
  • 需要解耦:你想让高层模块不依赖低层模块的具体实现

嗯,工厂方法模式就是这么回事。它不复杂,但用好了能让你的代码更灵活、更健壮。记住一点:设计模式是工具,不是目的。别为了用模式而用模式,要在真正需要的时候才出手。


公众号:蓝海资料掘金营,微信deep3321