设计模式之单例模式(二): 心得体会

设计模式之单例模式(一)-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)等更轻量的方式实现,避免全局化。

        总之,设计模式是解决特定问题的工具,而非通用方案 —— 滥用单例模式的本质,是用 “便捷性” 牺牲 “可维护性”,最终会让程序付出更高的技术债务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/82638.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

windows10 php报错

参考这个, 实际解决了问题, 主要是repair c 然后重启 【BUG】PHP Warning: ‘C:\\WINDOWS\\SYSTEM32\\VCRUNTIME140.dll‘ 14.0 is not compatible with this PHP bu_php warning: vcruntime140.dll 14.0 is not compat-CSDN博客

GPU显存的作用和如何选择

核心定义与作用 首先,显存的全称是显示内存,英文是Video RAM或VRAM,是显卡上的专用内存。 显存的主要作用是用来存储图形处理单元(GPU)需要处理的数据,比如纹理、顶点数据、帧缓冲区等。 数据中转站 GPU…

从零开始:用Tkinter打造你的第一个Python桌面应用

目录 一、界面搭建:像搭积木一样组合控件 二、菜单系统:给应用装上“控制中枢” 三、事件驱动:让界面“活”起来 四、进阶技巧:打造专业级体验 五、部署发布:让作品触手可及 六、学习路径建议 在Python生态中,Tkinter就像一把瑞士军刀,它没有花哨的特效,却能快速…

Unity基础-Mathf相关

Unity基础-Mathf相关 一、Mathf数学工具 概述 Mathf是Unity中封装好用于数学计算的工具结构体,提供了丰富的数学计算方法,特别适用于游戏开发场景。它是Unity开发中最常用的数学工具之一,能够帮助我们处理各种数学计算和插值运算。 Mathf…

Android Studio 之基础代码解析

1、 onCreate 在 Android 开发中,MainActivity 作为应用的入口 Activity,其 onCreate() 方法是生命周期中第一个且最重要的回调方法,负责初始化核心组件和界面。以下是其核心要点: 一、基本定义与作用 调用时机 当 Activity 首次…

AIGC图像去噪:核心原理、算法实现与深度学习模型详解

1. 背景概述 1.1 目标与范畴 在AIGC(人工智能生成内容) 的技术生态系统中,图像生成模型(如生成对抗网络GAN、扩散模型Diffusion Model)所产出的视觉内容,其质量常因训练数据中的固有瑕疵、生成过程中的随机扰动或数据传输期间的信号衰减而呈现出不同程度的退化。因此,…

电路图识图基础知识-自耦变压器降压启动电动机控制电路(十六)

自耦变压器降压启动电动机控制电路 自耦变压器降压启动电动机控制电路是将自耦变压器的原边绕组接于电源侧,副边绕组接 于电机侧。电动机定子绕组启动时的电压为自耦变压器降压后得到的电压,这样可以减少电动 机的启动电流和启动力矩,当电动…

Life:Internship finding

1. 前言 fishwheel writes this Blog to 记录自分自身在研二下找实习的经历。When 写这篇 Blog 的时候我的最后一搏也挂掉了,只能启用保底方案了。When I 打开我的邮箱时,发现里面有 nearly 100 多封与之相关的邮件,顿时感到有些心凉&#x…

Redis 常用数据类型和命令使用

目录 1 string 2 hash 3 list 4 set集合 5 zset有序集合 1 string 值可以是字符串、数字和二进制的value&#xff0c;值最大不能超过512MB 应用场景&#xff1a; 应用程序缓存 计数器 web共享session 限速 1.1 设置单个键值 set <key> value [EX seconds|PX…

Spring Boot缓存组件Ehcache、Caffeine、Redis、Hazelcast

一、Spring Boot缓存架构核心 Spring Boot通过spring-boot-starter-cache提供统一的缓存抽象层&#xff1a; #mermaid-svg-PW9nciqD2RyVrZcZ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-PW9nciqD2RyVrZcZ .erro…

【photoshop】专色浓度和专色密度

