设计模式之单例模式(二): 心得体会
设计模式之单例模式(一)-CSDN博客
目录
1.背景
2.分析
2.1.违背面向对象设计原则,导致职责混乱
2.2.全局状态泛滥,引发依赖与耦合灾难
2.3.多线程场景下风险放大,性能与稳定性受损
2.4.测试与维护难度指数级上升
2.5.违背 “最小知识原则”,代码可重用性极低
3.总结
1.背景
最新在做一个老项目的维护,里面对类的调用全部都是用的单例模式,我抽取了其中某一个类CTargetShowWidget,它的单实例模型关键部分代码如下:
//TargetShowWidget.h
class CTargetShowWidget : public QWidget
{
private:CTargetShowWidget(QWidget *parent = nullptr);public:~CTargetShowWidget();static CTargetShowWidget* getInstance();private:static CTargetShowWidget* m_pTargetShowWidget;
};//TargetShowWidget.cpp
CTargetShowWidget* CTargetShowWidget::m_pTargetShowWidget = NULL;CTargetShowWidget* CTargetShowWidget::getInstance()
{if (!m_pTargetShowWidget){m_pTargetShowWidget = new CTargetShowWidget(NULL);}return m_pTargetShowWidget;
}
初看好像没有什么问题,逻辑都是对的;但是细细品味,里面有几个关键的问题我们来一一说明。
2.分析
在程序中所有类都使用单实例模式(单例模式)会带来一系列设计和工程上的问题,违背面向对象设计的核心原则,严重影响代码的可维护性、扩展性和健壮性。
2.1.违背面向对象设计原则,导致职责混乱
1.单一职责原则被破坏
单例模式的核心是 “管理实例生命周期”+“实现业务逻辑”,二者强耦合在一个类中。当所有类都采用单例时,每个类不仅要处理自身业务,还要承担 “全局唯一实例” 的管理逻辑(如线程安全、实例销毁等),导致类的职责膨胀,代码复杂度飙升。
- 例:一个负责 “日志记录” 的单例类,本应专注于日志写入,却需要额外处理实例创建的线程同步逻辑,代码可读性和维护性下降。
2.开闭原则(扩展性)受损
单例模式通常通过 “私有化构造函数” 限制实例创建,这使得类难以被继承(子类无法调用父类私有构造函数),也无法在不修改原代码的前提下扩展功能(如创建单例的子类实例)。当所有类都被单例 “锁定” 时,后续需求变更(如需要多实例、子类扩展)会被迫修改底层代码,违反 “对扩展开放,对修改关闭” 的原则。
2.2.全局状态泛滥,引发依赖与耦合灾难
1.强耦合形成 “全局依赖网”
单例本质是 “全局变量的封装”,所有类的实例都是全局可访问的(通过静态接口获取),这会导致程序中充满隐性依赖—— 类 A 直接调用类 B 的单例实例,而类 B 可能又依赖类 C 的单例,形成复杂的 “全局依赖网”。
- 问题:依赖关系难以梳理,修改某个单例的接口或生命周期(如延迟初始化改为饿汉式),可能引发整个系统的连锁反应,调试时难以定位依赖源头。
2.内存与资源浪费,生命周期不可控
单例的实例通常在程序启动后长期存在(除非程序退出),即使某些类的功能在特定场景下才会使用。当所有类都是单例时,即使程序只用到 10% 的功能,也需要提前创建或保持 100% 的单例实例,导致内存占用过高,尤其对资源敏感的场景(如嵌入式、移动端)影响显著。
- 例:一个仅在用户点击 “设置” 时才用到的 “配置管理单例”,却在程序启动时就被创建,闲置内存直到程序结束。
3.全局状态破坏 “数据封装”
面向对象的核心是 “封装数据,暴露接口”,但单例的全局实例允许任何模块直接调用其方法、修改其状态,导致数据一致性难以保证(如多个模块同时修改单例的成员变量,引发竞态条件)。这种 “无边界的访问” 让程序变成 “不可控的全局状态机”,调试时难以追踪状态变化的源头。
4.内存泄露
如上面的代码肯定会出现内存泄露的。尤其是在动态库(如 Linux 下的 .so、Windows 下的 .dll)中写这样的类,因为动态库可以动态加载。当用dlopen加载动态库,然后调用CTargetShowWidget::getInstance()获取指针,用 dlclose卸载动态库时,CTargetShowWidget::getInstance()中new的内存是不会自动释放的,如果程序中不停的加载和卸载此动态库,加载过程中一直不停的分配内存,而卸载时候没有释放,这些内存会一直被进程占用,导致泄漏。
2.3.多线程场景下风险放大,性能与稳定性受损
1.线程安全成本激增
为保证单例在多线程环境下的唯一性,通常需要加锁(如 C++ 的std::mutex
、Java 的synchronized
)或使用原子操作。当所有类都是单例时,每个单例都可能成为线程竞争的 “锁热点”—— 尤其是频繁被调用的单例,加锁 / 解锁操作会带来显著的性能损耗,甚至引发死锁(若多个单例的锁顺序不一致)。
- 对比:非单例的普通类可通过 “局部变量” 或 “依赖注入” 在线程内独立使用,避免全局锁竞争。
2.销毁顺序与资源释放问题
单例的生命周期与程序一致,但其成员变量(如动态分配的内存、文件句柄、网络连接等)需要在程序退出时正确释放。当存在多个相互依赖的单例时,它们的销毁顺序无法保证(如单例 A 依赖单例 B 的数据,但若 B 先被销毁,A 销毁时可能访问到无效数据),导致崩溃或资源泄漏。
这种问题在 C++ 等没有自动垃圾回收的语言中尤为突出,即使在 Java 中,静态单例的销毁顺序也难以控制。
2.4.测试与维护难度指数级上升
1.单元测试无法隔离,结果不可靠
单例的全局状态会导致测试用例之间互相污染 —— 例如:
- 测试用例 1 修改了单例 A 的状态,测试用例 2 执行时依赖 A 的 “初始状态”,却拿到了被修改后的状态,导致测试失败。
- 无法模拟 “不同场景下的实例状态”,因为单例只有一个实例,难以注入测试数据(如模拟异常状态的单例)。
解决方式通常需要 “重置单例状态” 的额外接口,但这会进一步破坏封装性,且增加代码复杂度。
2.依赖注入失效,代码灵活性丧失
现代开发中,依赖注入(DI)是解耦的核心手段(如通过构造函数注入依赖的实例),但单例模式通过 “静态方法” 自我创建,无法被外部依赖替换(如单元测试时用 mock 对象替代真实单例)。当所有类都是单例时,程序完全丧失 “依赖替换” 的能力,只能硬编码依赖关系,难以适应需求变化(如切换底层实现、对接不同接口)。
2.5.违背 “最小知识原则”,代码可重用性极低
单例模式的 “全局访问” 特性,使得类与 “全局上下文” 强绑定 —— 一个单例类无法在另一个程序或模块中复用,除非接受其 “全局唯一” 的约束。而面向对象设计的目标之一是 “高内聚、低耦合”,让类可以像 “积木” 一样被复用,单例的全局化设计彻底破坏了这一点。
- 例:一个用于 “网络请求” 的单例类,若被设计为依赖全局的 “配置单例”,则无法在不包含该配置单例的项目中独立使用。
3.总结
单例模式本身是一种 “慎用的设计模式”,仅适用于明确需要全局唯一实例、且生命周期与程序一致的场景(如日志管理器、配置管理器)。当所有类都使用单例时,会将单例的缺点(全局状态、耦合、测试困难等)放大到极致,导致程序退化为 “面向过程的全局变量堆砌”,违背面向对象设计的核心思想。
优化建议:
1.优先使用 “普通类 + 依赖注入”:通过函数参数、上下文对象(如容器)传递实例,让依赖关系显性化,而非依赖全局访问;
2.仅在必要时使用单例:严格限制单例的适用场景(如真正需要 “全局唯一且生命周期与程序一致” 的场景,如日志器、全局配置),且每个系统中单例数量应控制在个位数;
3.用 “作用域唯一性” 替代 “全局唯一性”:若仅需在某个作用域(如线程内、函数内)保证唯一,可通过局部静态变量、线程本地存储(TLS)等更轻量的方式实现,避免全局化。
总之,设计模式是解决特定问题的工具,而非通用方案 —— 滥用单例模式的本质,是用 “便捷性” 牺牲 “可维护性”,最终会让程序付出更高的技术债务。
相关文章:
设计模式之单例模式(二): 心得体会
设计模式之单例模式(一)-CSDN博客 目录 1.背景 2.分析 2.1.违背面向对象设计原则,导致职责混乱 2.2.全局状态泛滥,引发依赖与耦合灾难 2.3.多线程场景下风险放大,性能与稳定性受损 2.4.测试与维护难度指数级上升 2.5.违背 “最小知识原…...
使用Python提取PDF元数据的完整指南
PDF文档中包含着丰富的元数据信息,这些信息对文档管理和数据分析具有重要意义。本文将详细介绍如何利用Python高效提取PDF元数据,并对比主流技术方案的优劣。 ## 一、PDF元数据概述 PDF元数据(Metadata)是包含在文档中的结构化信…...
uni-app学习笔记十八--uni-app static目录简介
本笔记内容摘录自工程简介 | uni-app官网 一个 uni-app 工程,就是一个 Vue 项目,在完成uni-app项目创建后,会生成一个static目录, 为什么需要static这样的目录? uni-app编译器根据pages.json扫描需要编译的页面&…...

