抽象工厂+单件模式:设计模式组合的工程实践与避坑指南

发布时间:2026/10/25 2:16:03
抽象工厂+单件模式:设计模式组合的工程实践与避坑指南 简介这份PDF实验报告面向软件工程、计算机专业学生及正在学习设计模式的中级开发者聚焦抽象工厂模式与单件模式的编程实现帮助读者理解多产品族对象的创建与工厂实例唯一性管理。报告以公司数据库模拟系统为场景涵盖用户、部门、项目三张表的查询与添加操作并支持MySQL与SQL Server两种数据库切换。资源包共1个PDF文件大小约301KB内容包含实验目的与要求、抽象工厂与单件模式原理、UML图、IFactory抽象工厂接口、IUser/IDepartment/IProject抽象产品类、Sqlserver与Mysql具体实现类以及具体工厂类代码分析结构完整、便于对照学习。目前已有122人学习。读者可从中获得设计模式落地的完整实验思路、接口与类层次划分方法以及结合单件模式管理工厂实例的参考实现适合作为课程实验报告撰写与设计模式复习的参考材料。1. 抽象工厂加单件为什么“多一个 new”就是设计事故很多人第一次看到“抽象工厂 单件”的组合会觉得这是两个设计模式的机械拼盘一个负责造对象一个负责保证唯一凑在一起无非是实验课凑字数。但真正把这段模拟代码跑一遍你会看到一个反直觉的现象——第二次调用MysqlFactory::GetInstance()时程序打印的是“Error, 抽象工厂实例已有”而不是再给你一个工厂。这个“拒绝创建”的行为恰恰是整个设计能 work 的关键。它解决的问题很具体一个应用要同时支持 MySQL 和 SQL Server 两套数据库用户表、部门表、项目表三张表各有查询和添加两种操作如果客户端到处new SqlserverUser()、new MysqlDepartment()数据库类型就散落在业务代码里切换一次数据库等于全量翻车。抽象工厂把“一族产品”的创建收口到一个工厂接口单件模式再把“一族只该有一个工厂”这件事钉死。搞懂这两层为什么必须叠在一起、单件那几行static到底在防什么比背下 UML 图有用得多。2. 抽象工厂为什么产品族必须由一个工厂统一发货2.1 产品族与产品等级结构先把两个维度分清这段实验代码里有两个容易混淆的维度。第一个维度是“产品等级结构”IUser、IDepartment、IProject是三个抽象产品各自代表一类业务实体第二个维度是“产品族”SqlserverUser、SqlserverDepartment、SqlserverProject构成 SQL Server 族MysqlUser、MysqlDepartment、MysqlProject构成 MySQL 族。抽象工厂模式约束的是同一个族的产品必须由同一个具体工厂创建。SqlserverFactory只造 SQL Server 族的三件套MysqlFactory只造 MySQL 族的三件套。这个约束看起来只是代码组织问题实际上是一致性约束——如果客户端用MysqlFactory造了用户对象却用SqlserverFactory造了部门对象运行时不会报类型错误因为两者都实现了IDepartment但语义上已经是一套混搭的脏数据。抽象工厂用“一个工厂管一族”从结构上堵死了这种混搭的可能。常见的实现思路是让抽象工厂接口只暴露创建方法不暴露任何状态// 抽象工厂只声明能造什么不关心怎么造 class IFactory { public: virtual ~IFactory() { cout 抽象工厂实例删除 endl; } virtual IUser* CreateUser() 0; // 造用户 virtual IDepartment* CreateDepartment() 0; // 造部门 virtual IProject* CreateProject() 0; // 造项目 };三个纯虚函数对应三个产品等级结构返回值都是抽象产品指针。客户端拿到的是IUser*调Insert和GetUser时走虚函数分发具体是 MySQL 还是 SQL Server 的实现在编译期就解耦了。参数上没有任何数据库类型标识因为“选哪个族”这件事已经在“选哪个工厂”时决定了工厂本身就是类型选择器。2.2 具体工厂如何把“选型”推迟到运行时具体工厂的实现非常薄薄到很多人觉得没必要class MysqlFactory : public IFactory { public: IUser* CreateUser() { return new MysqlUser(); } IDepartment* CreateDepartment() { return new MysqlDepartment(); } IProject* CreateProject() { return new MysqlProject(); } // ... 单件相关成员见下一章 };每个方法只有一行new但这一行new的位置很讲究。它把“具体类名”关在了工厂内部客户端代码里永远不出现MysqlUser这个字符串。这意味着切换数据库时改动点收敛到“客户端拿哪个工厂”这一处而不是散落在几十个new上。这就是抽象工厂相对简单工厂的核心收益简单工厂是一个工厂造所有产品加一个产品族要改工厂的switch抽象工厂是每个族一个工厂加族只需加一个工厂类符合开闭原则。代价也很明显加一个产品等级结构比如新增“日志表”时IFactory要加一个纯虚函数所有具体工厂都得跟着改。这是抽象工厂的经典取舍——对族扩展开放对产品等级扩展封闭。实验里三张表是固定的所以这个代价没暴露出来但真实项目里选型时必须先想清楚未来是加数据库类型多还是加业务表多。2.3 客户端只认接口不认具体类看客户端代码main里从头到尾没有出现过任何具体产品类名IFactory* factory1 MysqlFactory::GetInstance(); IUser* iu factory1-CreateUser(); iu-Insert(user); // 虚函数分发到 MysqlUser::Insert iu-GetUser(1); // 虚函数分发到 MysqlUser::GetUser IDepartment* idept factory1-CreateDepartment(); idept-Insert(dept);factory1的静态类型是IFactory*iu的静态类型是IUser*。整条调用链上客户端只知道“我要一个用户操作对象”不知道也不关心它是 MySQL 还是 SQL Server 实现。这种依赖倒置是抽象工厂能支撑“运行时切换数据库”的根本原因——切换只需要换一个工厂指针后面的代码一行不动。提示抽象工厂返回的裸指针在这里没有释放实验代码为了简洁省略了delete。真实项目里要么用智能指针要么在工厂析构时统一回收否则每切一次数据库就漏一族对象。3. 单件模式为什么工厂的构造函数必须藏进 private3.1 唯一性不是靠约定是靠编译器强制实验总结里有一句话很关键“具体工厂的构造函数需要放在 private 中这样客户端不能随意创建多个工厂。”这句话点出了单件模式的本质——唯一性不能靠文档约定“请大家只 new 一次”必须让new在语法层面就编译不过。class MysqlFactory : public IFactory { public: static IFactory* GetInstance() { if (factory NULL) { cout 抽象工厂实例生成 endl; factory new MysqlFactory(); } else { cout Error, 抽象工厂实例已有 endl; } return factory; } private: MysqlFactory() {} // 构造函数私有外部无法 new static MysqlFactory* factory; // 静态成员持有唯一实例 }; MysqlFactory* MysqlFactory::factory NULL; // 类外初始化构造函数私有化之后客户端写new MysqlFactory()直接编译报错唯一的入口只剩GetInstance()。静态成员factory是类级别的不属于任何对象所以能在没有实例的情况下被访问。类外那行 NULL初始化不能省——静态成员变量必须在类外定义并初始化否则链接阶段会报未定义符号这是 C 单件模式最常见的翻车点之一。3.2 懒汉式与“先判断再创建”的时序这段实现是典型的懒汉式第一次调用GetInstance()时才创建实例之后直接返回。判断逻辑是if (factory NULL)创建后赋值给factory下次进来走else分支打印错误。注意实验代码里SqlserverFactory::GetInstance有一个细节差异return new SqlserverFactory();写在cout之前而且没有把结果赋给factory这其实是个 bug——它每次都会 new 一个新对象然后立刻丢弃factory永远是 NULL。对比MysqlFactory的写法// MysqlFactory 的正确写法先赋值给静态成员再返回 factory new MysqlFactory(); return factory;这个对比值得单独拎出来说因为它暴露了单件模式最容易踩的坑创建和赋值必须是同一个动作。如果只return new不赋值单件就退化成了“每次调用都造新对象”唯一性完全失效而且因为构造函数私有外部还察觉不到。实验报告里 MySQL 那部分能正确打印“Error”SQL Server 那部分如果真跑起来会一直打印“实例生成”这就是血泪经验。3.3 单件作用域为什么 MySQL 工厂和 SQL Server 工厂可以各有一个实验总结第一条特别强调“只能对自身单件即生成一个 MySQL 工厂时不能再次生成第二个 MySQL 工厂但是可以生成 SQL Server 工厂。”这句话容易被忽略但它定义了单件的作用域边界。MysqlFactory::factory和SqlserverFactory::factory是两个独立的静态成员各自管各自的唯一性。所以系统里最多存在两个工厂实例一个 MySQL 工厂、一个 SQL Server 工厂。这不是设计缺陷而是刻意为之——如果全局只允许一个工厂那“同时支持两种数据库”就无从谈起了。单件约束的是“每个具体工厂类唯一”不是“所有工厂全局唯一”。这个边界在真实项目里对应的是“每个数据库连接池一个实例”的语义MySQL 连接池单件、SQL Server 连接池单件互不干扰。理解这一点才不会在扩展第三种数据库时误以为要改单件逻辑——加一个OracleFactory它自带一个静态factory天然就是第三个单件。4. 两者叠加工厂单件化之后对象生命周期怎么走4.1 从 GetInstance 到 CreateUser 的完整调用链把两个模式串起来看一次完整的“添加用户”操作经过这些环节// 第一步拿工厂单件首次调用触发创建 IFactory* f MysqlFactory::GetInstance(); // 第二步工厂造产品每次调用都是新对象 IUser* u f-CreateUser(); // 第三步通过抽象接口操作 u-Insert(user);第一步的GetInstance保证工厂全局唯一第二步的CreateUser每次返回新的MysqlUser实例。这里有个关键区分工厂是单件产品不是单件。工厂持有的是“创建能力”产品持有的是“一次操作的数据”如果把产品也做成单件多个请求并发操作同一个MysqlUser对象就会互相污染。实验代码里CreateUser每次都new正是这个语义。常见的实现思路是把工厂单件的生命周期和程序对齐进程启动后第一次用到时创建进程退出时随静态对象析构。实验代码里~MysqlFactory()打印“实例删除”但因为factory是裸指针且从未delete这个析构其实不会触发——这是教学代码的简化真实项目里要么用static MysqlFactory instance;的 Meyers 单件让析构自动发生要么显式管理。4.2 静态成员的定义与初始化顺序陷阱MysqlFactory* MysqlFactory::factory NULL;这行代码放在类外属于静态存储期对象。C 标准保证静态存储期对象在main之前完成零初始化但动态初始化比如这里显式赋 NULL的顺序在同一编译单元内按声明顺序跨编译单元则未定义。这段代码里赋的是NULL属于常量初始化没有顺序问题。但如果哪天有人把factory初始化成new MysqlFactory()就会踩到“静态初始化顺序灾难”——另一个编译单元里的静态对象如果在构造时调用GetInstance()可能拿到还没初始化的factory。规避办法就是保持懒汉式静态成员只做零初始化真正的创建推迟到第一次GetInstance()调用此时所有静态初始化都已完成。注意多线程环境下if (factory NULL)这个判断不是原子的两个线程同时进来可能各造一个实例。实验是单线程模拟没暴露这个问题生产代码要么加锁要么用 C11 的static局部变量语言保证线程安全初始化。4.3 用表格看清“谁唯一、谁不唯一”对象是否单件生命周期说明MysqlFactory是进程级静态成员持有全局唯一SqlserverFactory是进程级独立静态成员与 MySQL 工厂互不影响MysqlUser否每次CreateUser新建承载单次操作数据MysqlDepartment否每次CreateDepartment新建同上MysqlProject否每次CreateProject新建同上这张表是理解整个设计的钥匙。很多人把“单件模式”和“工厂模式”混着记以为工厂造出来的东西也是单件结果在产品类上也加静态成员导致并发场景下数据串台。记住单件约束的是创建者不是被创建者。5. 避坑与排查这段模拟代码最容易翻车的五个地方5.1 现象第二次 GetInstance 没有打印 Error反而又造了一个原因GetInstance里写成了return new MysqlFactory();而没有先赋值给静态成员factory。每次调用都走factory NULL分支因为factory从未被更新。解决创建和赋值合并先factory new MysqlFactory();再return factory;。改完后第二次调用会进else分支。这个坑在实验代码的 SQL Server 版本里真实存在对照 MySQL 版本就能看出差异。5.2 现象链接阶段报undefined reference to MysqlFactory::factory原因静态成员变量只在类内声明了static MysqlFactory* factory;没有在类外定义。类内声明只是告诉编译器“有这么个符号”不分配存储。解决在类外补上MysqlFactory* MysqlFactory::factory NULL;。注意这行不能写在头文件里否则多个编译单元包含时会重复定义应该放在对应的.cpp文件里。5.3 现象客户端new MysqlFactory()编译报错提示构造函数不可访问原因构造函数被放进了private这是单件模式的预期行为不是 bug。解决不要试图把构造函数改回public来“方便测试”。正确做法是通过GetInstance()获取实例如果测试需要重置单件加一个static void Reset()在测试代码里调用而不是破坏封装。5.4 现象切换数据库后部分表还是旧数据库的行为原因客户端某处直接new了具体产品类绕过了工厂。比如混入了new SqlserverUser()而其他对象走的是MysqlFactory。解决全局搜索具体产品类名SqlserverUser、MysqlUser等确保它们只出现在工厂的CreateXxx方法里。客户端代码里出现具体产品类名就是抽象工厂被架空的信号。5.5 现象程序退出时没有打印“抽象工厂实例删除”原因factory是裸指针从未delete析构函数自然不触发。实验代码为了演示单件逻辑省略了回收。解决如果要在退出时清理可以在GetInstance配套一个static void Destroy()或者改用static MysqlFactory instance;的 Meyers 单件写法让静态对象在程序结束时自动析构。注意 Meyers 单件在 C11 之后自带线程安全初始化是更省心的选择。6. 设计取舍单件工厂的边界在哪里什么时候不该用把这段实验代码放到真实工程语境里最值得琢磨的不是“怎么写出单件”而是“单件工厂的边界该划在哪”。实验里每个具体工厂各自单件这个选择背后有一个隐含假设一个进程内每种数据库只需要一个工厂。这个假设在模拟场景成立但在需要多数据源、多租户的系统里就会失效——同一个进程可能要连两个 MySQL 实例此时MysqlFactory单件反而成了枷锁你得把“工厂”和“数据源标识”绑定单件的作用域从“类”下沉到“类 配置键”。另一个取舍是抽象工厂对产品等级扩展的封闭性。实验固定三张表加表就要改IFactory和所有具体工厂这个成本在小规模下可接受但如果业务表有几十张且频繁增减抽象工厂的接口会膨胀得难以维护这时候常见的替代思路是用反射或注册表把“产品创建”数据化工厂只保留一个泛化的Create(string type)。代价是失去编译期类型检查换来扩展性——没有免费的设计只有匹配场景的设计。验证这套设计是否真的解耦我一般会做一个“替换实验”把客户端里的MysqlFactory::GetInstance()换成SqlserverFactory::GetInstance()如果除了这一行之外没有任何改动就能编译通过并跑通说明抽象工厂的依赖倒置是干净的如果冒出十几处编译错误说明具体类名泄漏到了业务层抽象白做了。这个实验比看 UML 图直观得多也是我每次评审工厂相关代码时必走的流程。从那以后我每次写工厂类都会先问自己一句这个单件约束的是“创建者唯一”还是“产品唯一”想清楚这一层才不会在并发和扩展时反复踩坑。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询