抽象工厂与单件模式:C++双数据库切换代码实现深度解析

发布时间:2026/10/24 14:17:11
抽象工厂与单件模式:C++双数据库切换代码实现深度解析 简介一份软件构造课程实验报告面向学习设计模式与数据库抽象层开发的软件工程专业学生。报告以徐奕同学2019年完成的抽象工厂模式与单件模式编程实验为基础针对MySQL与SQL Server两种数据库给出用户、部门、项目三类对象的具体实现并说明如何借助单件模式确保工厂实例唯一适合作为课程设计、实验报告撰写或设计模式复习的参考。包体为1个PDF文件大小约301KB内容包含实验UML图、IFactory抽象工厂、具体工厂类及主要函数代码分析结构完整便于对照阅读。该资源已有122人学习对希望理解多产品族创建和全局唯一实例控制方法的读者具有直接参考价值。1. 一个实验报告能讲明白的事抽象工厂怎么管住两套数据库这份实验报告表面上看是一份课程作业实际是一个典型的「设计模式示范工程」作者模拟了一个公司的数据库场景用户表、部门表、项目表三张表都要支持查询和添加而底层数据库可能是 MySQL 也可能是 SQL Server。如果老老实实写两套实现每个数据操作都要写 if 判断走哪个分支代码一多就乱成一团。抽象工厂模式正是解决这类「同一套业务、多个实现族」问题的标准答案——它把「选哪个数据库」这件事从业务代码里剥离出去调用方只跟抽象的 IUser、IDepartment、IProject 打交道至于背后是 MySQL 还是 SQL Server由工厂决定。报告中还叠加了单件模式让每个工厂在整个程序里只存在一个实例。适合人群很明确正在学设计模式的软件工程学生、想给老系统换数据库的开发者、以及对「多个产品族 工厂唯一性」这类组合设计感兴趣的人。读完这份报告你能把一个带双数据库切换能力的小型 C 工程从结构到代码逐层看透甚至顺手改成自己项目里可用的底座。2. 架构拆解用户、部门、项目三个产品族与双工厂的职责划分2.1 抽象工厂模式的本质不是“一个大工厂”而是“一组生产线”初学抽象工厂模式时最容易犯的错是把它理解成一个「啥都能造的超级工厂」。实际上抽象工厂模式解决的是「产品族」的问题一份业务里通常有多个产品比如用户、部门、项目它们各自有不同数据库的实现版本。如果不用设计模式你会写出createUserByMysql()、createUserBySqlServer()这种函数名满天飞的代码每次新增数据库都要全局搜索修改。而抽象工厂的思路是定义一组抽象产品接口IUser、IDepartment、IProject再为每个数据库定义一套「生产线」——SqlServerFactory 只生产 SqlServer 系列的三个产品MysqlFactory 只生产 Mysql 系列的三个产品。调用方拿到一个工厂实例就可以通过统一的CreateUser()、CreateDepartment()、CreateProject()方法创建出完整的一组产品对象。这组对象天然是配套的不会出现「用户是 MySQL 的、部门是 SQL Server 的」这种混乱局面。在此之上还有一层设计每个工厂类同时用单件模式约束保证全程序只有一套 MySQL 生产线或 SQL Server 生产线。为什么这样设计因为产品对象本身是轻量的工厂类内部不维护变化的状态重复创建纯粹是浪费更重要的是抽象工厂模式的应用里一套业务通常只需要一个具体工厂实例就够了。两个模式配合起来既解决了产品族的动态选择问题又避免了工厂对象被反复创建的资源开销。2.2 目录与类结构13 个类的职责对照整份代码没有分文件所有类都写在同一个 main 级别的源文件里实验报告场景一般如此真实工程会按类拆文件。但逻辑上可以清晰地划分出 13 个类它们组成了「4 层结构」抽象工厂层IFactory定义了创建产品对象的三个纯虚函数。抽象产品层IUser、IDepartment、IProject 三个接口分别定义 Insert 添加和 GetXxx 查询两个方法。具体产品层每张表都有两个实现版本共 6 个类。类名所属层次核心职责IFactory抽象工厂声明 CreateUser、CreateDepartment、CreateProject 三个接口SqlserverFactory具体工厂SQL Server 族创建 SQL Server 版的用户/部门/项目对象单件约束MysqlFactory具体工厂MySQL 族创建 MySQL 版的用户/部门/项目对象单件约束User / Department / Project简单数据类各自携带 int id 字段模拟一条表记录IUser / IDepartment / IProject抽象产品接口定义 Insert 与 GetXxx 方法与具体数据库解耦SqlserverUser / SqlserverDepartment / SqlserverProject具体产品SQL Server 族实现 SQL Server 版的添加与查询模拟输出MysqlUser / MysqlDepartment / MysqlProject具体产品MySQL 族实现 MySQL 版的添加与查询模拟输出数据类User、Department、Project故意做成只有int id的极简类因为这份实验的核心不是数据建模而是展示「如何在不同实现族之间做隔离」。如果你想把它用到真实项目里只需要在这些类里补充业务字段比如用户名字段、部门名称字段、项目预算字段并把具体产品类的Insert、GetXxx方法里的cout替换成真正的 SQL 语句执行逻辑。2.3 单件模式在工厂层的作用为什么不能有两个 MySQL 工厂报告中有一句意图很明确「注意使用单件模式写 MySQL 和 SQL Server 的具体工厂时只能对自身单件」。这句话包含两个关键信息点。第一单件是「各自单件」而不是「全局唯一」程序运行期间可以同时存在一个 SqlServerFactory 和一个 MysqlFactory但不允许同时存在两个 MysqlFactory。这个设计完全合理切换数据库的诉求意味着不同数据库的工厂可能需要共存而同一种工厂只需要一个。第二单件的实现方式是「懒汉式」——第一次调用 GetInstance 时才创建实例之后所有调用都返回同一个指针。懒汉式的优点是延迟加载、不用则已用则建缺点是第一次访问时在多线程环境下有竞态问题这个话题留到第 5 章避坑时再展开。从报告给出的代码来看两个具体工厂类都把构造函数放在private区这样客户端就不能直接new SqlserverFactory()只能通过GetInstance()获取。这是单件模式的标准姿势用语言特性而不是口头约定来强制约束。3. 源码走读从 IFactory 到 SqlServer 与 MySQL 的两条生产线3.1 IFactory 抽象工厂工厂接口的三种创建方法class IFactory{ public: virtual ~IFactory(){ cout 抽象工厂实例删除 endl; } virtual IUser* CreateUser() 0; virtual IDepartment* CreateDepartment() 0; virtual IProject* CreateProject() 0; };三个纯虚函数对应三条产品生产线的入口。CreateUser返回的是 IUser 抽象类型指针调用方拿到这个指针后不需要关心具体是哪个数据库的实现。这里有一个细节值得注意析构函数是virtual的但函数体是空的。写成虚析构是正确的——因为后续具体工厂类会被IFactory*指针持有删除基类指针时必须走虚析构才能正确释放派生类对象。不过实验中工厂对象实际上是单件且不显式删除的所以这个析构函数大概率不会被调用。如果你要在自己的工程里用同样的设计析构函数体里记得delete factory否则静态指针指向的对象永远不会释放跑一次不觉得跑一万次循环就会看到内存只涨不掉。3.2 三组具体产品类同一套方法签名两套实现逻辑以用户产品为例报告中的实现是这样的class IUser{ public: virtual void Insert(User* user) 0; virtual User* GetUser(int id) 0; }; class SqlserverUser: public IUser{ public: void Insert(User* user){ cout 增加一个用户 endl; } User* GetUser(int id){ cout 得到一个用户 endl; return NULL; } }; class MysqlUser: public IUser{ public: void Insert(User* user){ cout 增加一个用户 endl; } User* GetUser(int id){ cout 得到一个用户 endl; return NULL; } };两个具体类的方法体一模一样区别只是类名不同。这在实验代码里是正常的——它们的目的是展示「结构正确」而不是「真的发出 SQL」。但你在真实项目里会替换这两处逻辑SqlserverUser 的 Insert 里写EXEC 存储过程或拼 T-SQL 的 INSERT 语句MysqlUser 的 Insert 里写INSERT INTO user_table VALUES (...)GetUser 同理一个写SELECT * FROM users WHERE id?对应 SQL Server 的参数化写法另一个写SELECT * FROM users WHERE id?的 MySQL 版本占位符风格是%s或?取决于驱动。Department 和 Project 两组类的结构与此完全相同唯一的差异不过是从「用户」换成了「部门」「项目」。3.3 具体工厂与单件两个 GetInstance 的实现差异这是整段代码里最值得逐行读的部分因为两个工厂的单件实现存在值得说道的差异class SqlserverFactory: public IFactory{ public: ~SqlserverFactory(){ cout SQL Server 抽象工厂实例删除 endl; } IUser* CreateUser(){ return new SqlserverUser(); } IDepartment* CreateDepartment(){ return new SqlserverDepartment(); } IProject* CreateProject(){ return new SqlserverProject(); } static IFactory *GetInstance(){ if (factory NULL){ return new SqlserverFactory(); cout 抽象工厂实例生成 endl; } else { cout Error, 抽象工厂实例已有 endl; } return factory; } private: SqlserverFactory(){} static SqlserverFactory* factory; }; SqlserverFactory* SqlserverFactory::factory NULL;这段代码有三个问题任何一个放到真实工程里都是隐藏炸弹。第一GetInstance里return new SqlserverFactory()之后紧跟着一行cout这行输出是永远执行不到的不可达代码。第二更致命的是这行return new只创建了对象却没有把对象赋值给静态成员factory所以第二次调用GetInstance()时factory仍然是 NULL又会重新 new 一个实例单件约束直接失效。第三GetInstance的返回类型声明为IFactory*实际返回SqlserverFactory*虽然可以隐式转换但语义上并不严谨。再看 MySQL 工厂的实现作对比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: MysqlFactory(){} static MysqlFactory* factory; }; MysqlFactory* MysqlFactory::factory NULL;MysqlFactory 的写法才是正确的单件先打印日志再把 new 出来的对象赋值给静态成员变量最后返回该成员。两次调用GetInstance()第二次因为factory ! NULL走 else 分支打印「Error」。两份代码一对比明显 SqlserverFactory 版本是一个手误或笔误很可能是作者写完 SqlServer 版本后急着改 MySQL 版本把赋值语句漏了。这也解释了为什么实验报告的结果图中「第二次生成 MySQL 工厂失败」——那个输出是 MySQL 工厂产生的而 SqlServer 工厂如果真被客户端连续调用两次逻辑上会翻车。报告给出的实验总结里也提到「注意使用单件模式时只能对自身单件」但并没有意识到 SqlServer 这个函数体里的实现错误。读者在跑这份代码时如果发现「第二次居然也能 new 出 SqlServer 工厂」不是设计问题是这一行赋值漏了。4. 客户端流程从创建到输出“增加一个用户”的分步过程4.1 main 函数调用链路与执行顺序客户端的 main 函数是整份代码的入口把前面的类全部串起来。流程可以拆成六个步骤int main(){ User* user new User(); Department* dept new Department(); Project* pro new Project(); cout endl MySQL endl; IFactory *factory1 MysqlFactory::GetInstance(); IFactory *factory2 MysqlFactory::GetInstance(); // 第二次获取应失败 cout factory1 factory2 ? (factory1 factory2 ? true : false) endl; 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; }注意这段代码是报告原文 main 函数的一个「可运行」补全版——原文里创建了 factory2 并调用了 CreateUser 但输出示例中只有 MySQL 字样实际实验中应该能看到「Error, 抽象工厂实例已有」。整个执行顺序是先 new 三个数据对象它们没有构造函数等于三个空壳对象然后第一次调用 GetInstance 触发单件的创建分支打印「抽象工厂实例生成」同时静态指针 factory 被赋值为新工厂对象第二次调用 GetInstance 时 factory 已非空于是走 else 分支打印「Error, 抽象工厂实例已有」并返回同一个指针。之后的三组调用分别走 MySQL 版实现类的方法每组都输出「增加一个XXX」「得到一个XXX」。注意 main 里没有 delete 任何对象单件工厂对象也不建议 delete——程序退出后系统回收。4.2 预期输出结果的逐行解读如果按修正后的代码执行标准输出应该是MySQL 抽象工厂实例生成 Error, 抽象工厂实例已有 factory1 factory2 ? true 增加一个用户 得到一个用户 增加一个部门 得到一个部门 增加一个项目 得到一个项目每一行输出都对应一段代码逻辑「抽象工厂实例生成」来自第一次 GetInstance 的 if 分支「Error, 抽象工厂实例已有」来自第二次 GetInstance 的 else 分支factory1 factory2判断为 true 是因为两次返回的是同一个单件指针后面六行分别是三个产品族的 Insert 和 GetXxx 方法。如果直接跑报告里的原始代码而不做任何修改SqlServer 工厂版本的问题可能不会暴露——因为 main 只在 MySQL 工厂上连续调用了两次 GetInstance。但如果你把 main 里的MysqlFactory替换成SqlserverFactory运行结果会变成第一次 GetInstance 返回了新对象但没存指针第二次 GetInstance 因为 factory 仍然为 NULL再次进入 if 分支再次打印「抽象工厂实例生成」——这就意味着「Error」永远不会出现单件约束形同虚设。跑这种代码时测试端口不应该只覆盖正常路径至少要覆盖两次调用这个边界条件。4.3 编译运行与实验环境这份代码是标准 C不依赖任何第三方库。报告日期是 2019 年实验环境大概率是 Visual Studio 或 Dev-C但用现代 GCC 也能直接编译。把除 main 之外的所有类定义按顺序粘贴到一个factory_demo.cpp文件里然后执行g -o factory_demo factory_demo.cpp -stdc11 ./factory_demo-stdc11是为了启用当前标准语法实际这份代码用 C98 也能编译——它只用到了类继承、虚函数、静态成员这些非常基础的特性。如果你在 Windows 的 Visual Studio 里运行把 main 函数里的endl系列输出换成中文没问题VS 默认编码的源文件需要用 UTF-8 保存否则中文cout会乱码这是实验环境里经常踩到的第一个坑。5. 避坑清单这段实验代码里的 5 个典型陷阱5.1 第二次创建 SqlServer 工厂竟能成功现象按报告代码运行 SqlServerFactory::GetInstance() 两次第二次仍然打印「抽象工厂实例生成」没有显示「Error, 抽象工厂实例已有」。 原因SqlserverFactory 的 GetInstance 里写了return new SqlserverFactory()但少了一步factory new SqlserverFactory()赋值操作导致静态成员 factory 永远为 NULL单件约束从逻辑上失效。对照 MysqlFactory 的实现可以看到正确的顺序是「先赋值再返回」。 解决把 SqlserverFactory 的 GetInstance 修正为与 MysqlFactory 一致的写法先判断factory NULL然后在 if 分支里factory new SqlserverFactory(); cout 抽象工厂实例生成;最后统一return factory;。改完后重新编译运行第二次调用就会正常走 else 分支。5.2 中文字符的引号导致编译报错现象代码里大量出现cout “增加一个用户”用 GCC 编译时报错invalid character或expected expression提示位置正好在开头引号附近。 原因报告原文里的引号是中文全角引号「“」和「”」C 语法要求字符串字面量必须用英文半角双引号包住。这些全角引号也许在 Word 里显示正常但不属于合法的 C 词法单元。 解决全文搜索替换两个字符中文左引号和中文右引号都替换成英文半角。注意cout ...里的字符串内容可以保留中文但引号本身必须是半角字符。这个工程最简单在 IDE 里直接 CtrlF 输入全角引号从代码中复制替换为半角引号。5.3 return new 后面的 cout 永远不执行现象SqlserverFactory::GetInstance() 中return new SqlserverFactory(); cout 抽象工厂实例生成 endl;永远看不到打印出的日志但代码看起来逻辑没问题。 原因return 语句已经结束函数的执行后面的语句是「不可达代码」编译器会给出 warningunreachable code但不会报错。作者可能想先打印再返回但把 return 的顺序写错了。 解决把 return 移到函数末尾将赋值和打印放在前面。正确顺序是判断 —— 赋值 —— 打印 —— return。当你写任何「先输出再返回」的代码时记住 return 是终结点不要在其后追加任何逻辑。5.4 懒汉式单件在多线程下不是线程安全的现象如果这个代码被改造成多线程应用第一次有多个线程同时调用 GetInstance 时可能出现 factory 被创建多次的情况——不同线程拿到不同指针。 原因懒汉式单件「先判断再创建」是非原子操作线程 A 判断完 factory NULL 后还没赋值线程 B 也执行了判断同样看到 NULL于是两个线程各自 new 出一个实例。报告里的实验是单线程运行所以看不到这个现象。 解决最小改动是在 GetInstance() 内部加锁C11 可以用std::call_once配合局部静态变量实现线程安全的单件又叫 Meyers Singleton。或者干脆用「饿汉式」——在类外部直接初始化静态成员甚至 C11 之后可以写成static LocalSingleton getInstance(){ static LocalSingleton instance; return instance; }由编译器保证局部静态变量的线程安全初始化。5.5 静态成员变量的初始化位置容易漏写现象链接时出现undefined reference to SqlserverFactory::factory的报错。没有这一行代码编译能过链接过不了。 原因类内static SqlserverFactory* factory;只是声明必须在类外部定义并初始化报告里放在了类结束后的全局位置SqlserverFactory* SqlserverFactory::factory NULL;。初学者容易把静态成员当成普通成员只写声明不写定义。 解决每个具体工厂类后面紧跟一行全局定义。另一个小陷阱两个工厂类的静态成员名都叫 factory但因为作用域不同分别属于 SqlserverFactory 和 MysqlFactory互不干扰。在实际工程中你可以把两个定义放在类定义的下方一起维护避免漏掉。6. 二次开发思路把双数据库切换改造成参数化工厂拿到这份代码后最自然的改造方向不是继续对着控制台输出而是把它变成一个「可以动态选择数据库」的组件。原始设计里客户端要确定用哪个数据库只能手动 new 对应的工厂例如MysqlFactory::GetInstance()。真实项目里数据库连接配置通常来自配置文件、环境变量或运行时参数客户端不该写死数据库类型。我建议的改造是加一个「选择层」用一个函数取代客户端的手动选择#include string #include map class FactoryRegistry { public: static IFactory* getFactory(const std::string type) { static std::mapstd::string, IFactory* factories; if (factories.empty()) { factories[mysql] MysqlFactory::GetInstance(); factories[sqlserver] SqlserverFactory::GetInstance(); } auto it factories.find(type); return it ! factories.end() ? it-second : nullptr; } };这个 Registry 用 map 维护数据库名与工厂实例的映射工厂实例依然走单件但客户端代码变成了IFactory* factory FactoryRegistry::getFactory(mysql);。如果要新增 PostgreSQL 支持只需要新增一个 PostgresqlFactory 和一组 PostgresqlUser、PostgresqlDepartment、PostgresqlProject然后在注册表里加一行main 函数和业务代码完全不用改。这正好体现了抽象工厂模式「新增一个产品族不影响既有代码」的开放封闭原则。改造后的验证方法也很简单写一个测试函数循环遍历注册表里的数据库类型对每个工厂依次创建用户、部门、项目并调用 Insert 和 GetXxx断言输出与预期一致。这个测试脚本同时也是很好的回归用例——以后改任何具体产品类跑一遍就能知道哪些数据库分支被影响了。做完这个改造这份实验代码就从「作业」变成了「可扩展的多数据库操作骨架」数据类补字段、具体产品类补 SQL、工厂类补日志和资源回收。我当时改完这些之后每次拿到一份设计模式示范代码第一件事就是看它的单件写法是不是真的能拦住第二次创建然后立刻写一个两次调用 GetInstance 的测试用例去验证而不是直接跳进业务逻辑。这种「先验证模式约束、再填业务细节」的习惯帮我在后续好几个项目里躲开了单件失效的坑希望也能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询