阿里云ACP云计算备考笔记 (3)——云存储RDS
目录 第一章 云存储概览 1、云存储通用知识 ① 发展历史 ② 云存储的优势 2、云存储分类 3、文件存储业务场景 第二章 块存储 1、块存储分类 2、云盘的优势 3、创建云盘 4、管理数据盘 ① 格式化数据盘 ② 挂载数据盘 ③ 通过 API 挂载云盘 5、管理系统盘 ① 更…...

仓颉语言---Socket编程
一、什么是Socket编程? 1.定义 Socket(套接字)可以被理解为网络上两个进程之间通信的端点。它是网络通信的抽象表示,封装了底层网络协议的复杂性,为应用程序提供了一个简单统一的接口。 Socket 编程是一种网络编程范式…...
Mysql的B-树和B+树的区别总结
B 树也称 B- 树,全称为 多路平衡查找树,B 树是 B 树的一种变体。B 树和 B 树中的 B 是 Balanced(平衡)的意思。 目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 BTree 作为索引结构。 B 树& B 树两者有何异同呢&…...

【Java EE初阶 --- 多线程(初阶)】多线程的实现案例
乐观学习,乐观生活,才能不断前进啊!!! 我的主页:optimistic_chen 我的专栏:c语言 ,Java 欢迎大家访问~ 创作不易,大佬们点赞鼓励下吧~ 文章目录 前言单例模式实现单例模式…...

