
1. 从“new一个对象”说起我们为什么需要工厂方法做C开发的朋友大概都有过这样的经历代码写得好好的突然产品过来说要加一个新类型于是你满世界找switch找到一处加一个case改完编译、提测、上线风平浪静。直到下一次再加类型你又在同一个switch里再加一个分支。改了几轮之后那个函数越来越臃肿每次动它都提心吊胆生怕影响别的逻辑。这其实就是典型的“对象创建”耦合问题。调用方不仅依赖抽象接口还直接依赖具体类型导致任何新增类型的变化都会沿着调用链传导。面向对象设计里有一条非常重要的原则叫“依赖倒置原则”——高层模块不应该依赖低层模块二者都应该依赖抽象。而工厂方法模式解决的就是这一类问题把对象的创建和使用解耦让调用方只面对抽象接口把“到底创建哪个具体对象”的决策交给子类去完成。一句话概括工厂方法定义一个用于创建对象的接口让子类决定实例化哪一个类。这跟很多人熟悉的“简单工厂”不一样简单工厂是把创建逻辑集中在一个静态函数里用参数判断返回哪个对象工厂方法则是把创建逻辑下沉到子类每个子类负责创建一种产品调用方不需要知道具体类名甚至不需要知道产品是怎么组装出来的。这篇内容适合谁正在学设计模式但觉得概念太虚的C开发者工作里写了大量new但没梳理过创建逻辑的朋友以及想重构老代码却不知道怎么下手的同学。后面我会把 UML 结构、C代码实现、适用场景和踩过的坑都掰开讲清楚。2. 工厂方法的核心思想创建逻辑为什么不直接写在构造函数旁边工厂方法听起来高大上实际上它的出发点特别朴素把“用什么类”和“怎么用”拆开。2.1 从紧耦合到松耦合一个订单系统的演化过程假设你在做一个电商订单系统一开始只支持支付宝支付class Payment { public: virtual void pay(double amount) 0; virtual ~Payment() default; }; class AliPay : public Payment { public: void pay(double amount) override { std::cout 支付宝支付: amount 元 std::endl; } };订单处理逻辑里直接创建对象void processOrder(Order order, double amount) { // 处理订单其他逻辑... AliPay payment; // 直接 new 具体类型 payment.pay(amount); }这个代码的问题在哪里如果你要加微信支付直接改动processOrdervoid processOrder(Order order, double amount) { // 处理订单其他逻辑... if (order.payType wechat) { WeChatPay payment; payment.pay(amount); } else if (order.payType alipay) { AliPay payment; payment.pay(amount); } }看起来也没多复杂对吧可是当支付方式从2种变成5种当你需要在多个地方下单、退款、对账创建支付对象时每个调用点都要维护一份if-else。新增一种支付方式就要把所有调用点翻一遍。改漏了一处线上就出 bug。工厂方法解决这个问题的思路是不直接在业务代码里new具体对象而是通过一个工厂接口来创建。业务代码只依赖Payment抽象和PaymentFactory抽象至于创建的是哪种支付由具体的工厂子类决定。2.2 核心角色一眼看懂工厂方法模式一共有四个角色抽象产品Product定义产品的公共接口比如上面的Payment。具体产品Concrete Product实现抽象产品接口的具体类比如AliPay、WeChatPay。抽象工厂Creator声明工厂方法返回抽象产品类型。注意这个类不一定要完全抽象它也可以包含一些公共的业务逻辑。具体工厂Concrete Creator重写工厂方法返回具体的产品实例。这种结构的核心价值是你把“创建什么对象”的决策从调用方代码里彻底剥离出去了。调用方拿到的永远是一个Payment指针/引用它不需要关心这个指针背后是支付宝还是微信。哪天新增一个“银行卡支付”不需要碰任何业务代码只需要新增一个产品类和一个对应的工厂类。2.3 为什么不能用一个全局函数搞定对比简单工厂很多人会问我写一个全局函数根据字符串参数返回不同的产品对象效果不是一样吗std::unique_ptrPayment createPayment(const std::string type) { if (type alipay) return std::make_uniqueAliPay(); if (type wechat) return std::make_uniqueWeChatPay(); return nullptr; }这就是简单工厂。代码量更少理解起来更直观。那工厂方法优越在哪里答案是开闭原则的遵守程度。简单工厂的createPayment函数本身是“不封闭”的——每新增一个产品你必须修改这个函数。而工厂方法模式下新增产品不需要修改已有代码只需要新增类并注册到对应的工厂映射中原有的代码保持不动。换句话说简单工厂把“变化”集中到一个地方确实比散落在业务代码里好但它仍然是一个“可变的点”。工厂方法把“变化”转移到了“新增类”这个动作上类都加了工厂也必然要加但已有的类、已有的函数、已有的调用关系保持稳定。这在大型项目中意味着更少的回归风险。这里要补充一点如果你只是在一个小模块或者一个 Demo 里使用简单工厂完全够用。设计模式不是越多越好选择的原则是“复杂度与变化频率匹配”。工厂方法的开销是增加类和继承层级如果产品类型几乎不变用简单工厂或直接创建反而更省心。3. C实现详解从 UML 结构到现代 C 写法3.1 经典实现基类指针 虚函数我先给出一个最传统的 C 实现方便你对照 UML 理解结构。完整代码示例如下#include iostream #include memory // 抽象产品 class Payment { public: virtual void pay(double amount) 0; virtual ~Payment() default; }; // 具体产品支付宝 class AliPay : public Payment { public: void pay(double amount) override { std::cout 使用支付宝支付: amount 元 std::endl; } }; // 具体产品微信支付 class WeChatPay : public Payment { public: void pay(double amount) override { std::cout 使用微信支付: amount 元 std::endl; } }; // 抽象工厂 class PaymentFactory { public: virtual std::unique_ptrPayment createPayment() 0; virtual ~PaymentFactory() default; }; // 具体工厂创建支付宝 class AliPayFactory : public PaymentFactory { public: std::unique_ptrPayment createPayment() override { return std::make_uniqueAliPay(); } }; // 具体工厂创建微信支付 class WeChatPayFactory : public PaymentFactory { public: std::unique_ptrPayment createPayment() override { return std::make_uniqueWeChatPay(); } };这里有几个实现细节值得注意。第一工厂方法的返回值我用了std::unique_ptrPayment而不是裸指针。这是现代 C 的基本素养——用智能指针管理生命周期避免手动delete遗漏导致内存泄漏。如果你用的编译器不支持 C14 的std::make_unique可以用std::unique_ptrT(new T(...))替代甚至升级一下编译器更现实。第二抽象产品Payment的析构函数必须是virtual。这个坑我见过太多次了——有人写基类时忘了加虚析构结果通过基类指针 delete 派生类对象时只调用了基类析构派生类资源泄漏。在 C 里只要你的类有虚函数就把析构函数也声明为 virtual没得商量。第三工厂方法的返回类型是抽象产品但创建的是具体产品。调用方永远通过Payment接口操作不感知具体类型。3.2 使用方式调用方只和工厂打交道有了上面的工厂类业务代码可以这样写void processOrder(PaymentFactory factory, double amount) { auto payment factory.createPayment(); payment-pay(amount); } int main() { AliPayFactory aliFactory; processOrder(aliFactory, 99.5); WeChatPayFactory wechatFactory; processOrder(wechatFactory, 20.0); }看到了吗processOrder函数完全不关心传入的工厂具体是哪个它只知道调用createPayment()能拿到一个Payment。新增支付方式时processOrder一行都不用改。如果业务代码里存在一个多态的使用场景你还可以进一步组合用一个容器管理所有工厂按配置动态选择。int main() { std::unordered_mapstd::string, std::unique_ptrPaymentFactory factories; factories[alipay] std::make_uniqueAliPayFactory(); factories[wechat] std::make_uniqueWeChatPayFactory(); std::string payType wechat; auto factory *factories[payType]; auto payment factory.createPayment(); payment-pay(88.8); }这个写法在工程里很常见工厂和产品都用unordered_map按名称注册配置驱动选择代码结构干净直接。新增类型时只需往 map 里增加一项原有代码零改动。3.3 模板化改造减少重复工厂类上面的代码有个恼人的问题每新增一个产品就要写一个对应的工厂类。产品一多工厂类也跟着膨胀一堆只有三五行的类看着很啰嗦。有没有办法减少这种重复当然有。C 的模板在这里可以帮你一把template typename ConcreteProduct class ConcretePaymentFactory : public PaymentFactory { public: std::unique_ptrPayment createPayment() override { return std::make_uniqueConcreteProduct(); } };然后注册时直接指定模板参数factories[alipay] std::make_uniqueConcretePaymentFactoryAliPay(); factories[wechat] std::make_uniqueConcretePaymentFactoryWeChatPay();这样你就不需要为每个具体产品单独写工厂类了。新增BankCardPay时只需要定义产品类然后注册时多一行factories[bankcard] std::make_uniqueConcretePaymentFactoryBankCardPay()。代码重复率大幅下降结构更清晰。这个写法算是对经典工厂方法的一个“C 特有的简化”。注意它的适用前提是——所有具体产品的构造函数参数一致这里都是无参构造。如果你的产品有不同的初始化参数就得回到手动写工厂类的方案或者用其他方式统一构造签名。3.4 现代 C 的进一步演进lambda 工厂如果你的项目里已经有std::function和 lambda 的广泛使用工厂方法还可以有更轻量级的表达方式——直接用函数对象代替工厂类#include functional #include unordered_map // 定义一个“创建函数”类型 using PaymentCreator std::functionstd::unique_ptrPayment(); // 用法注册创建逻辑 std::unordered_mapstd::string, PaymentCreator creators; creators[alipay] []() { return std::make_uniqueAliPay(); }; creators[wechat] []() { return std::make_uniqueWeChatPay(); };这么写的本质仍然是工厂方法的思想——创建逻辑与使用逻辑分离但载体从“类”变成了“函数对象”。好处是免去了大量的类定义坏处是没有类的语义层级调试时看不到“工厂”这个抽象层。这两种方式各有取舍产品种类多、希望扩充时有强制约束、团队需要明确的设计边界 —— 用传统工厂类。模块内部使用、不希望引入太多类型、需要灵活组合创建逻辑 —— 用std::function版本。我个人在工程中会优先用带模板的工厂类版本因为它在“类层次清晰”和“代码量精简”之间取得了一个不错的平衡。4. 实战选型什么时候该用工厂方法什么时候别凑热闹模式没有绝对的好坏只有合用不合适。很多初学者的问题是“学了锤子看什么都像钉子”到处套设计模式。这里我结合实际经验把工厂方法的适用边界讲清楚。4.1 适合使用的典型场景场景一产品种类足够多且新增类型的频率较高。比如对接多家云存储服务本地存储、阿里云 OSS、腾讯云 COS你没法预测明年公司还会接入哪家。这时候用工厂方法新增供应商时只需要添加产品和工厂或注册 lambda业务代码完全不用动这体验是很爽的。场景二创建过程比较复杂需要组装、校验、设置默认值。比如创建一个“订单”对象它内部要生成订单号、初始化状态、设置时间戳、挂载明细列表。如果把这一堆逻辑散落在业务代码里每次创建订单都是一大坨。放到工厂方法里由专门的方法负责“组装一个合法可用的订单对象”业务侧只要求“给我一个订单”干净利落。场景三产品族之间存在替换关系需要整体切换。比如测试环境和生产环境使用不同的后端服务实现。通过工厂方法运行环境配置决定注入哪个具体工厂代码其他的部分不需要感知差异。4.2 不适合或者没必要用的场景场景一只有一个产品实现。如果这个系统永远只有一个Payment实现你加一个工厂等于多了一层永远只有一条路径的间接层。没有变化的可能就没有抽象的必要。场景二可以在编译期确定类型。如果产品类型在编译时就能确定完全可以用模板、用if constexpr来静态分发。工厂方法是一种运行时的多态机制运行时灵活性的代价是虚函数调用开销和代码的间接性。编译期能解决的事情不要拖到运行期。场景三产品对象的构造极其简单。比如就是一个简单的 DTO数据传输对象几个字段填完拉到。这时候直接MyStruct{...}聚合初始化就行用工厂方法属于杀鸡用牛刀。判断的标准很简单问自己未来这个“产品”类型会不会变在哪里变变的时候你希望哪个层面的代码受影响如果答案是“不影响调用方”很重要那工厂方法值得考虑如果答案是无所谓那别折腾。4.3 与其他创建型模式的取舍对比工厂方法、抽象工厂、建造者这三个模式都是和“对象创建”有关的但职责定位有差别容易混淆我整理了个对照表模式核心意图典型场景关注点简单工厂集中创建逻辑按参数返回实例类型不多、稳定的小模块消除重复创建代码工厂方法让子类决定创建哪种产品产品种类多、类型变化频繁创建与使用解耦可扩展抽象工厂创建一组相关或相互依赖的对象系统需要一套产品家族产品族的整体替换和一致性建造者分步骤构造复杂对象对象的组装过程复杂把“怎么构造”和“是什么”分离其中抽象工厂与工厂方法最容易被混淆。记住一个原则工厂方法针对的是“一种产品”的创建抽象工厂针对的是“一族产品”的创建。工厂方法通过继承来扩展产品线抽象工厂通过组合来搭配产品族。实际工程中抽象工厂内部经常会用到多个工厂方法来实现具体产品的创建两者是不同层级的东西。5. 代码落地的工程经验这些坑我替你踩过理论讲完了真正写代码的时候还有一堆细节问题。这一节我把实际工程中高频出现的“坑”和应对方案整理出来都是常规教程不会写的东西。5.1 内存管理返回智能指针而不是裸指针在你把创建逻辑集中到工厂函数时返回类型的选择很关键。我见过老代码里工厂方法返回裸指针Payment* createPayment();然后调用方用完手动delete。这种写法最大的问题不是“每次都要记得 delete”而是当代码规模上去之后某些异常路径下delete会被跳过——一个异常抛出来指针丢了内存泄漏。更危险的是如果同一个裸指针被多个地方持有有人delete了其他人还在用直接悬挂指针。所以我一律建议返回std::unique_ptr。如果你需要共享所有权再转成std::shared_ptr。唯一的例外是你明知这个对象的生命周期比调用方更长比如全局单例但那种场景实际上不需要工厂来创建直接用单例模式更合适。5.2 工厂的注册与配置把逻辑从 if-else 中解脱初学者拿到工厂方法最容易写成这样的版本// 抽象工厂 class PaymentFactory { public: virtual std::unique_ptrPayment createPayment(const std::string type) { if (type alipay) return std::make_uniqueAliPay(); if (type wechat) return std::make_uniqueWeChatPay(); return nullptr; } };这是把简单工厂放到了基类里完全违背了工厂方法的初衷。工厂方法的精神是每个具体工厂负责一种产品而不是一个工厂用if-else分发。工程上更舒服的做法是“注册表模式”每个具体工厂自己注册到统一的管理容器里按名称创建。// 伪代码示意 class FactoryRegistry { public: void registerFactory(const std::string name, std::unique_ptrPaymentFactory factory) { factories_[name] std::move(factory); } std::unique_ptrPayment create(const std::string name) { auto it factories_.find(name); if (it factories_.end()) { throw std::runtime_error(Unknown payment type: name); } return it-second-createPayment(); } private: std::unordered_mapstd::string, std::unique_ptrPaymentFactory factories_; };这个做法的好处是create方法里完全没有if-else逻辑路径非常清晰。新增产品类型时你甚至可以通过配置文件、插件机制来注册不改任何已有的 C 源代码。5.3 构造失败与异常安全工厂方法里创建对象时具体产品的构造函数可能抛异常。这本身没问题——make_unique在构造失败时会自动清理异常会向上传播。但你需要注意工厂本身的线程安全性。如果工厂对象在多个线程中被同时调用createPayment()你需要在工厂内部做好同步。通常情况下工厂类本身是无状态的不持有可变成员变量所以是线程安全的。一旦你在工厂类里加了缓存、计数器之类的状态就必须考虑加锁或原子操作。5.4 不要过度设计接口数量最小化有个很容易犯的“产品经理式膨胀”错误为了“以后可能用到”把工厂接口做得特别复杂。比如createPayment()还附带createPaymentWithDiscount()、createPaymentWithCoupon()等方法。这种设计会导致每次新增产品时所有子类都要跟着实现一堆其实没人在用的接口。克制一点。工厂方法的接口应该只做一件事创建一个产品。产品的复杂行为通过产品自身的接口去暴露而不是在工厂接口上横向堆叠方法。你实在需要复杂创建过程用建造者模式去扩展不要硬塞进工厂方法。5.5 调试技巧给工厂类加一点可观测性实际运行中如果出现“介质类型不对”“创建出来的对象行为异常”定位问题时的第一反应是什么大概率要确认创建函数到底走了哪条路径。如果工厂实现全是一行return std::make_unique...()你是没法下断点观察类型的。常见的做法是加一个简单的日志或诊断字段class ConcretePaymentFactory : public PaymentFactory { public: std::unique_ptrPayment createPayment() override { // 打印创建过程方便排查问题 return std::make_uniqueConcreteProduct(); } };在 debug 构建下可以在工厂方法入口打印__PRETTY_FUNCTION__快速确认当前执行的是哪个工厂。这个方法成本极低排查问题时的收益很高。6. 工厂方法与相关模式的组合应用现实项目从来不会只用单一模式。工厂方法经常和其他模式搭配使用这里说两个我实际用过、配合效果很好的组合。6.1 工厂方法 单例模式被复用工厂的优雅实现如果某个工厂在整个进程里只需要一个实例你可以在工厂类内部实现静态实例class AliPayFactory : public PaymentFactory { public: static AliPayFactory instance() { static AliPayFactory inst; return inst; } std::unique_ptrPayment createPayment() override { return std::make_uniqueAliPay(); } private: AliPayFactory() default; };需要创建支付宝对象时通过AliPayFactory::instance().createPayment()。这种写法避免了工厂对象的重复创建也保证了全局唯一性。缺点是这个单例工厂是一个具体类型调用方仍然间接依赖了它。折中的方案是注册表模式里用单例工厂做注册业务侧只依赖注册表接口。6.2 工厂方法 策略模式让创建逻辑可替换工厂方法决定“创建哪个产品”策略模式决定“用什么算法处理业务”。两者经常在同一个系统里协作。举个例子订单处理系统里支付的执行逻辑走余额、走优惠券叠加、走外部支付就是一个策略层的选择。你可以用工厂方法创建出支付对象再用策略对象定义这个支付对象以什么方式执行。创建与执行解耦替换任何一环都不影响另一环。class PaymentExecutor { public: virtual void execute(Payment payment, double amount) 0; }; class PrepaidExecutor : public PaymentExecutor { public: void execute(Payment payment, double amount) override { std::cout 前置充值校验... std::endl; payment.pay(amount); } };订单模块同时依赖PaymentFactory和PaymentExecutor新的支付方式接入时策略层完全不用改新的执行逻辑加入时工厂层完全不用动。两个维度的变化被隔离开来代码的演化自然多了。6.3 工厂方法 注册表 配置系统完整闭环如果项目规模再大一点我通常会把这几个东西组合成一个相对完整的配置驱动创建系统配置系统存放“支付类型”和“工厂注册名”的映射关系工厂注册表根据配置选中的工厂动态创建产品产品创建完成后交给策略层执行业务逻辑。整个过程中业务代码里看不见任何一个具体产品类的名字。新产品接入时你只需要在代码里新增产品类和工厂注册项并在配置文件中加一行映射。这种“改配置即上线”的效果对于需要频繁扩展的模块来说非常舒服。7. 实际项目重构案例从 if-else 到工厂方法的完整改造理论说了这么多拿一个我真实经历过的场景来完整复盘。某公司在做一个内部工单系统支持多种通知渠道邮件、短信、站内信。最初代码大概长这样void notifyUser(const std::string channel, const std::string message) { if (channel email) { EmailSender sender; sender.send(message); } else if (channel sms) { SmsSender sender; sender.send(message); } else if (channel inbox) { InboxSender sender; sender.send(message); } }新增渠道时改动集中在notifyUser里。到第五个渠道的时候这个函数已经快 60 行了测试也覆盖不全。重构分三步走。第一步定义抽象产品和具体产品class Notifier { public: virtual void send(const std::string message) 0; virtual ~Notifier() default; }; class EmailNotifier : public Notifier { public: void send(const std::string message) override { std::cout Email: message std::endl; } }; class SmsNotifier : public Notifier { public: void send(const std::string message) override { std::cout SMS: message std::endl; } }; class InboxNotifier : public Notifier { public: void send(const std::string message) override { std::cout Inbox: message std::endl; } };第二步定义抽象工厂和模板化具体工厂class NotifierFactory { public: virtual std::unique_ptrNotifier createNotifier() 0; virtual ~NotifierFactory() default; }; template typename ConcreteNotifier class ConcreteNotifierFactory : public NotifierFactory { public: std::unique_ptrNotifier createNotifier() override { return std::make_uniqueConcreteNotifier(); } };第三步业务侧通过注册表创建class NotificationService { public: NotificationService() { registerChannel(email, std::make_uniqueConcreteNotifierFactoryEmailNotifier()); registerChannel(sms, std::make_uniqueConcreteNotifierFactorySmsNotifier()); registerChannel(inbox, std::make_uniqueConcreteNotifierFactoryInboxNotifier()); } void notify(const std::string channel, const std::string message) { auto it factories_.find(channel); if (it factories_.end()) { throw std::runtime_error(Unsupported channel: channel); } auto notifier it-second-createNotifier(); notifier-send(message); } private: std::unordered_mapstd::string, std::unique_ptrNotifierFactory factories_; };重构后新增一个渠道只是“新增一个 Notifier 类 注册一行代码”的事NotificationService::notify本身不再变化。代码可读性好了测试也更聚焦——产品类和工厂可以分别做单元测试注册表的行为也能单独验证。关于重构一个过来人的建议是不要一步到位。先把分散在各处的new调用收敛到简单工厂里验证行为一致后再逐步演进到工厂方法。一次性把抽象层铺太满测试和 review 的负担都会变大反而容易中途放弃。8. 常见问题排查与避坑速查最后把一些高频问题和判断标准整理成速查表方便你写代码时对照自查。问题原因分析解决方案工厂返回的对象类型无法正确释放基类析构函数没有声明为 virtual基类析构函数改写成virtual ~Payment() default;工厂方法里出现 if-else 分发把简单工厂混进了工厂方法一个具体工厂只负责一种产品分发逻辑交给注册表调用方仍然依赖具体产品类工厂返回类型写成具体类型工厂方法的返回类型必须是抽象产品接口新增产品时需要改已有代码违背了开闭原则检查是否所有变化都通过新增类完成已有工厂和业务代码保持不动异常路径导致内存泄漏返回裸指针且异常分支未 delete统一返回std::unique_ptr产品构造函数需要不同参数模板工厂无法统一创建手动实现具体工厂类在工厂内部完成差异化的构造逻辑工厂被多个线程并发调用工厂内部有可变共享状态保证工厂无状态有状态时加锁或用atomic保护工厂类数量膨胀、代码冗余每种产品都手动写了工厂类用模板化工厂template typename T压缩代码量再补充几个判断时机的问题自测把这个模块里所有new语句删掉业务代码还能保持功能不变吗新增一个产品类型已有的类和函数需要修改多少处创建逻辑里有没有超过 3 行的“组装”代码在多个地方重复出现如果第二问的答案是多于 0 处说明创建与使用的耦合还没有彻底分开。如果第三问是经常重复出现说明确实需要一个专门的创建层。反之如果三个问题的答案都很轻那你目前的代码大概率不需要引入工厂方法。9. 写在最后的一些个人体会设计模式这东西你光看书很容易陷入“记住了例子、换场景就不会用了”的尴尬境地。我的经验是每次看到模式先别急着背定义而是思考一个问题这个模式到底在对抗什么变化工厂方法对抗的是“产品类型变化”带来的扩散性修改。理解了这一点你就知道它不是一招鲜而是有明确的适用边界的。还有一点想特别提醒C 这门语言比 Java 或者 C# 更灵活同一件事可以用模板、函数指针、虚函数等多种方式实现。同样的工厂思想在不同项目里落地形态可能完全不同。不要拘泥于教科书上的类图抓住“创建与使用解耦”这个本质然后根据项目的实际复杂度选择最合适的载体。我在实际项目里最常用的是注册表加模板工厂的组合——结构清晰、扩展容易、代码量也不算多。但如果你所在的模块很小、变化也少直接用简单工厂反而更实用。模式是为需求服务的不要本末倒置。最后分享一个小技巧遇到一个叫不准的设计决策先把模式画出来把参与者的职责写清楚然后问自己“这个抽象在 6 个月后还合理吗”如果连自己都说服不了那大概率是想多了。设计模式是用来解决问题的不是用来制造问题的。工厂方法尤其如此——它应该让代码演化变得更轻松而不是让你多维护一层没什么用的工厂类。