1.1 专色浓度 是图层填充到专色前&#xff0c;设置的前景色CMYK的K值。填充到专色后&#xff0c;可以查看到专色中图层的k值。 ps前景色填充快捷键 1.Windows 系统&#xff1a;Alt Delete&#xff1b;2.Mac 系统&#xff1a;Option Delete。 1.2专色密度 专色的属性&…

用电脑控制keysight示波器

KEYSIGHT示波器HD304MSO性能 亮点&#xff1a; 体验 200 MHz 至 1 GHz 的带宽和 4 个模拟通道。与 12 位 ADC 相比&#xff0c;使用 14 位模数转换器 &#xff08;ADC&#xff09; 将垂直分辨率提高四倍。使用 10.1 英寸电容式触摸屏轻松查看和分析您的信号。捕获 50 μVRMS …

leetcode hot100刷题日记——33.二叉树的层序遍历

解题总结二维vector的初始化方法 题目描述情况1&#xff1a;不确定行数和列数情况2&#xff1a;已知行数和列数情况3&#xff1a;已知行数但不知道列数情况4&#xff1a;已知列数但不知道行数 题目描述 解答&#xff1a;用队列 思路都差不多&#xff0c;我觉得对于我自己来说&a…

微服务面试资料1

在当今快速发展的技术领域&#xff0c;微服务架构已经成为构建复杂系统的重要方式之一。本文将围绕微服务的核心概念、技术栈、分布式事务处理、微服务拆分与设计&#xff0c;以及敏捷开发实践等关键问题展开深入探讨&#xff0c;旨在为准备面试的 Java 开发者提供一份全面的复…

【设计模式-4.8】行为型——中介者模式

说明&#xff1a;本文介绍行为型设计模式之一的中介者模式 定义 中介者模式&#xff08;Mediator Pattern&#xff09;又叫作调节者模式或调停者模式。用一个中介对象封装一系列对象交互&#xff0c;中介者使各对象不需要显式地互相作用&#xff0c;从而使其耦合松散&#xf…

Oracle 的 SEC_CASE_SENSITIVE_LOGON 参数

Oracle 的SEC_CASE_SENSITIVE_LOGON 参数 关键版本信息 SEC_CASE_SENSITIVE_LOGON 参数在以下版本中被弃用&#xff1a; Oracle 12c Release 1 (12.1)&#xff1a; 该参数首次被标记为"过时"(obsolete)但依然保持功能有效 Oracle 18c/19c 及更高版本&#xff1a; …

《图解技术体系》How Redis Architecture Evolves?

Redis架构的演进经历了多个关键阶段&#xff0c;从最初的内存数据库发展为支持分布式、多模型和持久化的高性能系统。以下为具体演进路径&#xff1a; 单线程模型与基础数据结构 Redis最初采用单线程架构&#xff0c;利用高效的I/O多路复用&#xff08;如epoll&#xff09;处…

【电赛培训课】测量与信号类赛题分析

一、赛题基本情况及硬件电路准备 &#xff08;一&#xff09;赛题基本情况 1.测量与信号类赛题统计 2.测量与信号类赛题特点 &#xff08;二&#xff09;硬件电路准备 综测环节不允许带入电脑和手机&#xff0c;需要自己根据题目要求和芯片参数指标进行设计和计算&#xff0c…

移动AI神器GPT Mobile:多模型自由切换

GPT Mobile是什么 GPT Mobile是一款开源的本地移动部署AI工具,主要用于安卓设备。以下是其相关介绍: 功能特点 多模型交互:支持与多个大型语言模型(LLM)同时进行对话,用户导入相应的API密钥,就可连接OpenAI、Anthropic、Google、Ollama等平台,还能根据需求自由切换不同…

AirSim/Cosys-AirSim 游戏开发(二)使用自定义场景

在实际的开发过程中很少会只用 AirSim 自带的 Blocks 场景&#xff0c;通常需要用到自定义的一些环境和模型&#xff0c;依托于强大的 UE 引擎可以较为逼真地完成场景渲染。这篇博客记录了如何从头开始导入一个自定义场景并加载 AirSim 插件。 【Note】&#xff1a;由于 UE Ed…