C++抽象工厂与单件模式实践:多数据库切换的工程化实现 简介面向软件构造课程学习者与设计模式初学者的实验报告资源主题为抽象工厂模式与单件模式的编程实现。报告基于 2019 年软件构造课程真实实验场景以公司数据库操作为例设计支持 MySQL 和 SQL Server 两种数据库的用户、部门、项目增删查改模块完整给出 IFactory 抽象工厂、SqlserverFactory、MysqlFactory 具体工厂以及 SqlserverUser、MysqlUser 等具体实现类的 C 代码。报告还详细解释了抽象工厂如何组织多产品族、单件模式如何保证工厂唯一实例并配有 UML 图与实验结论便于读者对照理解设计模式的落地方式。资源为单个 PDF 文件整体大小 301KB简洁便携目前已有 122 人下载学习适合正在复习设计模式、撰写实验报告或设计数据访问层的学习者参考。通过阅读可熟悉抽象工厂与单件模式的关键代码结构掌握按数据库类型动态切换实现类的思路。1. 抽象工厂与单件一次搞定多数据库切换的C模拟工程接到公司内部管理系统数据库要在 MySQL 和 SQL Server 之间来回切换最直观的做法是在每个数据操作里写 if-else 判断类型。表一多就乱套。这次拆的实验资源来自一份课程实验报告用抽象工厂模式把用户表、部门表、项目表的操作各自抽象成接口MySQL 和 SQL Server 分别实现一整族类调用方只认抽象接口再配合单件模式保证每个具体工厂在程序里只有一个实例。它纯 C 实现不依赖任何第三方库能直接编译运行。适合正在学设计模式的人也适合准备给系统做多数据源支持的人拿它的骨架改成真实业务代码。2. 抽象工厂模式三张表与两种数据库的产品族划分这个实验的核心需求看起来简单公司数据库有用户表、部门表、项目表每张表支持查询和添加数据库类型有 MySQL 和 SQL Server 两种。如果按常规思路直接写六个类——SqlserverUser、MysqlUser、SqlserverDepartment、MysqlDepartment、SqlserverProject、MysqlProject——也能跑但客户端代码里到处是新具体类切换数据库时一堆地方要改。抽象工厂模式换了一种组织方式先定义抽象的产品接口再定义抽象的工厂接口最后为每个数据库实现一整族产品。客户端只依赖抽象接口换数据库等同于换一个具体工厂业务代码一行不用动。下面从产品接口开始拆。2.1 抽象产品接口IUser、IDepartment、IProject 只做两件事需求里每张表只支持查询和添加所以抽象产品接口只需要两个方法一个 Insert一个 Get。这里先定义三个实体类作为数据载体再定义三个抽象接口。// 实体类承载业务数据实验里只放了主键 id class User { int id; }; class Department { int id; }; class Project { int id; }; // 抽象产品接口用户表的操作契约 class IUser { public: virtual void Insert(User* user) 0; virtual User* GetUser(int id) 0; virtual ~IUser() {} }; // 抽象产品接口部门表的操作契约 class IDepartment { public: virtual void Insert(Department* department) 0; virtual Department* GetDepartment(int id) 0; virtual ~IDepartment() {} }; // 抽象产品接口项目表的操作契约 class IProject { public: virtual void Insert(Project* project) 0; virtual Project* GetProject(int id) 0; virtual ~IProject() {} };三个接口的语义很明确Insert 接收实体指针负责把一条记录写进对应表Get 接收主键 id负责按 id 查出记录并返回实体指针。定义为纯虚函数是为了强制每个具体数据库产品都实现这两件事调用方拿到接口引用后不需要关心实现细节。这里我补了一个细节原资源里的抽象产品接口没有虚析构函数。如果将来通过 IUser 指针 delete 一个 SqlserverUser 对象析构不会调用子类的析构函数可能漏掉资源释放。我一般会加上virtual ~IUser() {}这种空实现成本为零但把隐患堵住了。2.2 抽象工厂 IFactory一组创建方法组合成家族契约有了三个抽象产品接口接下来需要定义按数据库类型创建整套产品的工厂接口。这就是抽象工厂模式的关键它不是一个创建方法而是三个创建方法组成的一组契约。class IFactory { public: virtual ~IFactory() {} virtual IUser* CreateUser() 0; virtual IDepartment* CreateDepartment() 0; virtual IProject* CreateProject() 0; };每个 Create 方法负责创建一个抽象产品接口对象。调用方通过 IFactory 指针调用 CreateUser()得到的是 IUser 指针并不关心背后是 SQL Server 实现还是 MySQL 实现。这种方法签名组合就是所谓的产品族契约。和工厂方法模式的区别值得说清楚工厂方法模式针对的是单一产品子类决定具体创建哪个产品对象抽象工厂模式针对的是整族产品一个具体工厂一次性给出一整套实现。如果只做用户表一个产品用工厂方法就够了这里有三张表、两个数据库抽象工厂是更自然的选择。2.3 两套具体产品族Sqlserver 与 Mysql 的双轨实现每个数据库都需要实现三个抽象产品接口。拿用户表举例两个具体产品长这样// SQL Server 用户表实现 class SqlserverUser : public IUser { public: void Insert(User* user) { cout 向SQL Server用户表增加一个用户 endl; } User* GetUser(int id) { cout 从SQL Server用户表得到一个用户 endl; return NULL; } }; // MySQL 用户表实现 class MysqlUser : public IUser { public: void Insert(User* user) { cout 向MySQL用户表增加一个用户 endl; } User* GetUser(int id) { cout 从MySQL用户表得到一个用户 endl; return NULL; } };这里用 cout 输出模拟真实的 SQL 执行因为实验环境没有连数据库。真实项目里把 cout 替换为具体的 SQL 拼装、参数绑定、结果集映射即可接口结构不用变。SqlserverDepartment、SqlserverProject、MysqlDepartment、MysqlProject 四个类的结构和上面完全一致只是对象名和输出文字不同完整清单在资源里。需要提醒的是原资源的两个数据库版本输出文案完全一样都是增加一个用户日志里根本看不出当前操作的是哪个库。我在复现时把输出改成了带库名的写法后面验证输出时会方便很多。新接触这个模式的同学容易漏掉这一步等日志混在一起时就后悔了。2.4 为什么不用简单工厂或工厂方法切换成本和扩展性的权衡如果只是临时应付两个数据库简单工厂也能做比如写一个静态方法根据传入的字符串 switch 到不同具体类。但问题在于每加一个数据库类型这个工厂方法就要加一个 case每加一张新表所有工厂方法都要动违背开闭原则的苗头很明显。抽象工厂把变化隔离在具体工厂这一层新增一个数据库只需要新增一套产品类和一个具体工厂类已有的 SqlserverFactory、MysqlFactory 和客户端代码全部不用动。这一点在实验里体会不深因为只有两张表三个产品但放到真实系统里产品族越复杂抽象工厂的价值越明显。第 6 章我会演示新增 Oracle 数据库要改哪些文件到那时候这个结论会更直观。3. 单件模式接入工厂私有构造、静态指针与 GetInstance抽象工厂解决了由谁创建、创建哪一族的问题但还有一个隐患客户端可能会随手 new 出多个 MysqlFactory。工厂本身不是重量级对象但每个实例都持有自己的一份状态多个实例并存容易埋下不一致的雷。实验给的做法是用单件模式来约束每个具体工厂在程序里只能存在一个实例。3.1 工厂为什么要单件全局唯一访问点与创建成本单件模式最常见的应用是数据库连接实例——整个应用程序只允许一个 Connection 实例。工厂类的处境类似每个产品族只需要一个具体工厂实例因为工厂本身不保存业务数据它的职责只是按需创建产品对象多个实例并存只会增加混淆。实验里还有一个更实际的理由抽象工厂的创建过程如果涉及读取配置文件、初始化连接参数这些开销不应该被重复执行。单件模式把第一次创建的结果缓存到静态指针里后续调用直接返回已有实例既保证全局只有一份也把重复初始化的开销省掉了。3.2 懒汉式单件的三个关键点private 构造、static 指针成员、类外初始化MysqlFactory 的单件化是原资源里最完整的一段我原样保留并加了注释class MysqlFactory : public IFactory { public: ~MysqlFactory() { cout MySQL抽象工厂实例删除 endl; } IUser* CreateUser() { return new MysqlUser(); } IDepartment* CreateDepartment() { return new MysqlDepartment(); } IProject* CreateProject() { return new MysqlProject(); } // 单件入口全局唯一获取工厂实例的通道 static IFactory* GetInstance() { if (factory NULL) { cout 抽象工厂实例生成 endl; factory new MysqlFactory(); } else { cout Error, 抽象工厂实例已有 endl; } return factory; } private: // 构造函数私有化外部无法直接 new MysqlFactory() MysqlFactory() {} static MysqlFactory* factory; }; // 静态成员变量必须在类外定义并初始化 MysqlFactory* MysqlFactory::factory NULL;三个关键点逐一说明。第一构造函数放在 private 区外部代码调用new MysqlFactory()时编译直接报错唯一的创建入口被堵在 GetInstance 里。第二factory 是 static 指针成员属于类而不属于对象所有调用方共享同一个判断状态这是单件能生效的前提。第三静态成员变量必须在类外初始化如果漏掉这行MysqlFactory* MysqlFactory::factory NULL;链接时会报未定义符号。关于 GetInstance 的参数说明这个方法无参返回 IFactory 指针。第一次调用时 factory 为空进入 if 分支执行 new并打印抽象工厂实例生成第二次及之后调用时 factory 非空走 else 分支打印错误提示然后返回已有实例。这个提示信息本身就是单件是否生效的调试信号。3.3 客户端调用流程先取工厂再按表建操作对象客户端代码是验证整套设计的关键。引用原资源的 main 逻辑int main() { User* user new User(); Department* dept new Department(); Project* pro new Project(); cout MySQL endl; // 第一次获取应该输出“抽象工厂实例生成” IFactory* factory1 MysqlFactory::GetInstance(); // 第二次获取应该输出“Error, 抽象工厂实例已有” IFactory* factory2 MysqlFactory::GetInstance(); IUser* iu factory1-CreateUser(); iu-Insert(user); iu-GetUser(1); IDepartment* idept factory1-CreateDepartment(); idept-Insert(dept); idept-GetDepartment(1); IProject* ipro factory1-CreateProject(); ipro-Insert(pro); ipro-GetProject(1); return 0; }逻辑顺序很清晰先获取工厂再用工厂创建用户表操作对象插入一条用户记录、按 id 查一条记录部门表和项目表的操作完全对称。由于 factory1 和 factory2 指向的是同一个实例后面无论用哪个变量创建产品行为完全一致。这也证明了单件模式的价值——外部不用管工厂怎么实例化的拿到手的一定是同一份。3.4 单件只挡同族MySQL 工厂唯一不影响 SQL Server 工厂原资源实验总结里有一条容易被忽略的结论MysqlFactory 的单件只对 MysqlFactory 自身生效并不阻止创建 SqlserverFactory 实例。这一点从代码结构上就能看出来factory 是 MysqlFactory 的静态成员它管不到 SqlserverFactory 的静态成员。换句话说程序里可以同时存在一个 MySQL 工厂实例和一个 SQL Server 工厂实例这是期望行为但不能存在两个 MySQL 工厂实例。理解这一点后面接双数据库同时访问的场景时就不会踩坑——单件是同类的单件不是全局的单件。4. 运行与结果验证编译命令、预期输出与一行切库设计模式学得再好跑不起来也是白搭。这一章把实验工程从代码变成可执行程序同时验证两件事编译链接是否通过单件是否真的拦截了第二次实例创建。4.1 工程文件组织与编译命令原资源把所有类都放在一个文件里适合快速演示。实际工程项目里我一般会拆成头文件和源文件按角色分文件这样每个文件职责单一也方便复用。建议的文件结构abstract_factory/ ├── products.h # User/Department/Project 实体类 ├── abstract_products.h # IUser/IDepartment/IProject 抽象接口 ├── concrete_products.h # SqlserverUser/MysqlUser 等六个具体产品类 ├── factory.h # IFactory、SqlserverFactory、MysqlFactory └── main.cpp # 客户端入口编译命令很简单因为整个工程没有任何第三方依赖g -stdc11 main.cpp -o factory_demo ./factory_demo如果还是想用单文件验证把所有类和 main 写进一个 cpp 文件直接g main.cpp -o factory_demo一样能过。头文件里注意加 include guard避免重复包含时报重定义错误。Windows 上直接用 Visual Studio 建一个空控制台项目把文件加进去编译也可以不需要额外配置。4.2 main 函数执行流程与预期输出按改进后的带库名输出文案main 的完整执行流程预期如下表执行步骤控制台输出factory1 MysqlFactory::GetInstance()抽象工厂实例生成factory2 MysqlFactory::GetInstance()Error, 抽象工厂实例已有iu-Insert(user)向MySQL用户表增加一个用户iu-GetUser(1)从MySQL用户表得到一个用户idept-Insert(dept)向MySQL部门表增加一个部门idept-GetDepartment(1)从MySQL部门表得到一个部门ipro-Insert(pro)向MySQL项目表增加一个项目ipro-GetProject(1)从MySQL项目表得到一个项目这里有一个容易疑惑的点程序结束前没有任何 delete所以析构函数的 cout 根本不会执行。如果你在输出里看不到MySQL抽象工厂实例删除不是代码写错了而是实例根本没被销毁。想验证析构输出可以在 main 末尾手动delete factory1;但这会引出第 5 章讲的坑。4.3 一行切换 SQL Server抽象工厂的价值验证抽象工厂最爽的地方在这一步。把 main 里的两行替换成 SQL Server 工厂IFactory* factory1 SqlserverFactory::GetInstance(); IFactory* factory2 SqlserverFactory::GetInstance();其余代码一行都不用改。编译运行后抽象工厂实例生成这行由 SqlserverFactory 触发三张表的操作全部落到 SQL Server 产品族。客户端从头到尾只依赖 IFactory、IUser、IDepartment、IProject 四个抽象类型具体数据库类型被隔离在工厂这一层这就是抽象工厂模式解耦能力的直接体现。4.4 验证单件唯一性的两个方法地址比较与计数输出单件是否真的唯一光看日志不够严谨。我一般会在测试代码里打印 GetInstance 返回的指针地址IFactory* f1 MysqlFactory::GetInstance(); IFactory* f2 MysqlFactory::GetInstance(); cout f1 address: f1 endl; cout f2 address: f2 endl;如果两次输出地址相同单件生效地址不同说明 GetInstance 里 new 的结果没被缓存每次调用都新建了实例。第二个方法是给 GetInstance 加一个静态计数变量每次进入方法自增一次最后打印总数正常情况下总数应该等于 1。这两个方法都能在第一时间暴露单件失效问题。5. 避坑与排查单件失效、实例泄漏与复制粘贴事故再好的设计模式落地时也会被细节坑。这一章全部是我在复现这个实验时遇到或观察到的真实问题。每条按现象→原因→解决整理能直接用在排查现场。5.1 坑一return new 没赋值单件形同虚设现象连续两次调用 SqlserverFactory::GetInstance()每次都打印抽象工厂实例生成且两次返回的指针地址不同。单件完全失效。原因GetInstance 里写的是return new SqlserverFactory();注意这里直接把 new 的结果返回了并没有赋值给静态指针 factory。下一次调用时 factory 仍然是 NULL继续进入 if 分支再 new 一次。而且 return 后面的 cout 语句永远不会执行属于不可达代码。解决先赋值给 factory再统一从尾部 return 这个指针。// 错误写法new 了直接返回factory 永远为空 static IFactory* GetInstance() { if (factory NULL) { return new SqlserverFactory(); cout 抽象工厂实例生成 endl; // 永远执行不到 } else { cout Error, 抽象工厂实例已有 endl; } return factory; } // 正确写法先执行 factory new再 return factory static IFactory* GetInstance() { if (factory NULL) { cout 抽象工厂实例生成 endl; factory new SqlserverFactory(); } else { cout Error, 抽象工厂实例已有 endl; } return factory; }这个坑极具隐蔽性因为单从语法上看完全合法编译器不会报任何警告。我复现时差点把锅甩给编译器。从那以后我检查单件代码第一眼一定看 new 出来的对象有没有被静态指针接住。5.2 坑二析构输出张冠李戴MysqlFactory 打印 SQL Server 字样现象销毁 MysqlFactory 实例时控制台输出的是SQL Server 抽象工厂实例删除而不是 MySQL 的提示。原因原资源里 MysqlFactory 的析构函数是从 SqlserverFactory 复制粘贴过来的改类名时漏掉了析构函数里的输出文字。类名和日志文案不一致排查时极具迷惑性——程序行为是 MySQL 的日志却总提示 SQL Server。解决复制粘贴生成具体类后逐文件核对类名、方法名、输出文案三处。批量生成六个具体产品类和两个工厂类时这种错误几乎必现一次测试时重点看日志里的数据库名和类名的匹配关系。5.3 坑三new 出来没人 delete析构函数成了摆设现象程序退出时没有任何析构输出用内存检测工具能查出多处泄漏。User、Department、Project 三个实体是 new 的MysqlUser 等产品对象是工厂 new 的全部没有对应 delete。原因原资源为了演示设计模式把内存管理完全省略了。真实工程里这种写法会持续泄漏服务跑几天内存就飘起来了。解决小实验无所谓真实项目建议用智能指针兜底。以 C11 为例std::unique_ptrIFactory factory(MysqlFactory::GetInstance()); std::unique_ptrIUser iu(factory-CreateUser()); std::unique_ptrUser user(new User()); iu-Insert(user.get());到这里你会发现问题工厂指针被 unique_ptr 接管后程序退出时会调用 MysqlFactory 的析构函数那行MySQL抽象工厂实例删除终于能看到了。但要注意GetInstance 返回的是 IFactory 指针unique_ptr 析构时通过 IFactory 的虚析构调用具体的析构这也是第 2 章我坚持让抽象产品接口加虚析构的原因。5.4 坑四私有构造堵住了 new也堵住了优雅销毁现象单件工厂实例没有提供任何销毁入口。进程不退实例就永远驻留想测试重建工厂的场景只能重启进程。原因构造函数私有化之后外部无法 delete 并重新 new 一个实例而 GetInstance 里也没有提供对应的 DestroyInstance 方法。解决需要可靠生命周期管理时补一个静态销毁方法并把静态指针复位为 NULLstatic void DestroyInstance() { if (factory ! NULL) { delete factory; factory NULL; } }销毁后再次调用 GetInstance 会重新走创建流程这正好可以复现实例生成 → 错误提示 → 销毁 → 再次生成的完整状态切换。调用时机放在程序收尾处或者单元测试的 teardown 阶段。5.5 坑五多线程并发首访可能创建两个工厂现象两个线程同时第一次调用 GetInstance线程 A 判断 factory 为 NULL还没执行 new线程 B 也判断为 NULL随后两个线程各 new 一个实例。单件在并发下彻底失效。原因懒汉式单件的if (factory NULL)判断和后续的factory new不是原子操作存在时间窗口。这是懒汉式的天生缺陷。解决最直接的办法是给 GetInstance 加锁static IFactory* GetInstance() { std::lock_guardstd::mutex lock(mutex_); if (factory NULL) { factory new MysqlFactory(); } return factory; }注意锁必须放在 if 判断外面否则两个线程会串行进入第二次仍会重新 new。代价是每次调用都有锁开销所以生产环境常用双重检查锁定来优化但 C 里内存序问题比想象中复杂新手不建议自行实现。下一章会给出一个更省心的替代方案。6. 扩展第三种数据库接入、Meyer 单件与智能指针收尾抽象工厂的价值只有在扩展时才真正显现。假设现在要接 Oracle 数据库改动点非常清晰新增 OracleUser、OracleDepartment、OracleProject 三个产品类分别继承 IUser、IDepartment、IProject再新增 OracleFactory 继承 IFactory 并实现三个 Create 方法。客户端只换一行把MysqlFactory::GetInstance()换成OracleFactory::GetInstance()三张表操作自然切到 Oracle 产品族。已有的 Sqlserver 和 Mysql 代码一行不用动。单件工厂的写法我推荐换成 Meyer 单件这是 C11 标准里线程安全且最省心的一种实现。同样是 MysqlFactory改写后长这样class MysqlFactory : public IFactory { public: static MysqlFactory GetInstance() { static MysqlFactory instance; // 首次调用时构造标准保证线程安全 return instance; } private: MysqlFactory() {} MysqlFactory(const MysqlFactory); // 禁止拷贝 MysqlFactory operator(const MysqlFactory); // 禁止赋值 };函数局部静态变量的初始化由编译器保证线程安全而且返回的是引用不需要 delete不存在泄漏问题。唯一要适应的是调用方式变成MysqlFactory::GetInstance().CreateUser()语义上更干净。如果项目里必须统一用 IFactory 指针可以写IFactory* p MysqlFactory::GetInstance();中转一下。我把这份代码按接口 → 产品 → 工厂 → 客户端四个层次重排时最大的教训就在第 5 章的坑一一个 return new 的疏漏会让整个单件静默失效日志还打印着实例生成让人误以为一切正常。从那以后我每次写完单件都强制走一遍自查清单静态指针有没有类外初始化、new 出来的对象有没有赋值给指针、返回值是不是同一个指针、析构文案有没有张冠李戴。这套检查花了不到两分钟但帮我躲过了至少三次莫名其妙的单件失效排查。完整工程已经整理进实验资源直接下载后编译运行就能复现上面所有输出和两个工厂的行为对比。希望帮到你。本文还有配套的精品资源点击获取