
简介软件构造课程中抽象工厂模式与单件模式的编程实现实验由徐奕学号E21614061完成面向软件工程专业学生及设计模式初学者。实验报告以公司数据库模拟场景切入围绕用户、部门、项目三类数据操作设计抽象接口与MySQL、SQL Server两套具体实现并通过单件模式限制工厂实例数量完整呈现从需求分析、UML建模到代码实现的全部过程。资源为1个PDF文档压缩包仅301KB已有122人学习。报告详述了抽象工厂接口IFactory、具体工厂类SqlserverFactory与MysqlFactory以及SqlserverUser、MysqlProject等数据库操作实现类配有实验时间、实验目的、原理说明、实验代码和实验结论同时讨论了单件模式通过静态方法或私有构造函数确保全局唯一实例的常见实现方式。借助本报告读者不仅能获得一份结构完整的实验报告范本还能深入理解如何在运行时动态切换数据库类型、保持数据库操作接口一致性从而掌握多产品族创建与全局唯一实例控制的核心设计思想。1. 抽象工厂与单件为什么一个“数据库切换”需求最后会演变成双模式嵌套一个看起来只是“公司数据库支持 MySQL 和 SQL Server 两种库”的需求如果直接写代码大多数人第一反应是写一堆 if-else判断当前配置是什么数据库然后分别执行对应的 SQL 语句。但这份实验报告给出的方案却是先定义一个抽象工厂接口再为每种数据库实现一套完整的产品族最后还要用单件模式把工厂实例锁死成一个。明明是两句 if 就能解决的问题为什么要绕这么大一圈关键原因在于在真实系统里用户表、部门表、项目表不是三个孤立的数据结构而是一组“产品族”——它们共享同一个数据库选型约束。你不可能在 MySQL 下用 MySQL 的用户表操作、却又用 SQL Server 的部门表操作。抽象工厂模式的存在意义就是把这组产品的创建逻辑打包成不可拆散的整体而单件模式则解决了另一个问题既然一个数据库对应一套产品族那这套产品族的工厂在进程里只需要一个实例。两个模式叠加表面上是在写“创建对象的代码”实际上是在回答两个更底层的问题——产品族的一致性怎么保证以及工厂本身的生命周期怎么管理。本文的核心脉络先交代清楚第一章讲清楚这两个模式结合要解决什么问题第二章拆解抽象工厂的产品族创建机制第三章讲单件模式的底层约束也就是构造函数私有化和静态指针第四章是避坑排查重点分析这类实现里最容易翻车的线程安全和实例管理问题第五章回到设计取舍讨论这套方案在真实工程中的边界。2. 抽象工厂模式产品族一致性是怎么被“结构”强制保证的2.1 从 if-else 到抽象工厂变化点在哪里先看最朴素的做法。假设没有设计模式代码会长这样根据数据库类型字符串判断该创建哪个具体操作对象。这种写法的问题不在于性能而在于“一致性”完全靠程序员自觉——如果未来加入一个 Oracle 分支但忘了更新某一处判断就会出现用户表走 MySQL、项目表走 SQL Server 的混乱状态。这个 bug 在测试环境未必暴露因为测试环境通常只配置一种数据库等部署到生产环境切换数据库时才炸。抽象工厂的思路是反过来的不是由调用方决定创建谁而是调用方只拿到一个工厂接口由这个工厂负责创建一组完整的、彼此配套的产品。就像下单买套餐而不是单点菜品——套餐的内容是固定的你不会拿到可乐配米饭这种不协调的组合。在实验代码里IFactory接口定义了三个纯虚函数分别创建IUser、IDepartment、IProject这就是产品族的边界。2.2 接口隔离为什么客户端只依赖抽象类实验代码里有一个极易被忽略的设计点客户端代码中User* user new User()创建的是具体数据对象但操作这些对象的iu、idept、ipro变量它们的类型分别是IUser*、IDepartment*、IProject*。这是整个模式最核心的约束——客户端代码里不能出现任何具体产品类名。// 客户端只认识抽象接口不认识 SQL Server 或 MySQL 的具体类 IFactoryfactory1 MysqlFactory::GetInstance(); IUseriu factory1-CreateUser(); iu-Insert(user); iu-GetUser(1);这段代码里从factory1拿到手的是IUser*指针真正调用Insert和GetUser时实际执行的是MysqlUser的方法。如果哪天要把数据库从 MySQL 切换到 SQL Server客户端唯一的改动是把MysqlFactory::GetInstance()换成SqlserverFactory::GetInstance()——一行代码的差异其余全部不变。这就是依赖倒置原则在起作用高层模块不依赖低层模块的具体实现而是双方都依赖抽象。CreateUser()、CreateDepartment()、CreateProject()三个纯虚函数的意义还在于它们把“创建”动作和“使用”动作分离开了。客户端不知道创建过程中发生了什么——是直接 new 还是从对象池取、是否需要初始化连接这些细节全部封装在具体工厂内部。2.3 每个产品族内部的自洽性为什么 MySQL 工厂不能返回 SQL Server 的对象抽象工厂最容易被新手误解的地方是以为它只是把多个工厂方法做了简单聚合。实际上抽象工厂的关键在于“每个具体工厂内部的所有产品必须是属于同一个产品族的”。MysqlFactory的CreateUser()返回MysqlUserCreateDepartment()返回MysqlDepartmentCreateProject()返回MysqlProject——这三个类都实现了 MySQL 的访问逻辑它们组合在一起才构成一个完整的、可工作的 MySQL 数据库操作集。如果把MysqlFactory::CreateDepartment()的实现改成return new SqlserverDepartment()编译器不会报错因为SqlserverDepartment也实现了IDepartment接口语法上是合法的。但程序运行后会出现用户表走 MySQL、部门表却通过 SQL Server 的语法去查——这种“产品族内部不一致”的问题在抽象工厂模式里被视为架构级错误。工厂方法模式解决的是“一个产品”的创建问题抽象工厂解决的是“一组产品”的协调问题两者的边界就在这里。3. 单件模式构造函数私有化与静态指针的底层机制3.1 为什么工厂需要单件创建型模式的“实例唯一性”需求现在考虑第二个问题工厂为什么需要单件直接new MysqlFactory()不行吗从实验代码可以看到MysqlFactory的内部状态其实很简单——它只有CreateUser等几个方法本身不保存数据库连接或缓存数据理论上创建多个实例也不会出逻辑错误。但这里有一个资源和管理层面的考虑工厂是产品族的入口如果系统里同时存在多个MysqlFactory实例这些实例本身没有状态但它们各自创建的产品对象可能分布在不同的缓存上下文里极端情况下会让“到底走了哪个工厂”变得难以追踪。用单件模式限制工厂的唯一性真正收益是两点一是省掉了重复创建工厂对象的开销虽然这个开销本身很小但设计上的洁癖更强调的是“这个工厂必须只有一份因为它是全局策略的代表”——数据库选型是全局决策不应该被多个工厂实例模糊掉二是为后续扩展留下空间如果未来工厂需要持有数据库连接池或配置对象单件模式保证了状态只存在一份。3.2 static 成员与类外初始化的执行顺序实验代码里有一段关键的全局初始化// SqlserverFactory.h 内部 static SqlserverFactory* factory;// 类外定义 SqlserverFactory* SqlserverFactory::factory NULL;这个 NULL不是随意写的。C 的静态成员变量必须在类外定义并初始化否则链接时会报“未定义引用”的错误。这里的初始化为 NULL 意味着工厂实例初始不存在等到第一次调用GetInstance()时才真正创建——这就是懒汉式单件的起点。很多初学者在这里会犯一个错把静态成员初始化写在类内部。但实际上 C 标准规定静态数据成员的声明只是声明不分配存储空间必须到类外定义才真正分配内存。这个细节决定了单件模式的初始化入口不在构造函数而在GetInstance静态方法里。3.3 构造函数 private 的“防御半径”实验总结里明确写着“具体工厂的构造函数需要放在 private 中这样客户端不能随意创建多个工厂。”这一句是整个单件模式的核心机制。class SqlserverFactory : public IFactory { private: SqlserverFactory() {} static SqlserverFactory* factory; };构造函数是 private 之后外部代码new SqlserverFactory()会被编译器直接拦截——因为new操作的第一步是调用构造函数如果构造函数不可访问new表达式本身就不合法。客户端唯一能拿到实例的入口只剩GetInstance()静态方法而在这个方法内部做了“存在则返回已有实例不存在则创建”的判断。这里需要特别说明的是GetInstance()里的逻辑瑕疵。实验代码中SqlserverFactory::GetInstance()写了下面这段static IFactory *GetInstance(){ if (factory NULL ){ return new SqlserverFactory(); cout抽象工厂实例生成endl; } else { coutError, 抽象工厂实例已有endl; return factory; } }这段代码有一个明显的 bugreturn new SqlserverFactory()执行后函数直接返回后面的cout永远不会执行。而且更关键的是new出来的对象没有赋值给factory所以第一次调用后factory仍然是 NULL。第二次调用时factory NULL仍然成立于是又创建一个新实例同时又因为return提前退出cout照样不打印。对比MysqlFactory::GetInstance()的正确实现static IFactory *GetInstance(){ if (factory NULL ){ cout抽象工厂实例生成endl; factory new MysqlFactory(); } else { coutError, 抽象工厂实例已有endl; } return factory; }区别一目了然必须先给factory赋值再返回。SqlserverFactory那份代码的return写在了赋值之前导致单件完全失效——每一次调用都会产生新对象。这个细节恰恰说明了单件模式的本质不是写了私有构造函数就算单件必须保证“创建后立即保存引用后续复用同一引用”整个闭环才成立。4. 避坑指南单件与抽象工厂结合时的常见问题与排查4.1 现象GetInstance 每次都返回新对象如果你照着实验报告里SqlserverFactory的写法实现运行后发现打印了多次“抽象工厂实例生成”说明单件失效了。原因很明确return语句在赋值之前执行factory成员从未被写入。解决方法是把赋值语句移到return之前并删除提前返回的分支逻辑。4.2 现象构造函数私有后类外无法创建对象有些同学把构造函数设为 private 后发现连GetInstance()内部创建对象也编译不过。原因是 C 的访问控制是按类作用域来算的——GetInstance()是SqlserverFactory的静态成员函数它拥有访问私有构造函数的权限所以new SqlserverFactory()在该函数内是合法的。如果编译报错检查一下GetInstance是不是写成了非成员函数或者放在了别的类里。4.3 现象多线程环境下出现多个工厂实例懒汉式单件在单线程下没问题但两个线程同时第一次调用GetInstance()时可能同时判断factory NULL然后各自new一次最终得到两个实例。排查方法是在GetInstance入口加锁或者在 C11 及以后直接用 magic static——函数内的 static 局部变量初始化是线程安全的。4.4 现象析构函数把单件实例删了实验代码里定义了~SqlserverFactory(){coutSQL Server 抽象工厂实例删除endl;}如果有人对GetInstance返回的指针执行delete之后再用同指针调用CreateUser()就会触发未定义行为。解决方案是避免外部删除工厂指针正规做法是私有化析构函数或使用智能指针管理单件生命周期。5. 回归设计取舍抽象工厂嵌套单件究竟解决了什么、牺牲了什么回到最开始的问题——这个实验的价值不在代码能跑而在于它逼你回答“为什么不能更简单”。抽象工厂把产品族的创建策略封装成接口使得数据库切换对客户端来说是透明的操作单件模式把工厂的实例数量锁死成一个保证了全局策略的一致性。两者结合本质上是把“创建”这个行为从业务代码里彻底剥离让业务层只面对抽象接口。但代价也是明确的。第一个代价是类数量爆炸——每个产品族需要抽象产品、具体产品、抽象工厂、具体工厂四个层次这套实验里还只有三个产品、两种数据库类已经超过十个。如果产品数量翻倍类数量线性增长维护成本显著上升。第二个代价是扩展新数据库的代价不小每增加一种数据库需要新增三个具体产品类和一个具体工厂类同时还要保证新工厂正确注册到选择逻辑里。第三个代价是工厂层次与单件模式的耦合——单件锁死了工厂实例但如果未来要支持多租户、每个租户不同数据库配置这个“唯一实例”反而变成障碍。我一般会这么理解这个实验抽象工厂加单件是设计模式组合教学里最经典的一对搭档一个管“创建什么”一个管“创建几次”。但在真实工程里很少有系统直接裸用这种结构——因为现代框架的依赖注入和运行时配置已经把这个需求抽象到了更高层次。比如你只需要在配置文件里写一行db.typemysql框架扫描实现类自动装配哪还需要手写两个工厂类但从教学角度看手写一遍的意义在于让你亲自体会到“接口 多实现 实例管理”这条链路上每一个决策的代价。从那以后我每次设计多数据库支持系统都会先问一句是切换整个产品族还是只切换某个产品的实现如果是前者抽象工厂依然是最稳的底部架构。希望帮到你。本文还有配套的精品资源点击获取