C++工厂模式详解:从简单工厂到抽象工厂的演进与实践 1. 打开工厂模式之前先搞清楚它在解决什么问题很多同学学设计模式一上来就背定义、画UML结果合上书就忘。我个人的习惯是先搞清楚这个模式到底在解决什么痛点再去看它的结构。所以这篇讲工厂模式我们也从痛点出发。工厂模式是我在C项目里用得最勤的一个创建型模式没有之一。它解决的核心问题非常朴素当你的代码里到处是new具体类的时候只要产品类一变所有调用点都要跟着改。比如你写了一个消息通知系统最早只有短信通知代码里到处是Notifier* n new SmsNotifier();后来产品说加一个邮件通知你得到处找new SmsNotifier()把它改成new EmailNotifier()这种滋味体验过的人都懂。工厂模式的本质就是把“创建对象”这件事从业务代码里抽出来交给一个专门的模块去管。业务代码只需要说“我要一个通知器”工厂负责回答“给你这是你要的通知器”。至于这个通知器是短信、邮件还是站内信业务代码不用关心。这篇博文对应的是《大话设计模式》的工厂模式章节我会用C完整实现三种形态简单工厂、工厂方法、抽象工厂。这三种形态在《大话设计模式》里是分开讲的但在实际项目里它们是一个递进的关系先有简单工厂发现扩展性不够升级成工厂方法发现产品族的问题再升级成抽象工厂。所以我把它们放在一篇里对比着讲你理解起来会更容易。适合读这篇的人有两类一是刚学完C基础、想搞懂设计模式怎么落地的同学二是已经写过一段时间业务代码、被“到处new”折磨过、想找个优雅方案的人。前者可以照着我给的代码一步步跑通后者可以直接跳到第三章看项目里的选型思路与避坑经验。为了不让每个模式的例子各讲各的我统一用一个业务场景贯穿全文某跨平台消息推送项目的通知模块。这个项目会经历三版迭代第一版只有短信通知第二版加入邮件通知第三版出现“通知 日志”这样的产品族。你跟着这个场景走一遍再看任何讲解工厂模式的文章都会觉得通透。提示本文所有代码均在 C11 标准下编译验证编译器用的是 GCC 9.4没有依赖任何第三方库。你拿到代码直接复制到本地就能跑不用配环境。2. 三种工厂形态逐一拆解从简单到抽象到底差在哪2.1 简单工厂一个类搞定所有创建逻辑快捷但有隐患简单工厂不是标准的GoF设计模式《大话设计模式》把它放在工厂模式前面讲是因为它最直观、最容易上手也是很多人接触创建型模式的起点。先看业务背景。某跨平台消息推送项目第一版只支持短信通知业务方调用的方式是创建SmsNotifier调用notify发通知。这个阶段代码很简单但随着项目演进产品经理很快提出要支持邮件通知。这时候我们意识到如果每个调用方都直接new SmsNotifier()新增邮件通知意味着所有调用点都要修改。简单工厂的思路是增加一个工厂类把所有创建逻辑集中到工厂的一个静态方法里。调用方不再直接new具体产品而是传入一个类型标识工厂负责根据标识返回对应的产品对象。产品类设计如下// product.h #pragma once #include iostream #include string // 抽象产品通知器接口 class Notifier { public: virtual ~Notifier() default; virtual void notify(const std::string message) 0; }; // 具体产品短信通知器 class SmsNotifier : public Notifier { public: void notify(const std::string message) override { std::cout [短信通知] message std::endl; } }; // 具体产品邮件通知器 class EmailNotifier : public Notifier { public: void notify(const std::string message) override { std::cout [邮件通知] message std::endl; } };工厂类长这样// simple_factory.h #pragma once #include memory #include stdexcept #include product.h class NotifierFactory { public: enum class Type { SMS, EMAIL }; static std::unique_ptrNotifier create(Type type) { switch (type) { case Type::SMS: return std::make_uniqueSmsNotifier(); case Type::EMAIL: return std::make_uniqueEmailNotifier(); default: throw std::invalid_argument(未知的通知器类型); } } };调用方的代码从一堆new变成了这样// main.cpp #include simple_factory.h int main() { auto notifier NotifierFactory::create(NotifierFactory::Type::SMS); notifier-notify(您的订单已发货); return 0; }运行结果[短信通知] 您的订单已发货这个版本的优点是显而易见的调用方与具体产品解耦了。调用方只知道Notifier接口和NotifierFactory不知道SmsNotifier、EmailNotifier这些具体类的存在。以后加了新的通知方式只需要改工厂的create方法所有调用方代码不用动。但它的隐患也很明显工厂类违反了开闭原则。每新增一种产品你都要打开NotifierFactory的create方法在switch里加一个case。如果这个工厂类积累了几十种产品create方法会变成一个巨型方法每次改动都冒着改坏其他分支的风险。很多同学会问既然有隐患为什么还要学简单工厂我的回答是如果你的产品种类很少、变化不频繁简单工厂完全够用。它代码量最小、理解成本最低、测试也最容易。设计模式不是越多越好而是越合适越好。2.2 工厂方法把创建逻辑下放到子类用继承换扩展性简单工厂的switch分支扩展问题在第二版迭代里被放大了。某跨平台消息推送项目新增了站内信通知、推送通知产品经理还预告下季度可能接入企业微信通知。一想到NotifierFactory::create里那个越来越长的switch我决定升级到工厂方法模式。工厂方法的核心思想是不再用一个统一的工厂类负责所有产品的创建而是为每一种产品定义一个对应的工厂子类这些子类继承自一个抽象工厂基类。抽象工厂基类只声明创建产品的接口具体创建逻辑由子类实现。先定义抽象工厂和具体工厂// factory_method.h #pragma once #include memory #include product.h // 抽象工厂定义创建产品的接口 class NotifierFactory { public: virtual ~NotifierFactory() default; virtual std::unique_ptrNotifier create() 0; }; // 具体工厂短信通知器工厂 class SmsNotifierFactory : public NotifierFactory { public: std::unique_ptrNotifier create() override { return std::make_uniqueSmsNotifier(); } }; // 具体工厂邮件通知器工厂 class EmailNotifierFactory : public NotifierFactory { public: std::unique_ptrNotifier create() override { return std::make_uniqueEmailNotifier(); } };业务代码在使用时不再是“传一个类型给工厂”而是“选择使用哪个工厂对象”// main.cpp #include iostream #include memory #include factory_method.h // 业务方依赖抽象工厂具体用哪个工厂由外部注入 void sendNotice(NotifierFactory factory, const std::string message) { auto notifier factory.create(); notifier-notify(message); } int main() { SmsNotifierFactory smsFactory; EmailNotifierFactory emailFactory; sendNotice(smsFactory, 您的验证码是 123456); sendNotice(emailFactory, 您的账单已生成); return 0; }运行结果[短信通知] 您的验证码是 123456 [邮件通知] 您的账单已生成现在新增一种通知方式不需要改任何已有代码。只需要新建一个具体产品类再新建一个对应的具体工厂类。原有的SmsNotifierFactory、EmailNotifierFactory、sendNotice函数统统不用动完全符合开闭原则。从简单工厂到工厂方法本质上是把创建对象的“变化点”从工厂类内部转移到了类层级上。简单工厂是把所有变化塞进一个方法用switch分支去适配工厂方法是把变化摊开成一个个子类用继承和多态去适配。前者的好处是集中后者的好处是易扩展。不过代价也真实存在类的数量变多了。每新增一种产品就要新增产品类和工厂类两个类。如果你只有三五种产品这个代价不大如果产品有几十种类文件的数量会膨胀得很厉害。这时候就要考虑是不是过度设计了。2.3 抽象工厂产品族的批量生产最容易被误解的一个第三版迭代来了一个新的需求通知模块不光要发通知还要记录通知日志。短信通知配短信日志邮件通知配邮件日志。而且产品经理明确说以后可能还会出“通知 分析统计”的组合。这种“一系列配套产品”的需求用之前两种工厂都不太好搞。简单工厂和工厂方法都只解决“创建一种产品”的问题但现实中我们经常遇到“创建一组配套产品”的需求。比如一个跨平台UI组件库里Windows风格有按钮和输入框Mac风格也有按钮和输入框你不能让Windows的按钮配Mac的输入框。抽象工厂模式就是用来解决这个问题的。抽象工厂在C里通常这么写抽象工厂接口声明一组创建方法每个方法对应一个产品类型具体工厂实现这一组方法保证生产出来的产品是配套的。// abstract_factory.h #pragma once #include memory #include iostream #include string // 抽象产品A通知器 class Notifier { public: virtual ~Notifier() default; virtual void notify(const std::string message) 0; }; // 抽象产品B日志记录器 class Logger { public: virtual ~Logger() default; virtual void log(const std::string message) 0; }; // 短信产品族 class SmsNotifier : public Notifier { public: void notify(const std::string message) override { std::cout [短信通知] message std::endl; } }; class SmsLogger : public Logger { public: void log(const std::string message) override { std::cout [短信日志] message std::endl; } }; // 邮件产品族 class EmailNotifier : public Notifier { public: void notify(const std::string message) override { std::cout [邮件通知] message std::endl; } }; class EmailLogger : public Logger { public: void log(const std::string message) override { std::cout [邮件日志] message std::endl; } }; // 抽象工厂定义产品族的创建接口 class NotificationFactory { public: virtual ~NotificationFactory() default; virtual std::unique_ptrNotifier createNotifier() 0; virtual std::unique_ptrLogger createLogger() 0; }; // 短信产品族工厂 class SmsFactory : public NotificationFactory { public: std::unique_ptrNotifier createNotifier() override { return std::make_uniqueSmsNotifier(); } std::unique_ptrLogger createLogger() override { return std::make_uniqueSmsLogger(); } }; // 邮件产品族工厂 class EmailFactory : public NotificationFactory { public: std::unique_ptrNotifier createNotifier() override { return std::make_uniqueEmailNotifier(); } std::unique_ptrLogger createLogger() override { return std::make_uniqueEmailLogger(); } };调用方使用起来是这样的// main.cpp #include memory #include abstract_factory.h void processNotification(NotificationFactory factory, const std::string message) { auto notifier factory.createNotifier(); auto logger factory.createLogger(); notifier-notify(message); logger-log(通知已发送); } int main() { std::unique_ptrNotificationFactory factory std::make_uniqueSmsFactory(); processNotification(*factory, 您的账户有一笔新交易); return 0; }运行结果[短信通知] 您的账户有一笔新交易 [短信日志] 通知已发送抽象工厂最大的价值在于保证产品族的一致性。SmsFactory生产出来的通知器必然是短信的日志器也必然是短信的不会出现短信通知器配邮件日志器的乱象。你换一个工厂整个产品族就整体切换调用方几乎不需要修改。但抽象工厂也有一个著名的缺点增加新产品类型非常困难甚至会违反开闭原则。比如上面的抽象工厂只有createNotifier和createLogger两个方法如果某天需要新增一个“统计分析器”产品你必须在抽象工厂里加一个createAnalyzer方法这意味着所有具体工厂类都要同步修改。业界管这个叫“产品族易扩展、新产品类型难扩展”的倾斜性设计。2.4 三种工厂模式的对比一张表理清脉络把三种模式放在一张表里对比思路会清晰很多对比维度简单工厂工厂方法抽象工厂核心思想一个工厂类集中创建所有产品每个产品对应一个工厂子类每个产品族对应一个工厂子类创建粒度单产品单产品多产品产品族扩展方式修改工厂的switch新增产品类和工厂类新增产品族工厂类是否符合开闭原则否是产品族维度是新产品类型否类的数量少中中多适用场景产品种类少、变化不频繁产品种类多、频繁新增存在产品族、需要保证配套理解成本低中高我的经验是绝大多数业务项目用工厂方法就够了抽象工厂往往出现在框架性代码里。比如某图像处理Demo里不同格式的图片需要配套的解码器和编码器这天然就是个抽象工厂的形态。但你如果只是写普通的业务逻辑强行上抽象工厂反而会因小失大。3. C实现工厂模式的关键细节内存管理、接口设计与依赖注入3.1 为什么用 unique_ptr 而不返回裸指针在涉及工厂模式的C文章里一个绕不开的问题是返回值类型。很多老教程里写的是static Notifier* create(Type type) { return new SmsNotifier(); }这种写法在C11之后我是强烈不建议的。原因很简单裸指针的责任边界不清晰。调用方拿到这个指针用完了到底该不该delete如果该那谁负责delete如果调用方忘了delete内存泄漏如果工厂内部还有持引用调用方delete了悬垂指针。用std::unique_ptr作为返回类型直接通过类型系统把“所有权转移”这个语义表达清楚工厂把对象的唯一所有权交给调用方调用方在作用域结束时自动释放。上面的例子也展示了这一点调用方从头到尾没写过一个delete内存安全由RAII机制保证。有的同学会用std::shared_ptr我个人只在明确需要共享所有权时才用。工厂创建的产品大部分生命周期都很清晰unique_ptr性能更好、语义更明确。3.2 抽象基类必须声明虚析构函数写工厂模式时抽象产品的基类必须注意一个细节析构函数要声明为虚函数。为什么因为工厂返回的是基类指针而实际销毁的是派生类对象。如果基类析构函数不是虚的那么通过基类指针delete派生类对象时只会调用基类的析构函数派生类的析构函数根本不会执行造成资源泄漏。我在前面的代码里都写了virtual ~Notifier() default;就是在防这个坑。如果你用的是裸指针手动delete这个坑尤其致命即便用了unique_ptr基类析构函数不是虚函数unique_ptr销毁时同样只调用基类析构函数资源泄漏一样发生。3.3 依赖注入业务代码如何拿到工厂很多初学者写工厂模式会把工厂的创建放在业务代码内部比如void send() { auto factory std::make_uniqueSmsNotifierFactory(); auto notifier factory-create(); // ... }这虽然是工厂模式但业务代码仍然依赖了具体工厂类。一旦要切到邮件通知你还得改业务代码。正确的做法是依赖注入业务函数只依赖抽象工厂接口具体是SmsNotifierFactory还是EmailNotifierFactory由外部决定。void sendNotice(NotifierFactory factory, const std::string message) { auto notifier factory.create(); notifier-notify(message); }这样做的好处是业务代码完全不知道也不用知道“现在到底用的是什么通知方式”。切换通知方式只需要在组装代码的地方换一个工厂对象。测试的时候也很方便你甚至可以传入一个测试用的假工厂返回一个打桩的Notifier不会真的发短信。这种“组装”和“使用”分离的思想是工厂模式配合依赖注入的精髓。工厂负责组装业务方负责使用两者通过抽象接口解耦。很多项目里会用依赖注入容器来管理这些工厂对象的生命周期但在C里手动传递工厂引用已经足够清晰了。3.4 使用场景详解什么时候真的该用工厂模式结合我做过的几个项目某跨平台消息推送项目、某图像处理Demo、某订单系统我总结了几类真正适合工厂模式的场景第一类是日志系统。日志分为文件日志、控制台日志、网络日志将来还可能加数据库日志。日志的创建逻辑稳定但具体的日志目标经常变化用一个日志工厂统一管理业务代码只管调日志接口非常舒服。第二类是消息推送/通知系统也就是本文的贯穿案例。短信、邮件、站内信、企业微信、App推送每增加一个渠道加一个产品和工厂类旧代码几乎不动。第三类是存储层选择。某个数据访问组件需要支持SQLite、MySQL、Redis等多种存储后端通过工厂根据配置返回对应的存储访问对象业务层完全透明。相反如果出现以下情况我建议不要用工厂模式产品种类极少两三个且几乎不变此时工厂模式带来的类膨胀和维护成本大于收益或者你根本不确定未来会不会扩展可以先写简单的new等真出现第二个产品再重构成工厂模式。设计模式是为变化做准备的没有变化就没有必要提前设计。4. 工厂模式与设计原则、其他模式的关系串起来理解才牢固4.1 开闭原则工厂模式的价值核心《大话设计模式》里强调最多的原则就是开闭原则对扩展开放对修改关闭。简单说就是加新功能时尽量不改已有代码而是通过新增代码来实现。工厂模式是开闭原则的最佳实践案例。工厂方法模式下新增一种产品你新增了两个类原有的类一个都不用改。这就是“对扩展开放”。同时由于业务方只依赖抽象接口产品类的变化不会传导到上层这就是“对修改关闭”。要理解这个原则的真正分量你得吃过“改代码改出Bug”的亏。我在某跨平台消息推送项目里见过这种情况调用方代码里直接new了十几种具体对象某次新增渠道时有人为了省事用全局替换把某个new全换成了另一种结果把不需要换的地方也换了线上出了严重事故。如果当初用了工厂模式这次改动只是新增两个类完全不会碰已有代码。这就是开闭原则在实战中的意义它保护的是已有代码的稳定性。4.2 依赖倒置原则工厂模式的上层设计哲学依赖倒置原则说高层模块不应该依赖低层模块两者都应该依赖抽象抽象不应该依赖细节细节应该依赖抽象。这听起来绕口但工厂模式里的体现非常直接。业务模块如sendNotice函数是高层的具体产品SmsNotifier是低层的两者之间加了一个抽象接口Notifier业务模块只依赖Notifier。具体产品类也依赖Notifier接口实现了同样的接口约定。这样一来高层和低层都面向抽象编程互不直接依赖。从架构设计的角度看依赖倒置原则让系统的演进方向变得非常清晰变化隔离在抽象接口之下。只要接口稳定接口之上的所有代码都是稳定的。这也是为什么工厂模式常被视为架构设计的基础工具。4.3 工厂模式与单例、策略、模板方法的搭配工厂模式很少单独使用它经常和其他模式配合。我举几个常见的搭配工厂 单例工厂类本身不需要有多个实例完全可以做成单例。比如全局唯一的配置管理工厂进程内只需要一个。C里可以用静态局部变量的方式实现线程安全的单例。class ConfigFactory { public: static ConfigFactory instance() { static ConfigFactory factory; return factory; } // ... private: ConfigFactory() default; };工厂 策略工厂创建出来的产品对象内部可以使用策略模式。比如通知器里集成了多种发送策略即时发送、定时发送、批量发送通知器本身是一个策略的持有者或上下文。工厂负责把策略也组装好业务层拿到手就是完整可用的对象不用关心内部策略怎么拼接。工厂 模板方法在抽象工厂基类里可以用模板方法模式定义创建产品的固定流程比如“校验参数 - 创建新产品 - 注册到管理器 - 返回产品”。具体步骤中的“创建新产品”由子类实现其他步骤由基类统一完成。这种组合方式能减少子类的重复代码。这些搭配模式本质上都是让每个类只承担单一的职责类之间通过接口协作。设计模式不是孤立的知识点它们是同一套面向对象设计思想的不同切面。串起来理解比一个个孤立地背要深刻得多。5. 常见问题与排查技巧实录5.1 问题速查表问题现象原因解决方案析构不完整程序退出时出现内存泄漏报告基类析构函数不是虚函数给抽象产品基类加virtual ~Notifier() default;工厂创建失败传入非法类型标识时程序崩溃工厂方法没有处理未知类型分支在default分支抛异常或返回nullptr所有权混乱多个调用方共同持有同一产品指针释放两次使用裸指针且所有权不明确统一用unique_ptr表达唯一所有权类爆炸每新增一个产品要写两个类文件数量膨胀产品种类多且频繁变化评估是否用简单工厂或函数指针表简化切换产品失败换了工厂对象但业务表现没有变化业务代码内直接new了具体工厂改为依赖注入由外部传入抽象工厂5.2 排查思路先复现再定位遇到工厂模式相关的问题我的排查步骤一般是固定的。第一步先确认问题发生在创建阶段还是使用阶段。如果是创建阶段比如工厂抛异常那问题在工厂内部重点检查类型标识是否合法、参数是否正确、依赖的服务是否初始化。如果是使用阶段比如产品行为不对那问题在产品类内部跟工厂模式关系不大。第二步检查所有权边界。C里工厂模式出的问题十有八九跟内存管理有关。确认工厂返回的是unique_ptr、shared_ptr还是裸指针所有权转移的语义是否清晰。第三步检查是否存在循环依赖。工厂类依赖产品类产品类如果又依赖工厂类很容易形成循环依赖导致编译错误或设计混乱。解决办法通常是把抽象接口拆出去单独放一个头文件。5.3 独家避坑心得踩过几次坑之后我总结了几条工厂模式的专属经验第一工厂方法的参数越少越好。我见过有人为了“灵活性”给工厂的create方法设计了好几个参数结果调用方传参负担极重而且参数组合多了工厂内部的分支逻辑也复杂。大多数情况下工厂方法只需要一个参数甚至不需要参数。第二不要试图用一个工厂掌管所有产品。有些同学的简单工厂越写越大最后变成一个“万能工厂”所有类型都由它创建。一旦产品数量超过十个这个工厂类就成了维护噩梦。这时候应该考虑拆分成多个工厂或者升级为工厂方法模式。第三编译期多态优先于运行期多态。如果产品类型在编译期就能确定用模板技术完全可以替代工厂模式性能更好、代码更短。工厂模式真正适合的是运行期根据配置或输入动态决定产品类型的场景。我在某图像处理Demo里就用模板策略替代了工厂效果很好。具体用哪种方案取决于你的产品类型是编译期已知还是运行期动态决定的。第四产品类内部尽量保持轻量。工厂只负责创建对象不应该负责产品的业务功能。如果一个产品类里既包含业务逻辑又包含创建逻辑那它其实是“半工厂、半产品”的状态职责混乱维护起来非常难受。6. 最后的代码模板与个人心得这一章我整理了一个可以直接套用的三合一样板先用简单工厂快速落地等扩展点明确后再逐步重构成工厂方法或抽象工厂。很多刚学设计模式的人容易陷入“一步到位”的执念我的建议恰恰相反先跑起来再演进。我个人的体会是工厂模式是设计模式里少有的“学了就能用、用了就有效果”的模式。早期我写代码也不注意这些直到某跨平台消息推送项目的20多个调用点因为改动产品类型而连夜发版我才真正体会到“到处new”的代价。后来在项目里逐步引入工厂模式代码的维护成本肉眼可见地降了下来。最后再分享一个小技巧给工厂类起名字的时候尽量体现它生产的东西。比如NotifierFactory一看就知道是生产通知器的LoggerFactory一看就是生产日志器的。命名清晰项目里其他人用起来也顺手。设计模式的命名规范不是摆设它本身就是在传递设计意图。如果你正在自己的项目里实践工厂模式建议先找一个最常变化的产品类型入手把该类型的创建逻辑抽出来写成简单工厂跑通。跑通了之后你再评估是否需要引入工厂方法或抽象工厂。这样一步一步来比一口气把整个项目重构成满屏工厂要稳妥得多。