第17章:从SRP到工厂模式:工厂模式如何体现单一职责

单一职责原则(SRP)说起来简单——一个类只做一件事。但真正落地时,你会发现它无处不在。今天我想聊聊工厂模式,看看它怎么把SRP玩得明明白白。

工厂模式有三种常见形态:简单工厂、工厂方法、抽象工厂。它们都围绕一个核心思想:把对象的创建和使用分离。说白了,就是让创建对象的职责独立出来。

17.1 为什么需要工厂模式?

先看一个反面教材。假设你写了一个日志系统,支持文件日志、数据库日志、网络日志:

class Logger {
public:
    void log(const std::string& msg) {
        if (type == "file") {
            // 写文件
        } else if (type == "db") {
            // 写数据库
        } else if (type == "network") {
            // 发网络请求
        }
    }
private:
    std::string type;
};

这段代码有什么问题?一个类干了三件事:判断类型、创建资源、执行日志。每次新增日志类型,你都得改这个类。我在项目中见过类似的代码,一个类里塞了七八种if-else,改一次提心吊胆一次。

工厂模式就是来解决这个问题的。它把「创建对象」这个职责单独拎出来,让Logger只关心「写日志」这件事。

17.2 简单工厂:最直接的职责分离

简单工厂是最朴素的做法。它用一个专门的类来创建对象:

class LoggerFactory {
public:
    static Logger* create(const std::string& type) {
        if (type == "file") return new FileLogger();
        if (type == "db") return new DbLogger();
        if (type == "network") return new NetworkLogger();
        return nullptr;
    }
};

// 使用
Logger* logger = LoggerFactory::create("file");
logger->log("hello");

你看,LoggerFactory只负责创建,Logger只负责记录。职责边界清晰了。

我的经验:简单工厂适合产品类型少、变化不频繁的场景。我曾经在一个小工具项目里用简单工厂管理三种数据导出格式,代码量少,维护起来很舒服。

但简单工厂有个硬伤:每次新增产品,都要改工厂类。这违反了开闭原则。怎么办?工厂方法模式上场了。

17.3 工厂方法:把创建延迟到子类

工厂方法模式把工厂类也抽象化。每个产品对应一个工厂子类:

class LoggerFactory {
public:
    virtual Logger* create() = 0;
    virtual ~LoggerFactory() = default;
};

class FileLoggerFactory : public LoggerFactory {
public:
    Logger* create() override { return new FileLogger(); }
};

class DbLoggerFactory : public LoggerFactory {
public:
    Logger* create() override { return new DbLogger(); }
};

// 使用
LoggerFactory* factory = new FileLoggerFactory();
Logger* logger = factory->create();

这样一来,新增一种日志类型,你只需要新增一个产品类和一个工厂类,已有的代码一行都不用改。每个工厂类只负责创建一种产品,SRP贯彻到底。

核心变化:简单工厂把创建逻辑集中在一个类里,工厂方法把创建逻辑分散到各个子类。前者是「集中管理」,后者是「分而治之」。

我曾经接手过一个支付系统,支持微信、支付宝、银联三种支付方式。原代码用简单工厂,每次新增支付方式都要改工厂类,还经常改出bug。后来重构为工厂方法模式,每个支付方式一个工厂,新增支付方式就像搭积木一样简单。

17.4 抽象工厂:创建一组相关对象

当你的产品不是单个对象,而是一组相关对象时,抽象工厂就派上用场了。

举个例子。一个UI框架需要支持不同操作系统风格。Windows风格下,按钮是圆角的、菜单是蓝色的;Mac风格下,按钮是扁平的、菜单是灰色的。这些对象是成套出现的:你不能在Windows风格下用Mac的按钮。

class UIFactory {
public:
    virtual Button* createButton() = 0;
    virtual Menu* createMenu() = 0;
    virtual ~UIFactory() = default;
};

class WindowsUIFactory : public UIFactory {
public:
    Button* createButton() override { return new WindowsButton(); }
    Menu* createMenu() override { return new WindowsMenu(); }
};

class MacUIFactory : public UIFactory {
public:
    Button* createButton() override { return new MacButton(); }
    Menu* createMenu() override { return new MacMenu(); }
};

抽象工厂的职责很明确:保证创建的一组对象是兼容的。每个具体工厂只负责一个产品族,SRP依然成立。

注意:抽象工厂的缺点是扩展产品族容易,但扩展产品等级困难。比如你想新增一个「文本框」组件,所有具体工厂都得改。设计时要想清楚哪些是稳定的,哪些是易变的。

17.5 三种工厂模式的对比

模式 职责范围 扩展方式 适用场景
简单工厂 一个工厂创建所有产品 修改工厂类 产品少、变化少
工厂方法 每个工厂创建一种产品 新增工厂子类 产品类型经常扩展
抽象工厂 每个工厂创建一组产品 新增工厂子类 产品族成套出现

17.6 工厂模式与SRP的关系

回到单一职责原则。工厂模式到底怎么体现SRP?我总结了三层:

  1. 创建职责独立:把对象的创建从业务逻辑中抽离出来,让业务类只关心业务。
  2. 每个工厂职责单一:简单工厂负责所有创建,工厂方法每个子类只创建一种产品,抽象工厂每个子类只创建一个产品族。
  3. 客户端职责简化:客户端不再需要知道如何创建对象,只需要告诉工厂「我要什么」。

你想想看,如果没有工厂模式,创建逻辑散落在各个业务类里,每个类都承担了「创建」和「使用」两个职责。一旦创建逻辑变化,所有相关类都得改。这就是典型的SRP被破坏。

一句话总结:工厂模式是SRP在对象创建领域的最佳实践。它让「创建」这件事有了自己的归属地。

17.7 知识体系图

下面这张图展示了工厂模式与SRP的关系,以及三种工厂模式的演进脉络:

工厂模式与SRP关系图 单一职责原则 (SRP) 核心思想:将对象创建职责独立出来 简单工厂 一个工厂创建所有产品 职责集中,扩展需改工厂 工厂方法 每个工厂创建一种产品 职责分散,扩展新增子类 抽象工厂 每个工厂创建一组产品 保证产品族兼容性 共同目标:让「创建」和「使用」各司其职 每个工厂类只承担一种创建职责,完美符合SRP

17.8 避坑指南

最后分享几个我踩过的坑:

  • 不要滥用工厂模式:如果只是创建一两个对象,直接new就行了。工厂模式也有成本,多了反而增加复杂度。
  • 简单工厂不是设计模式:严格来说,简单工厂不是GoF的23种设计模式之一,它更像是一种编程习惯。但实践中很好用。
  • 工厂方法不要和模板方法混淆:工厂方法用虚函数创建对象,模板方法用虚函数定义算法骨架。两者目的不同。
  • 抽象工厂不要过度设计:我曾经在一个只需要两种产品的项目里用了抽象工厂,结果代码量翻了一倍。后来发现简单工厂就够用了。

工厂模式的核心价值,就是让「创建」这件事有了自己的职责边界。它不复杂,但用好了能让代码结构清晰很多。下次你写if-else创建对象时,不妨想想:是不是该把创建职责独立出来了?

专注资料整理