设计模式学习笔记 - 设计模式与范式 - 创建型:2.单例模式(中):为什么不推荐使用单例模式?又有何替代方案?

前言

尽管单例是一个很常用的实际模式,在实际的开发中,也经常使用,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用。所以,今天就针对这个说法详细地讲讲。

  • 单例模式有哪些问题?为什么被称为反模式?
  • 如果不用单例,该如何表示全局唯一类?有何替代的解决方案?

单例模式有哪些问题?

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写间接、使用简单,在代码中我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码,会带来诸多问题。

1. 单例对 OOP 特性不太友好

OOP 的四大特性是是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都是支持不友好的。为什么这么说呢? 还是通过 IdGenerator 例子来讲解。

public class Order {public void create(...) {// ...long id = IdGenerator.getInstance().getId();// ...}
}public class User {public void create(...) {// ...long id = IdGenerator.getInstance().getId();// ...}
}

IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求的变化,我们需要所有用到 IdGenerator 类的地方,这样的代码改动会比较大。

public class Order {public void create(...) {// ...long id = IdGenerator.getInstance().getId();// 将上面一行替换为下面一行代码long id = OrderIdGenerator.getInstance().getId();// ...}
}public class User {public void create(...) {// ...long id = IdGenerator.getInstance().getId();// 将上面一行替换为下面一行代码long id = UserIdGenerator.getInstance().getId();// ...}
}

此外,单例对继承、多态的支持也不友好。一旦你选择将某个类设计成单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

2.单例会隐藏类之间的依赖关系

我们知道,代码的可读性非常重要。在阅读代码的时候,我们希望一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐藏。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

3.单例对代码的扩展性不友好

单例类只能有一个对象实例。如果未来某一天,我们需要再代码中创建两个或多个实例,那就要对代码有比较大的改动。

可能,你会想,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么互需要两个或多个实例呢?
在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便控制数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后,我们发现,系统中有些 SQL 语句运行的非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。

如果,我们将数据库连接池设计成单例类,显然无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池确实没有涉及成单例类。

4.单例对代码的可测试不友好

单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。

此外,如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那在我们编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的一个成员变量的值,从而导致测试结果互相影响的问题。关于这一点,你可以回过头去看下《规范与重构 - 3.什么是代码的可测试性?如何写出可测试性好的代码?》中的 “其他场景的 Anti-Patterns:全局变量” 那部分的代码示例和讲解。

5.单例不支持有参数的构造函数

单例类不支持有参的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。针对这个问题,我们来看下有哪些解决方案。

第一种解决思路:创建完实例后,再调用 init() 函数传递参数。需要注意的是,我们在使用这个单例类的时候,要先调用 init() 方法,然后才能调用 getInstance() 方法,否则代码会抛出异常。具体的代码实现如下所示:

public class Singleton {private static Singleton instance = null;private final int paramA;private final int paramB;private Singleton(int paramA, int paramB) {this.paramA = paramA;this.paramB = paramB;}public static Singleton getInstance() {if (instance == null) {throw new RuntimeException("Run init() first.");}return instance;}public synchronized static Singleton init(int paramA, int paramB) {if (instance != null) {throw new RuntimeException("Singleton has been created!");}instance = new Singleton(paramA, paramB);return instance;}
}Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();

第二种解决思路:将参数放到 getInstance() 方法中。

public class Singleton {private static Singleton instance = null;private final int paramA;private final int paramB;private Singleton(int paramA, int paramB) {this.paramA = paramA;this.paramB = paramB;}public synchronized static Singleton getInstance(int paramA, int paramB) {if (instance == null) {instance = new Singleton(paramA, paramB);}return instance;}
}

不知道你有没有发现,上面的代码其实有点问题。如果我们如下代码,两次执行 getInstance() ,那获取到的 singleton1singleton2paramAparamB 都是 10 和 50。也就是说,第二次的参数 (20, 30) 没有起作用,而构建的过程没有给与提示,这样就会误导用户。

Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);

第三种解决思路:将参数放到另一个全局变量中。具体的实现代码如下所示。Config 是一个存储了 paramAparamB 值的全局变量。里面的值就可以像下面的代码那样通过静态常量来定义,也可以从配置文件中加载得到。实际上,这种方式是比较推荐的。

public class Config {public static final int PARAM_A = 123;public static final int PARAM_B = 456;
}public class Singleton {private static Singleton instance = null;private final int paramA;private final int paramB;private Singleton() {this.paramA = Config.PARAM_A;this.paramB = Config.PARAM_B;}public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

有何替代的解决方案?

为了保证全局唯一,除了使用单例类,还可以使用静态方法来实现。这也是项目开发中经常用到的一种实现思路。比如,上一节课讲的 ID 唯一递增生成器的例子,用静态方法实现以下,就是下面这个样子:

public class IdGenerator {private static AtomicLong id = new AtomicLong(0);public static long getId() {return id.incrementAndGet();}
}// 使用举例
long id = IdGenerator.getId();

不过静态方法这种实现思路,并不能解决之前提到的问题。实际上它比单例更加不灵活,比如,它无法支持懒加载。

实际上,单例除了我们之前讲到的使用方法之外,还有另一种使用方法。具体代码如下所示:

// 1.老的使用方法
public void demoFunc() {// ...long id = IdGenerator.getInstance().getId();// ...
}// 2.新的使用方式:依赖注入
public void demoFunc(IdGenerator idGenerator) {// ...long id = idGenerator.getId();// ...
}
// 外部调用demoFunc()时,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInstance();
demoFunc(idGenerator);

基于新的方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类成员变量),可以解决单例隐藏类之间依赖关系的问题。不过,对于单例存在的其他问题,比如对 OOP 特性、扩展性、可测试性不友好等问题,还是无法解决。

所以,如果要完全解决这些问题,可能要从根本上,寻找其他方式来实现全局唯一类。实际上,类对象的全局唯一性可以通过多种不同的方式来保证。既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容易(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码时保证不创建两个对象)。

回顾

1.单例类存在哪些问题?

  1. 单例对 OOP 特性的支持不友好。
  2. 单例会隐藏类之间的依赖关系
  3. 单例对代码的扩展性不友好
  4. 单例对代码的可测试性不友好
  5. 单例不支持有参的构造函数

2.单例有什么替代解决方案

为保证全局唯一,除了使用单例,还可以使用静态方法来实现。不过静态方法这种实现思路并不能解决我们之前提到的问题。如果要完全解决这些问题,可能要从根上,寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码时保证不创建两个对象)。

有人把单例当做反模式,主张杜绝子在项目中使用。个人觉得这有点极端。模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类是没有太大问题。对于一些全局的类,我们在其他地方创建的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

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

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

相关文章

软考 系统架构设计师系列知识点之系统性能(3)

接前一篇文章:软考 系统架构设计师系列知识点之系统性能(2) 所属章节: 第2章. 计算机系统基础知识 第9节. 系统性能 系统性能是一个系统提供给用户的所有性能指标的集合。它既包括硬件性能(如处理器主频、存储器容量、…

用pdf2docx将PDF转换成word文档

pdf2docx是一个Python模块,可以将PDF文件转换为docx格式的Word文档。 pdf2docx模块基于Python的pdfminer和python-docx库开发,可以在Windows、Linux和Mac系统上运行。它可以从PDF文件中提取文本和图片,并将其转换成可编辑的Word文档&#xf…

ZC3201 耐压40V输出12V 300mA LDO

概述 ZC3201是一款40V高精度微安级功率LDO稳压器。只有luA的功耗使其适用于大多数高压节电系 统。其最大工作电压高达40V. 其他功能包括低压差,1%的极高输出精度,限流保护和高纹波抑制比。 ZC3201采用SOT89-3,SOT23…

分布式游戏服务器

1、概念介绍 分布式游戏服务器是一种专门为在线游戏设计的大型系统架构。这种架构通过将游戏服务器分散部署到多台计算机(节点)上,实现了数据的分散存储和计算任务的并行处理。每个节点都负责处理一部分游戏逻辑和玩家请求,通过高…

DM-达梦数据库实时主备搭建

dm实时主备说明 将主库产生的 Redo日志传输到备库,备库接收并重演Redo日志,从而实现备库与主库的数据同步。 一、环境准备 1.1、配置环境准备 首先搭建实时主备,要规划好机器的,我准备两台机器服务器 主服务器 mast…

监控系统prometheus+grafana+发送告警信息

1、基础环境准备两台或更多的主机 2、关闭selinux vi /etc/selinux/config,修改SELINUX的值为disabled 3、关闭防火墙 systemctl disable firewalld systemctl stop firewalld 4、prometheus官网下载 https://prometheus.io/download/ 5、grafana官网下载 https…

Cronos zkEVM 基于 Covalent Network(CQT)数据可用性 API,推动其 Layer2 DeFi 生态更好地发展

在一项旨在显著改善 DeFi 生态的战略举措中,Cronos 与 Covalent Network(CQT)携手合作,以期待 Cronos zkEVM 的推出。这一整合,预计将进一步降低以太坊生态系统的交易成本、提升交易速度,并带来更好的交易体…

开源IT自动化运维工具Ansible Playbook介绍

Ansible Playbook 是一种基于 YAML 格式的配置、编排与自动化工具,用于定义和执行IT基础设施的配置管理和应用程序部署任务。它允许您以声明式的方式编写任务集,这些任务集可以在一组或多组主机上按顺序执行,以实现自动化的配置管理、应用部署…

【Qt】使用Qt实现Web服务器(三):QtWebApp中HttpRequest和HttpResponse

1、HttpRequest 1.1 示例 1)在Demo1的Dump HTTP request示例 在浏览器中输入http://127.0.0.1:8080点击Dump HTTP request 2)切换到页面:http://127.0.0.1:8080/dump 该页面显示请求和响应的内容: Request: Method: GET Path: /dump Version: HTTP/1.1 Headers: accep…

C语言 指针练习

一、 a、b是两个浮点型变量&#xff0c;给a、b赋值&#xff0c;建立两个指针分别指向a的地址和b的地址&#xff0c;输出两个指针的值。 #include<stdio.h> int main() {float a,b,*p1,*p2;a10.2;b2.3;p1&a;p2&b;printf("a%f,b%f\n",a,b);printf("…

Python 深度学习第二版(GPT 重译)(三)

七、使用 Keras&#xff1a;深入探讨 本章涵盖 使用 Sequential 类、功能 API 和模型子类创建 Keras 模型 使用内置的 Keras 训练和评估循环 使用 Keras 回调函数自定义训练 使用 TensorBoard 监控训练和评估指标 从头开始编写训练和评估循环 您现在对 Keras 有了一些经…

零基础入门多媒体音频(4)-GENIVIProjectAudioManager总览

GENIVI Project的AudioManager是一个专门设计用于汽车信息娱乐系统的音频管理解决方案。它负责管理和控制车辆内的音频源和音频路径&#xff0c;确保各种音频信号能够正确、高效地在车辆的音响系统中传输和播放。 AudioManager的核心功能包括音频源的管理、音频路径的控制以及音…

【Spring Cloud】微服务通信概述

SueWakeup 个人主页&#xff1a;SueWakeup 系列专栏&#xff1a;学习技术栈 个性签名&#xff1a;人生乏味啊&#xff0c;我欲令之光怪陆离 本文封面由 凯楠&#x1f4f7; 友情赞助播出 目录 前言 1. Dubbo&#xff08;Spring Cloud Alibaba&#xff09;和 Spring Cloud 的适…

Python利用pygame实现飞机大战游戏

文章目录&#xff1a; 一&#xff1a;运行效果 1.演示 2.思路和功能 二&#xff1a;代码 文件架构 Demo 必备知识&#xff1a;python图形化编程pygame游戏模块 一&#xff1a;运行效果 1.演示 效果图◕‿◕✌✌✌ Python利用pygame实现飞机大战游戏运行演示 参考&#x…

AMPQ和rabbitMQ

RabbitMQ 的 Channel、Connection、Queue 和 Exchange 都是按照 AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;标准实现的。 AMPQ的网络部分 AMQP没有使用HTTP&#xff0c;使用TCP自己实现了应用层协议。 AMQP实现了自己特有的网络帧格式。 一个Connection…

[网鼎杯 2020 朱雀组]Think Java

[网鼎杯 2020 朱雀组]Think Java swagger [[swagger]] 首先下载源码&#xff0c;查看之后发现 查找swagger资料&#xff0c;或者扫描&#xff0c;得到&#xff1a;swagger-ui.html swagger-ui 提供了一个可视化的UI页面展示描述文件。接口的调用方、测试、项目经理等都可以…

[激光原理与应用-76]:光束指向性与影响因素

目录 一、光束指向性 1.1 概述 2.2 光束指向性与光斑大小的区别 2.3 光束指向性与时间的关系 二、激光器的光束指向性的测量方法 2.1 概述 2.2 计算方法 三、激光器中影响光束指向性的因素 一、光束指向性 1.1 概述 光束指向性是指光束传播方向的特性&#xff0c;也可…

Java与Go:对象

对象是面向对象编程的核心概念之一&#xff0c;它具有封装、抽象、继承、多态等特性&#xff0c;能够帮助程序员更好地组织和管理程序&#xff0c;提高代码的可读性、可维护性和可重用性。今天我们来聊一聊Java中的class和Go语言的struct。 Java的class 在Java中&#xff0c;…

【Linux】进程通信

目录 一、管道通信 二、共享内存 三、消息队列 一、管道通信 管道是由操作系统维护的一个文件&#xff0c;管道通信的本质就是将管道文件作为临界资源&#xff0c;实现不同进程之间的数据读写&#xff0c;但是管道只允许父子进程或者兄弟进程之间的通信。 管道文件本身是全…

刷题DAY29 | LeetCode 491-递增子序列 46-全排列 47-全排列 II

491 递增子序列&#xff08;medium&#xff09; 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。 数组中可能含有重复元素&#xff0c;如出现两个整数相等&#xff0c;也…