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

工厂方法模式,说白了就是让子类去决定创建哪个对象。我刚开始接触设计模式时,总觉得简单工厂已经够用了,干嘛还要搞这么复杂?直到有一次我在项目中遇到了一个需求变更的噩梦……嗯,从那以后我才真正理解了工厂方法的威力。

4.1 模式动机与定义

先说说简单工厂的问题。你想想看,简单工厂把所有对象的创建逻辑都塞在一个函数里,用if-else或者switch去判断该new哪个类。这看起来挺方便,但一旦要新增产品类型,你就得改那个工厂函数。开闭原则呢?对扩展开放,对修改关闭——简单工厂恰恰违背了这一点。

我在一个跨平台图形库项目中就吃过这个亏。当时用简单工厂创建不同操作系统的按钮,Windows、macOS、Linux各一个。后来要加一个移动端版本,我不得不去修改那个已经稳定运行了半年的工厂函数。改完之后,Windows的按钮莫名其妙出了bug,排查了半天才发现是改代码时不小心动了一个条件分支。

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

定义:工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类的实例化推迟到子类。

4.2 角色与结构

工厂方法模式有四个核心角色,我习惯把它们分成两组来理解:

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

结构上其实很对称:产品这边一套接口+实现,工厂那边也是一套接口+实现。工厂方法返回的是产品接口类型,具体工厂决定返回哪个具体产品。

«interface» Product ConcreteProductA ConcreteProductB «interface» Creator + FactoryMethod(): Product ConcreteCreatorA ConcreteCreatorB 创建 Client 使用 产品体系 工厂体系 依赖关系

4.3 C++代码实现

直接上代码。还是拿日志系统举例,假设我们要支持文件日志和控制台日志两种方式。

// 产品接口
class Logger {
public:
    virtual ~Logger() = default;
    virtual void log(const std::string& message) = 0;
};

// 具体产品:文件日志
class FileLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << "[FileLogger] " << message << std::endl;
        // 实际项目中这里会写入文件
    }
};

// 具体产品:控制台日志
class ConsoleLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << "[ConsoleLogger] " << message << std::endl;
    }
};

// 工厂接口
class LoggerFactory {
public:
    virtual ~LoggerFactory() = default;
    virtual std::unique_ptr<Logger> createLogger() = 0;
};

// 具体工厂:文件日志工厂
class FileLoggerFactory : public LoggerFactory {
public:
    std::unique_ptr<Logger> createLogger() override {
        return std::make_unique<FileLogger>();
    }
};

// 具体工厂:控制台日志工厂
class ConsoleLoggerFactory : public LoggerFactory {
public:
    std::unique_ptr<Logger> createLogger() override {
        return std::make_unique<ConsoleLogger>();
    }
};

客户端使用起来是这样的:

void clientCode(LoggerFactory& factory) {
    auto logger = factory.createLogger();
    logger->log("系统启动成功");
}

int main() {
    FileLoggerFactory fileFactory;
    ConsoleLoggerFactory consoleFactory;
    
    clientCode(fileFactory);    // 输出: [FileLogger] 系统启动成功
    clientCode(consoleFactory); // 输出: [ConsoleLogger] 系统启动成功
    
    return 0;
}

个人经验:我习惯用 std::unique_ptr 管理工厂创建的对象,这样调用方不用操心内存释放。如果你需要多个对象共享同一个日志实例,可以考虑返回 std::shared_ptr。

4.4 与简单工厂对比

简单工厂和工厂方法,说白了就是「集中管理」和「分散管理」的区别。我整理了一个对比表,你一看就明白:

对比维度 简单工厂 工厂方法
创建逻辑位置 一个工厂类集中处理 分散到各个子工厂
新增产品 修改工厂类(违反开闭原则) 新增工厂子类(符合开闭原则)
产品种类少时 代码简洁,够用 略显冗余
产品种类多时 工厂类膨胀,难以维护 结构清晰,易于扩展
适用场景 产品类型稳定,很少变化 产品类型频繁扩展

避坑指南:我曾经在一个项目中过度使用工厂方法,每个产品都配一个工厂,结果产品只有两三种,工厂文件却建了一大堆。后来我反思了一下:模式不是越多越好,合适才是关键。产品类型稳定时,简单工厂完全够用;只有当产品线确实会频繁扩展时,才值得上工厂方法。

还有一个细节值得注意:简单工厂通常用静态方法,工厂方法则依赖多态。这意味着工厂方法模式天然支持依赖注入,你可以把工厂对象作为参数传递,在测试时轻松替换为mock工厂。这一点在大型项目中特别有用。

嗯,工厂方法模式就讲到这里。记住一句话:把变化封装起来,让扩展变得优雅。下次当你发现简单工厂的switch-case越来越长时,就该考虑用工厂方法重构了。


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