
1. 项目概述从“造家具”到理解抽象工厂最近在带新人做设计模式相关的项目复盘发现很多朋友对“抽象工厂模式”的理解还停留在“一个工厂生产多种产品”的层面总觉得它和工厂方法模式差不多只是复杂一点。这其实是个挺大的误区。正好我手头有一个几年前用C实现的“家具生产系统”的Demo当时是为了给团队做内部培训写的麻雀虽小五脏俱全。今天我就拿这个项目当例子掰开揉碎了讲讲抽象工厂模式到底解决了什么实际问题以及如何用C把它从UML图落地成可运行的代码。简单来说这个“家具生产系统”模拟的是一个家具厂商它需要生产不同风格比如现代风、古典风的成套家具。每一套家具都包含几个核心部件椅子、沙发、茶几。我们的目标是当客户选定一种风格后系统能自动生产出风格一致的一整套家具而不会出现一把现代风格的椅子配上一个古典风格的沙发这种“混搭”灾难。抽象工厂模式就是解决这类“产品族”创建问题的银弹。它不仅仅是“生产多种产品”更是确保一系列相关或依赖的对象被一起创建而无需指定它们的具体类。下面我们就从设计思路开始一步步拆解这个系统的实现。2. 核心思路与UML设计拆解在动手写代码之前搞清楚“为什么是抽象工厂”以及“如何设计”比直接敲键盘重要得多。我们先抛开C语法从问题本质和设计图入手。2.1 问题域分析为什么不用简单工厂或工厂方法很多新手的第一反应可能是给每种风格建一个工厂类不就行了比如ModernFactory生产所有现代家具ClassicalFactory生产所有古典家具。这其实就是抽象工厂的雏形。那我们为什么还需要“抽象”一层呢设想一下如果未来我们要增加一个“北欧风”系列。用简单工厂你可能需要在一个巨大的createFurniture函数里写一堆if-else或switch-case每增加一个风格就要修改这个核心函数违反了“开闭原则”。用工厂方法你需要为每一个产品椅子、沙发、茶几都创建一个抽象的创建方法和一系列具体工厂现代椅子工厂、古典椅子工厂...这会导致类爆炸且难以约束“成套”的概念。抽象工厂的核心优势在于将“产品族”的创建逻辑封装到一个独立的工厂接口中。客户端我们的主程序只依赖于这个抽象的工厂接口和抽象的产品接口。至于具体生产的是现代族还是古典族由运行时传入的具体工厂对象决定。这样增加新的产品族比如北欧风我们只需要新增一套具体产品类和对应的具体工厂类无需修改任何现有代码扩展性极好。2.2 UML类图深度解读下图清晰地展示了我们系统的静态结构理解了它代码就呼之欲出了。我们结合点、线、面来解读注此处用文字描述UML图关键部分实际项目中应绘制清晰图表“面”——抽象层稳定部分AbstractFactory抽象工厂 系统的核心接口。它声明了一组“创建方法”每个方法负责创建一种抽象产品。在我们的系统中就是createChair(),createSofa(),createCoffeeTable()。它定义了“能生产一套家具”的能力但不关心具体风格。AbstractChairAbstractSofaAbstractCoffeeTable抽象产品 分别代表椅子、沙发、茶几的抽象接口。它们声明了所有家具部件共有的方法比如sitOn(),lieOn(),putCup()。这些接口保证了无论什么风格客户端都能以统一的方式操作家具。“线”——关联与依赖AbstractFactory依赖AbstractProduct 这是最核心的依赖关系。抽象工厂的每个创建方法返回类型都是对应的抽象产品指针或智能指针。这意味着工厂承诺“生产出来的是椅子”但不承诺是“现代椅”还是“古典椅”。客户端依赖AbstractFactory和AbstractProduct 这是设计的关键成果。客户端代码里只有抽象工厂和抽象产品的指针完全不知道ModernFactory或VictorianChair的存在。这实现了“依赖倒置”。“点”——具体实现层可变部分ModernFactoryVictorianFactory具体工厂 它们实现了AbstractFactory接口。ModernFactory::createChair()返回一个new ModernChair()。每个具体工厂负责生产一整套风格一致的具体产品。ModernChair/VictorianChair,ModernSofa/VictorianSofa, ...具体产品 它们实现了对应的抽象产品接口并提供了风格特有的属性和行为。比如VictorianChair的sitOn()方法可能打印“坐在华丽的雕花木椅上”。关键设计原则体现开闭原则 系统对扩展开放新增北欧风对修改关闭不碰现有工厂和客户端代码。依赖倒置原则 高层模块客户端不依赖低层模块具体风格二者都依赖抽象。单一职责原则 每个具体工厂只负责创建同一族的产品每个具体产品只实现自身功能。注意 抽象工厂模式不适合“增加新产品类型”的场景。比如如果要在系统里新增一个“衣柜”产品那么就需要修改AbstractFactory接口所有具体工厂类都要跟着改。这是该模式的局限性在设计初期就要确定产品族的稳定性。3. C 实现详解从接口到具体类理论说透了我们上代码。我会用现代CC11/14的一些特性来写让代码更安全、更清晰。3.1 抽象产品与工厂接口定义首先定义最稳定的抽象层。这里我使用纯虚函数来定义接口并用std::unique_ptr管理资源避免裸指针和内存泄漏。// AbstractProduct.h #pragma once #include iostream #include memory // 抽象椅子接口 class AbstractChair { public: virtual ~AbstractChair() default; virtual void sitOn() const 0; virtual void hasLegs() const 0; // 一个可能的产品公共属性 }; // 抽象沙发接口 class AbstractSofa { public: virtual ~AbstractSofa() default; virtual void lieOn() const 0; virtual void fold() const 0; // 假设沙发可能有折叠功能 }; // 抽象茶几接口 class AbstractCoffeeTable { public: virtual ~AbstractCoffeeTable() default; virtual void putCup() const 0; virtual void getMaterial() const 0; // 材质属性 }; // 抽象工厂接口 class AbstractFactory { public: virtual ~AbstractFactory() default; // 使用智能指针明确所有权转移语义 virtual std::unique_ptrAbstractChair createChair() const 0; virtual std::unique_ptrAbstractSofa createSofa() const 0; virtual std::unique_ptrAbstractCoffeeTable createCoffeeTable() const 0; };关键点解析虚析构函数 基类必须有虚析构函数这样才能通过基类指针正确释放派生类对象的内存。 default让编译器生成默认实现简洁安全。纯虚函数 0将函数声明为纯虚函数使类成为抽象类无法实例化强制子类实现。std::unique_ptr 工厂方法返回智能指针。这意味着工厂将创建出的对象所有权转移给调用者调用者无需关心delete极大地避免了内存泄漏。这是现代C资源管理的核心实践。3.2 现代风格具体产品实现接下来实现现代风格这一族产品。它们需要实现所有抽象接口中声明的方法。// ModernFurniture.h #pragma once #include AbstractProduct.h class ModernChair : public AbstractChair { public: void sitOn() const override { std::cout 坐在简约舒适的现代风格椅子上。 std::endl; } void hasLegs() const override { std::cout 现代椅拥有纤细的金属腿。 std::endl; } }; class ModernSofa : public AbstractSofa { public: void lieOn() const override { std::cout 躺在低矮、线条流畅的现代沙发上。 std::endl; } void fold() const override { std::cout 现代沙发不支持折叠。 std::endl; } }; class ModernCoffeeTable : public AbstractCoffeeTable { public: void putCup() const override { std::cout 将杯子放在现代风格的玻璃茶几上。 std::endl; } void getMaterial() const override { std::cout 材质钢化玻璃与不锈钢。 std::endl; } };3.3 古典风格具体产品实现古典风格产品族实现同样的接口但行为不同。// VictorianFurniture.h #pragma once #include AbstractProduct.h class VictorianChair : public AbstractChair { public: void sitOn() const override { std::cout 坐在华丽雕花、软垫厚实的维多利亚风格高背椅上。 std::endl; } void hasLegs() const override { std::cout 古典椅拥有复杂雕刻的木制椅腿。 std::endl; } }; class VictorianSofa : public AbstractSofa { public: void lieOn() const override { std::cout 躺在宽敞、带有天鹅绒衬垫的古典沙发上。 std::endl; } void fold() const override { std::cout 古典沙发为固定结构无法折叠。 std::endl; } }; class VictorianCoffeeTable : public AbstractCoffeeTable { public: void putCup() const override { std::cout 将陶瓷茶杯放在厚重的实木雕花茶几上。 std::endl; } void getMaterial() const override { std::cout 材质桃花心木与黄铜镶嵌。 std::endl; } };实操心得在具体产品类的实现中override关键字是C11的好东西。它明确告诉编译器和读代码的人这个函数是重写基类的虚函数。如果拼写错误或函数签名不匹配编译器会报错能及早发现错误。3.4 具体工厂的实现具体工厂类的实现非常规整它们就是“组装”同一族产品的站点。// ConcreteFactories.h #pragma once #include AbstractProduct.h #include ModernFurniture.h #include VictorianFurniture.h class ModernFactory : public AbstractFactory { public: std::unique_ptrAbstractChair createChair() const override { return std::make_uniqueModernChair(); // 使用 make_unique更安全 } std::unique_ptrAbstractSofa createSofa() const override { return std::make_uniqueModernSofa(); } std::unique_ptrAbstractCoffeeTable createCoffeeTable() const override { return std::make_uniqueModernCoffeeTable(); } }; class VictorianFactory : public AbstractFactory { public: std::unique_ptrAbstractChair createChair() const override { return std::make_uniqueVictorianChair(); } std::unique_ptrAbstractSofa createSofa() const override { return std::make_uniqueVictorianSofa(); } std::unique_ptrAbstractCoffeeTable createCoffeeTable() const override { return std::make_uniqueVictorianCoffeeTable(); } };关键点解析std::make_unique C14提供的工厂函数用于创建std::unique_ptr。它比直接new更安全因为能避免内存泄漏例如在异常发生时。代码中也更简洁。工厂的单一职责ModernFactory的每个方法都返回现代风格的产品。它内部对“现代风格”这一族产品的具体类了如指掌但对客户端隐藏了这些细节。4. 客户端代码与系统组装客户端代码是模式威力的最佳体现。它完全面向接口编程与具体类解耦。// Client.cpp #include AbstractProduct.h #include ConcreteFactories.h #include memory // 客户端函数接收一个抽象工厂生产并使用一套家具 void createAndUseFurniture(const AbstractFactory factory) { std::cout \n 开始生产一套家具 std::endl; // 客户端只通过抽象接口操作 auto chair factory.createChair(); auto sofa factory.createSofa(); auto table factory.createCoffeeTable(); std::cout \n--- 展示家具功能 --- std::endl; chair-sitOn(); chair-hasLegs(); std::cout std::endl; sofa-lieOn(); sofa-fold(); std::cout std::endl; table-putCup(); table-getMaterial(); std::cout 一套家具展示完毕 \n std::endl; // unique_ptr 离开作用域自动释放内存无需手动delete } int main() { std::cout 抽象工厂模式演示家具生产系统\n std::endl; // 创建现代风格工厂 ModernFactory modernFactory; std::cout 【订单1现代风格客厅】 std::endl; createAndUseFurniture(modernFactory); // 传入具体工厂对象 // 创建古典风格工厂 VictorianFactory victorianFactory; std::cout 【订单2古典风格书房】 std::endl; createAndUseFurniture(victorianFactory); // 传入另一个具体工厂对象 // 想象一下未来新增北欧风格 // 1. 定义 NordicChair, NordicSofa, NordicCoffeeTable // 2. 定义 NordicFactory 继承 AbstractFactory // 3. 在main中创建 NordicFactory 并传入 createAndUseFurniture // 完毕原有代码一行都不用改。 return 0; }编译与运行以Linux/macOS的g为例g -stdc14 -o furniture_system Client.cpp ./furniture_system预期输出抽象工厂模式演示家具生产系统 【订单1现代风格客厅】 开始生产一套家具 --- 展示家具功能 --- 坐在简约舒适的现代风格椅子上。 现代椅拥有纤细的金属腿。 躺在低矮、线条流畅的现代沙发上。 现代沙发不支持折叠。 将杯子放在现代风格的玻璃茶几上。 材质钢化玻璃与不锈钢。 一套家具展示完毕 【订单2古典风格书房】 开始生产一套家具 --- 展示家具功能 --- 坐在华丽雕花、软垫厚实的维多利亚风格高背椅上。 古典椅拥有复杂雕刻的木制椅腿。 躺在宽敞、带有天鹅绒衬垫的古典沙发上。 古典沙发为固定结构无法折叠。 将陶瓷茶杯放在厚重的实木雕花茶几上。 材质桃花心木与黄铜镶嵌。 一套家具展示完毕 5. 模式深度探讨与实战技巧实现完了我们来聊聊更深层的东西和实际项目中容易踩的坑。5.1 抽象工厂 vs. 工厂方法本质区别再辨析很多人分不清这两个模式。用我们这个项目类比工厂方法模式 如果我们的需求是“生产一种家具”比如专门生产各种风格的椅子。我们会有一个ChairFactory抽象类下面有ModernChairFactory,VictorianChairFactory。每个工厂只生产一种产品椅子。它的关注点是单一产品的多态创建。抽象工厂模式 我们的需求是“生产一套风格一致的家具”。ModernFactory这个类内部使用了类似工厂方法的思想createChair,createSofa但它的核心目标是创建一组相关的产品产品族。抽象工厂通常是由多个工厂方法组成的。简单记工厂方法是“一对一”一个创建者对一个产品抽象工厂是“一对多”一个创建者对一族产品。5.2 C实现中的内存管理精要我们用了std::unique_ptr这是现代C的推荐做法。但你需要理解其所有权语义工厂函数返回std::unique_ptr 意味着“我将这个对象的所有权移交给你调用者你负责它的生命周期”。调用者拿到后可以移动(std::move)它但无法复制。当unique_ptr离开作用域对象自动销毁。为什么不直接用new返回裸指针 容易导致内存泄漏。如果客户端忘记delete或者在使用过程中发生异常资源就无法释放。智能指针利用RAII资源获取即初始化机制将资源管理绑定到对象生命周期从根本上解决了这个问题。如果需要共享所有权怎么办 比如某件家具需要被多个“房间”对象引用。可以考虑使用std::shared_ptr。这时抽象工厂接口的返回类型也要相应改为std::shared_ptr。但需谨慎使用避免循环引用导致内存无法释放。5.3 扩展新产品族的完整流程假设业务需要增加“北欧风”创建新产品类NordicChair,NordicSofa,NordicCoffeeTable均继承自对应的抽象产品类并实现虚函数。创建新工厂类NordicFactory继承自AbstractFactory在其三个创建方法中分别返回std::make_uniqueNordicChair()等。在客户端使用 在main函数中实例化NordicFactory并传给createAndUseFurniture函数。关键 在整个过程中AbstractFactory、AbstractProduct以及最重要的客户端函数createAndUseFurniture都无需做任何修改。这就是符合“开闭原则”的优雅扩展。5.4 常见问题与排查技巧实录在实际项目中应用抽象工厂模式可能会遇到以下典型问题问题1 编译错误 “invalid new-expression of abstract class type”现象 在具体工厂的createXXX函数中return new ConcreteProduct();时报错。排查 这几乎总是因为ConcreteProduct类没有实现基类AbstractProduct的所有纯虚函数。检查你的具体产品类是否每个声明为0的虚函数都有对应的override实现。使用override关键字可以帮助编译器提前发现这类错误。问题2 运行时多态失效总是调用基类函数现象 通过工厂创建了对象但调用虚函数时执行的不是子类的实现。排查确保基类的析构函数是virtual的。确保你是通过基类指针或引用来调用虚函数。如果你将对象按值传递或存储会发生“对象切片”多态会失效。检查你的具体产品类函数声明是否正确使用了override确保签名完全匹配。问题3 增加新产品类型如新增“衣柜”非常困难现象 这是抽象工厂模式的结构性局限不是bug。应对设计初期评估 在架构设计时就要评估产品族的稳定性。如果产品类型椅子、沙发、茶几很可能变化那么抽象工厂可能不是最佳选择。使用其他模式组合 可以考虑结合原型模式Prototype或者依赖注入容器来获得更大的灵活性。但复杂度也会增加。问题4 代码中充斥着大量的具体工厂和产品类现象 当产品族和产品类型都很多时类的数量会乘积式增长。优化技巧使用宏或代码生成 在风格固定、只是产品类型多的场景可以考虑用元编程技术减少重复代码但会降低可读性。反思设计 是否过度设计如果每个产品族的创建逻辑非常简单就是new一下也许用简单的“工厂方法配置文件”会更简洁。抽象工厂的威力在于每个产品族的创建逻辑可能很复杂如果逻辑简单其优势就不明显。6. 项目总结与模式选用思考回过头看这个“家具生产系统”它完美诠释了抽象工厂模式的应用场景系统需要独立于其产品的创建、组合和表示方式系统需要配置多个产品族中的一个需要强调一系列相关的产品对象设计以便进行联合使用。在C实现中我们通过纯虚接口、智能指针和override关键字构建了一个类型安全、资源管理清晰、扩展性良好的系统。createAndUseFurniture函数是最高层次的抽象它只知道“需要一套家具”至于这套家具是现代的、古典的还是未来的完全由传入的工厂对象决定。最后分享一个我个人的选型心得不要为了用模式而用模式。当你发现你的代码中创建对象时有一系列 if-else 来判断“风格”、“主题”、“平台”等并且这些判断散落在各处每次新增一个选项都要修改很多处时就该考虑抽象工厂了。它的引入会增加类的数量但换来的是客户端代码的极度简洁和后续维护的轻松。在架构评审时如果被问到“这里为什么用抽象工厂”你的回答应该是“为了将产品族的创建逻辑与使用逻辑解耦以支持未来可能增加的新的产品族符合开闭原则。” 这才是对模式价值的真正理解。