☰
抽象工厂与单件模式:数据库切换场景下的双模式嵌套解析
2026/10/24 20:54:17 网站建设 项目流程

简介:软件构造课程中抽象工厂模式与单件模式的编程实现实验,由徐奕(学号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()返回MysqlUser,CreateDepartment()返回MysqlDepartment,CreateProject()返回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 { cout<<"Error, 抽象工厂实例已有!"<<endl; return factory; } }

这段代码有一个明显的 bug:return 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 { cout<<"Error, 抽象工厂实例已有!"<<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入口加锁,或者在 C++11 及以后直接用 magic static——函数内的 static 局部变量初始化是线程安全的。

4.4 现象:析构函数把单件实例删了

实验代码里定义了~SqlserverFactory(){cout<<"SQL Server 抽象工厂实例删除!"<<endl;},如果有人对GetInstance返回的指针执行delete,之后再用同指针调用CreateUser()就会触发未定义行为。解决方案是避免外部删除工厂指针,正规做法是私有化析构函数或使用智能指针管理单件生命周期。

5. 回归设计取舍:抽象工厂嵌套单件,究竟解决了什么、牺牲了什么

回到最开始的问题——这个实验的价值不在代码能跑,而在于它逼你回答“为什么不能更简单”。抽象工厂把产品族的创建策略封装成接口,使得数据库切换对客户端来说是透明的操作;单件模式把工厂的实例数量锁死成一个,保证了全局策略的一致性。两者结合,本质上是把“创建”这个行为从业务代码里彻底剥离,让业务层只面对抽象接口。

但代价也是明确的。第一个代价是类数量爆炸——每个产品族需要抽象产品、具体产品、抽象工厂、具体工厂四个层次,这套实验里还只有三个产品、两种数据库,类已经超过十个。如果产品数量翻倍,类数量线性增长,维护成本显著上升。第二个代价是扩展新数据库的代价不小:每增加一种数据库,需要新增三个具体产品类和一个具体工厂类,同时还要保证新工厂正确注册到选择逻辑里。第三个代价是工厂层次与单件模式的耦合——单件锁死了工厂实例,但如果未来要支持多租户、每个租户不同数据库配置,这个“唯一实例”反而变成障碍。

我一般会这么理解这个实验:抽象工厂加单件,是设计模式组合教学里最经典的一对搭档,一个管“创建什么”,一个管“创建几次”。但在真实工程里,很少有系统直接裸用这种结构——因为现代框架的依赖注入和运行时配置已经把这个需求抽象到了更高层次。比如你只需要在配置文件里写一行db.type=mysql,框架扫描实现类自动装配,哪还需要手写两个工厂类?但从教学角度看,手写一遍的意义在于:让你亲自体会到“接口 + 多实现 + 实例管理”这条链路上每一个决策的代价。从那以后我每次设计多数据库支持系统,都会先问一句:是切换整个产品族,还是只切换某个产品的实现?如果是前者,抽象工厂依然是最稳的底部架构。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询