简单工厂模式:模式动机与定义
说实话,简单工厂模式是我最早接触的设计模式之一。那时候我刚从学校出来,写代码还停留在「一个类干所有事」的阶段。直到有一次,我接手了一个支付模块——微信支付、支付宝、银联、Apple Pay……每次新增一种支付方式,就要改一大片 if-else。改得我头皮发麻。
后来我意识到,其实可以把「创建对象」这件事单独拎出来。这就是简单工厂模式的初衷。
模式动机
你想想看,客户端代码里如果到处都是 new XXX(),一旦要换实现,就得改源码。这违反了开闭原则。简单工厂模式的核心动机就是:将对象的创建和使用分离。
说白了,客户端只需要告诉工厂「我要什么」,工厂帮你把对象造好。客户端不用关心这个对象是怎么 new 出来的,也不用关心它属于哪个具体类。
核心思想:用一个专门的工厂类来封装对象创建逻辑,客户端通过参数获取产品实例。
模式定义
简单工厂模式(Simple Factory Pattern)属于创建型模式,但它并不在 GoF 的 23 种设计模式之列。它更像是一种编程习惯,或者说是一个「准模式」。我个人的理解是:它是工厂方法模式的一个简化版,适用于产品种类不多且相对稳定的场景。
定义如下:
定义一个工厂类,根据传入的参数动态决定创建哪一种产品类的实例。
角色与结构
简单工厂模式的结构非常清晰,就三个角色。我在项目中画过无数次这种图,每次给新人讲都拿这个举例。
角色说明
| 角色 | 名称 | 职责 |
|---|---|---|
| Factory | 工厂类 | 核心类,负责创建产品对象。通常提供一个静态方法。 |
| Product | 抽象产品 | 定义产品的公共接口,所有具体产品必须实现它。 |
| ConcreteProduct | 具体产品 | 实现抽象产品接口的具体类,工厂负责创建它的实例。 |
结构图
下面这张图是我用 SVG 画的,展示了简单工厂模式的核心结构。你看,客户端只跟 Factory 和 Product 打交道,具体产品被藏起来了。
C++ 代码实现
好了,理论说完了,咱们直接上代码。我记得有一次给团队做代码评审,看到有人用 switch-case 写了 200 行的对象创建逻辑。我当时就说:「兄弟,咱们用简单工厂重构一下吧。」
下面是一个日志记录器的例子。假设我们有文件日志和控制台日志两种实现。
抽象产品
// ILogger.h
#pragma once
#include <string>
class ILogger {
public:
virtual ~ILogger() = default;
virtual void log(const std::string& message) = 0;
};
具体产品
// ConsoleLogger.h
#pragma once
#include "ILogger.h"
#include <iostream>
class ConsoleLogger : public ILogger {
public:
void log(const std::string& message) override {
std::cout << "[Console] " << message << std::endl;
}
};
// FileLogger.h
#pragma once
#include "ILogger.h"
#include <fstream>
class FileLogger : public ILogger {
public:
explicit FileLogger(const std::string& filename) : filename_(filename) {}
void log(const std::string& message) override {
std::ofstream file(filename_, std::ios::app);
if (file.is_open()) {
file << "[File] " << message << std::endl;
}
}
private:
std::string filename_;
};
工厂类
// LoggerFactory.h
#pragma once
#include "ILogger.h"
#include "ConsoleLogger.h"
#include "FileLogger.h"
#include <memory>
#include <stdexcept>
class LoggerFactory {
public:
enum class LoggerType {
Console,
File
};
static std::unique_ptr<ILogger> createLogger(LoggerType type) {
switch (type) {
case LoggerType::Console:
return std::make_unique<ConsoleLogger>();
case LoggerType::File:
return std::make_unique<FileLogger>("app.log");
default:
throw std::invalid_argument("Unknown logger type");
}
}
};
客户端使用
// main.cpp
#include "LoggerFactory.h"
int main() {
auto logger = LoggerFactory::createLogger(LoggerFactory::LoggerType::Console);
logger->log("系统启动成功");
auto fileLogger = LoggerFactory::createLogger(LoggerFactory::LoggerType::File);
fileLogger->log("写入日志文件");
return 0;
}
个人习惯:我一般把工厂方法设计成静态的,这样客户端不用先实例化工厂对象。但要注意,静态方法不利于单元测试中的 mock。如果你的项目对可测试性要求高,可以考虑用非静态方法配合依赖注入。
优缺点分析
任何模式都有两面性。简单工厂虽然好用,但也不是万能的。我曾经在一个项目中过度使用它,结果工厂类变得臃肿不堪,维护起来很痛苦。
优点
- 封装创建逻辑:客户端不需要知道具体产品类的名字,只需要传一个参数。说白了,把「怎么创建」这件事藏起来了。
- 代码集中管理:所有对象的创建都在一个地方,修改起来方便。比如要改所有日志记录器的构造参数,只需要改工厂类。
- 降低耦合:客户端只依赖抽象产品接口,不依赖具体实现。这符合依赖倒置原则。
缺点
- 工厂类职责过重:一旦产品种类增多,工厂类会变得很庞大。我曾经见过一个工厂类里有 30 多个 case 分支,那代码简直没法看。
- 违反开闭原则:新增产品类型时,必须修改工厂类。这其实违背了「对扩展开放,对修改关闭」的原则。
- 不适用于产品结构复杂的场景:如果产品对象的构造过程很复杂,需要多个步骤或者依赖其他对象,简单工厂就力不从心了。这时候可以考虑建造者模式。
避坑指南:我曾经在一个电商项目中,用简单工厂创建了 20 多种促销策略。结果每次新增促销活动,都要改工厂类,而且一不小心就改出 bug。后来我重构成了工厂方法模式,每个策略有自己的工厂,才解决了这个问题。所以我的建议是:产品种类不超过 5 个时,用简单工厂很合适;超过 5 个,考虑工厂方法或抽象工厂。
适用场景
| 场景 | 说明 |
|---|---|
| 产品种类少且稳定 | 比如日志记录器、数据库连接驱动等,种类固定不变 |
| 客户端只需要知道产品接口 | 客户端不关心具体实现,只关心能不能用 |
| 创建逻辑简单 | 不需要复杂的初始化参数或依赖注入 |
嗯,简单工厂模式就讲到这里。它虽然简单,但却是理解工厂方法模式和抽象工厂模式的基础。你想想看,如果连对象的创建都管理不好,后面的架构设计就更难展开了。