制作一款打飞机游戏64:关卡设计
今天我想完成第一个音乐循环的关卡设计。 初始设置 首先,我要删除所有之前创建的敌人和“大脑”(可能指敌人的行为模式或AI)。我不想保留它们,我要从零开始,重新创建敌人。但我会保留精灵(游戏中的角色或…...
开发常用的QT mql组件
Column Column 是一种将其子项沿单个列定位的类型。它是不使用锚点的情况下垂直定位一系列项目的便捷方式。 add : Transition bottomPadding : real leftPadding : real move : Transition padding : real populate : Transition rightPadding : real spacing : rea…...
Git操作记录
一.简单上传操作 1.Git 全局设置 git config --global user.name "xxx" git config --global user.email "xxx"2.创建新存储库 git clone gitgitlab.xxx.cn:xx/xxx/xxx.git cd test touch README.md git add README.md git commit -m "add README&qu…...
Vue Router的核心实现原理深度解析
1. Vue Router的基本架构 Vue Router的核心功能是实现前端路由,即在不重新加载页面的情况下更改应用的视图。它的基本架构包括: 路由配置:定义路径与组件的映射关系路由实例:管理路由状态和提供导航方法路由视图:渲染…...

Python趣学篇:用Pygame打造绚烂流星雨动画
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《Python星球日记》 目录 一、项目简介与效果展示二、技术栈与核…...
AI系统负载均衡与动态路由
载均衡与动态路由 在微服务架构中,负载均衡是实现服务高可用和性能优化的关键机制。传统负载均衡技术通常围绕请求数、连接数、CPU占用率等基础指标进行分发,而在AI系统中,特别是多模型、多异构算力(如CPU、GPU、TPU)共存的环境下,负载均衡不仅要考虑节点资源消耗,还需…...

山西省第十八届职业院校技能大赛 网络建设与运维赛项 样题
山西省第十八届职业院校技能大赛 网络建设与运维赛项 (学生组) 样题 2024 年 11 月 xx 日 2 赛题说明 一、竞赛项目简介 “网络建设与运维”竞赛共分为模块一:网络理论测试与网络 运维;模块二: 网络建设与调试&a…...
Stone 3D新版本发布,添加玩家控制和生物模拟等组件,增强路径编辑功能,优化材质编辑
后续版本号改为构建日期加小版本,所以最新版本为20250603.01 功能更新如下: 1. 改写fps-controls组件,简化游戏应用的创建,你只需要一个场景glb,然后给Scene节点添加fps-controls组件,即可完成一个第一人…...
【Qt】之【Get√】【Bug】通过值捕获(或 const 引用捕获)传进 lambda,会默认复制成 const
通过值捕获(或 const 引用捕获)传进 lambda,会默认复制成 const。 背景 匿名函数外部定义 QSet<QString> nameSet,需要传入匿名函数使用修改 connect(dlg, ..., [nameSet](...) {nameSet.insert(name); // ❌ 这里其实是 const QSet…...
排序算法C语言实现
算法概览 排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性适用场景插入排序O(n)O(n)O(1)稳定小规模/基本有序希尔排序O(n log n)O(n)O(1)不稳定中等规模冒泡排序O(n)O(n)O(1)稳定教学/小规模堆排序O(n log n)O(n log n)O(1)不稳定大规模数据选择排序O(n)O(n)O(1)不稳定…...

Python----目标检测(训练YOLOV8网络)
一、数据集标注 在已经采集的数据中,使用labelImg进行数据集标注,标注后的txt与原始 图像文件同名且在同一个文件夹(data)即可。 二、制作数据集 在data目录的同目录下,新建dataset目录,以存放制作好的YOLO…...

构建 MCP 服务器:第一部分 — 资源入门
什么是模型上下文协议? 模型上下文协议(MCP) 是Claude等大型语言模型 (LLM) 与外部数据和功能安全交互的标准化方式。您可以将其想象成一个平视显示器,或者 AI 的 USB 端口——它提供了一个通用接口,允许任何兼容 MCP 的 LLM 连接到您的数据和工具。 MCP 提供了一个集中式协…...
c# :this() 和 :base()区别
在 C# 中,:this() 和 :base() 都用于构造函数的重载和继承,但它们有不同的用途和上下文: 1. :this() 用途:用于调用当前类中的其他构造函数(构造函数重载)。场景:当你希望一个构造函数先执行另…...

使用ZYNQ芯片和LVGL框架实现用户高刷新UI设计系列教程(第十五讲)
这一期讲解lvgl中日历控件的基础使用,Calendar 部件是一个经典日历,它具有以下功能:• 通过一个7x7矩阵显示任何月份 • 显示日期名称 • 突出显示当前日期(今天) • 突出显示任何用户定义的日期 日历是一个可编辑的小…...

Vue中实现表格吸底滚动条效果,列太多时左右滚动条始终显示在页面中
1、安装 npm install el-table-horizontal-scroll 2、全局注册(main.js) import horizontalScroll from el-table-horizontal-scrollVue.use(horizontalScroll) 如下图,在main.js加上上面的代码 3、表格内引用 <el-table :data"…...

BeeWorks 协同办公能力:局域网内企业级协作的全场景重构
在企业数字化办公场景中,BeeWorks 以强大的协同办公能力,将局域网内的通讯、协作、业务流程整合为统一整体。作为专注于企业级局域网环境的协作平台,其不仅提供即时通讯基础功能,更通过办公工具集成、会议能力强化、业务系统对接等…...
Mermaid 绘图--以企业权限视图为例
文章目录 一、示例代码二、基础结构设计2.1 组织架构树2.2 权限视图设计 三、销售数据权限系统四、关键语法技巧汇总 一、示例代码 在企业管理系统开发中,清晰的权限视图设计至关重要。本文将分享如何使用 Mermaid 绘制直观的企业权限关系图,复制以下代…...
Redis(02)Win系统如何将Redis配置为开机自启的服务
一、引言 Redis 是一款高性能的键值对存储数据库,在众多项目中被广泛应用。在 Windows 环境下,为了让 Redis 能更稳定、便捷地运行,将其设置为系统服务并实现自动启动是很有必要的。这样一来,系统开机时 Redis 可自动加载…...

C++课设:高效的日程管理系统
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、C日程管理系统的时代价值1. 为什么选…...

功能测试、性能测试、安全测试详解
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 一、功能测试 1、单接口功能 手工测试中的单个业务模块,一般对应一个接口 例如: 登录业务------登录接口 加入购物车业务------加入购…...

提示词指南 --- 提示词的基本结构
提示词指南 --- 提示词的基本结构以及三种角色 什么是Prompt (提示词)Prompt的基本结构和三种角色提示词的三种核心“角色”(Role) 真实例子 什么是Prompt (提示词) 我们可以把“Prompt(提示词)”想象成和AI聊天时你说的“一句话…...
UI学习—cell的复用和自定义cell
前言 Nib是什么? Nib就是.xib文件:一个可视化的UI界面文件,它记录了一个UI组件(例如一个表格单元格Cell)的界面布局信息,可以在interfaceBuilder中创建 [UINib nibWithNibName:"CustomCell" b…...

20250605使用boot-repair来恢复WIN10和ubuntu22.04.6双系统的启动
rootrootrootroot-X99-Turbo:~$ sudo apt-get install boot-repair rootrootrootroot-X99-Turbo:~$ sudo add-apt-repository ppa:yannubuntu/boot-repair rootrootrootroot-X99-Turbo:~$ sudo apt-get install boot-repair 20250605使用boot-repair来恢复WIN10和ubuntu22.04.6…...