在C++项目里,当你发现上层业务代码到处出现new ConcreteProduct这样的语句时,基本可以断定代码耦合已经出问题了:一旦产品类型增加或者实现方式变更,所有调用点都要跟着改。工厂方法模式就是解决这个问题的经典方案,它通过定义一个创建对象的接口,把真正的实例化动作交给子类完成,让对象创建过程与使用过程彻底解耦。本文从抽象基类的设计入手,一步步给出完整的C++实现方案。

工厂方法模式的核心结构
工厂方法模式包含四个角色:抽象产品、具体产品、抽象工厂和具体工厂。抽象产品通常是一个包含纯虚函数的基类,它只声明"能做什么",不关心"怎么做";具体产品继承抽象产品并实现全部接口;抽象工厂声明一个返回抽象产品指针的工厂方法;具体工厂则重写这个方法,返回对应具体产品的实例。
这种结构的关键在于依赖方向的转变。上层模块只依赖抽象工厂和抽象产品两个接口,完全不知道具体产品的存在。当需要更换产品实现时,只需要替换具体工厂,上层代码一行都不用动。这就是所谓的设计原则:"依赖于抽象,而不依赖于具体实现"。
下面是标准的类结构示意代码:
// 抽象产品
class Product {
public:
virtual ~Product() = default;
virtual void use() = 0;
};
// 具体产品A
class ConcreteProductA : public Product {
public:
void use() override { /* A的实现 */ }
};
// 抽象工厂
class Factory {
public:
virtual ~Factory() = default;
virtual Product* createProduct() = 0;
};
// 具体工厂A
class ConcreteFactoryA : public Factory {
public:
Product* createProduct() override {
return new ConcreteProductA();
}
};完整实现方案:智能指针管理对象生命周期
裸指针在C++里容易造成内存泄漏,尤其是上层拿到产品指针后忘记释放的情况非常常见。现代C++推荐工厂方法返回std::unique_ptr,明确所有权归属,调用方用完自动释放,安全性和可读性都大幅提升。
另一个必须重视的细节是虚析构函数。抽象产品基类必须声明virtual ~Product() = default;,否则通过基类指针delete派生类对象属于未定义行为,析构函数不会被正确调用,资源就会泄漏。这是手写工厂模式时最容易踩的坑之一。
下面给一个完整的可编译示例,模拟日志记录器的创建场景:
#include <iostream>
#include <memory>
#include <string>
// 抽象产品:日志记录器
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& msg) = 0;
};
// 具体产品:控制台记录器
class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[Console] " << msg << std::endl;
}
};
// 具体产品:文件记录器
class FileLogger : public Logger {
public:
void log(const std::string& msg) override {
// 实际项目中写入文件,这里简化输出
std::cout << "[File] " << msg << std::endl;
}
};
// 抽象工厂
class LoggerFactory {
public:
virtual ~LoggerFactory() = default;
virtual std::unique_ptr<Logger> createLogger() = 0;
};
// 具体工厂
class ConsoleLoggerFactory : public LoggerFactory {
public:
std::unique_ptr<Logger> createLogger() override {
return std::make_unique<ConsoleLogger>();
}
};
class FileLoggerFactory : public LoggerFactory {
public:
std::unique_ptr<Logger> createLogger() override {
return std::make_unique<FileLogger>();
}
};
int main() {
std::unique_ptr<LoggerFactory> factory = std::make_unique<FileLoggerFactory>();
auto logger = factory->createLogger();
logger->log("服务启动成功");
return 0;
}运行后输出[File] 服务启动成功。注意main函数里上层代码只接触LoggerFactory和Logger两个抽象类型,把FileLoggerFactory换成ConsoleLoggerFactory就能切换实现,完全不影响业务逻辑。
简单工厂、工厂方法与抽象工厂如何选择
简单工厂不是标准GoF模式,它用一个静态函数加switch分支来创建对象,写法简单但违反开闭原则:每新增一种产品都要修改工厂函数的判断逻辑。它适合产品种类少且稳定的场景,比如内部小工具代码。
工厂方法每新增一种产品只需添加一对新的具体产品和具体工厂类,完全符合开闭原则,代价是类的数量膨胀。如果你的产品只有一个维度变化,比如上面的日志器只区分输出介质,用工厂方法就够了。
抽象工厂则用于产品族场景,即产品存在多个维度的组合变化,比如界面库中Windows风格的按钮加滚动条、Linux风格的按钮加滚动条。三种模式的对比可以归纳如下:
| 模式 | 产品维度 | 开闭原则 | 类数量 |
|---|---|---|---|
| 简单工厂 | 单一产品,分支判断 | 不满足 | 少 |
| 工厂方法 | 单一产品类型,子类决定 | 满足 | 中等 |
| 抽象工厂 | 多产品组成的族 | 族内满足 | 多 |
常见误区与进阶技巧
第一个误区是工厂方法返回值用裸指针却不约定所有权,团队里多人使用时几乎必然出现重复释放或者泄漏。规范做法是统一返回std::unique_ptr,需要共享所有权时再改成std::shared_ptr。
第二个误区是过度设计。如果产品类型就两三种且几乎不会扩展,硬套工厂方法反而增加复杂度,直接构造更清晰。设计模式的价值在于应对变化,没有变化就没有必要引入额外抽象层。
进阶技巧方面,可以结合模板实现静态工厂,减少类膨胀:
template <typename T>
class GenericFactory : public Factory {
public:
std::unique_ptr<Product> createProduct() override {
return std::make_unique<T>();
}
};
// 使用方式,无需为每个产品单独写工厂类
GenericFactory<ConcreteProductA> factoryA;还可以注册一个全局的工厂注册表,用字符串键映射到创建函数,实现运行时按配置创建产品,这在插件式架构中非常实用。无论采用哪种变体,记住核心不变:抽象基类定义契约,具体产品负责实现,工厂负责把两者连接起来,而上层代码永远只面向抽象编程。