设计模式之美
王争
前 Google 工程师,《数据结构与算法之美》专栏作者
123425 人已学习
新⼈⾸单¥98
登录后,你可以任选6讲全文学习
课程目录
已完结/共 113 讲
设计模式与范式:行为型 (18讲)
设计模式之美
15
15
1.0x
00:00/00:00
登录|注册

42 | 单例模式(中):我为什么不推荐使用单例模式?又有何替代方案?

程序员自己保证不创建两个类对象
IOC容器(如Spring IOC容器)
工厂模式
解决隐藏类之间依赖关系的问题
将单例生成的对象作为参数传递给函数
无法解决之前提到的问题
无法实现mock替换
影响代码的可测试性
限制了代码的扩展性、灵活性
无法应对创建多个实例的需求
难以识别类之间的依赖关系
不友好地支持继承、多态特性
难以应对不同业务采用不同的ID生成算法
违背基于接口而非实现的设计原则
解决单例支持参数传递的问题
重构代码以提高可测试性
单例的替代解决方案
单例存在的问题
其他方式
依赖注入
静态方法
单例不支持有参数的构造函数
单例对代码的可测试性不友好
单例对代码的扩展性不友好
单例会隐藏类之间的依赖关系
单例对OOP特性的支持不友好
课堂讨论
重点回顾
有何替代解决方案?
单例存在哪些问题?
我为什么不推荐使用单例模式?

该思维导图由 AI 生成,仅供参考

上一节课中,我们通过两个实战案例,讲解了单例模式的一些应用场景,比如,避免资源访问冲突、表示业务概念上的全局唯一类。除此之外,我们还学习了 Java 语言中,单例模式的几种实现方法。如果你熟悉的是其他编程语言,不知道你课后有没有自己去对照着实现一下呢?
尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用。所以,今天,我就针对这个说法详细地讲讲这几个问题:单例这种设计模式存在哪些问题?为什么会被称为反模式?如果不用单例,该如何表示全局唯一类?有何替代的解决方案?
话不多说,让我们带着这些问题,正式开始今天的学习吧!

单例存在哪些问题?

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

1. 单例对 OOP 特性的支持不友好

确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

单例模式在实际开发中常用,但存在一些问题,有人认为它是一种反模式。单例模式对面向对象编程的特性支持不友好,违背了抽象、继承和多态原则,导致代码扩展性差。此外,它隐藏了类之间的依赖关系,降低了代码可读性,也不友好于代码的扩展性和可测试性。文章提出了替代方案,包括使用静态方法、通过init()函数传递参数、将参数放到getInstance()方法中,以及将参数放到另外一个全局变量中。这些方法能解决单例模式存在的问题,但仍需从根本上寻找其他方式来实现全局唯一类,如工厂模式、IOC容器等。总之,单例模式并非绝对反模式,关键在于合理使用。文章还就如何通过重构代码提高代码的可测试性和解决单例支持参数传递的问题展开了讨论。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《设计模式之美》
新⼈⾸单¥98
立即购买
登录 后留言

全部留言(80)

  • 最新
  • 精选
  • Roger宇
    想问一下老师,所谓两个资源池,慢的请求独占一个的设计,如何知道一个sql请求会快会慢?快与慢更多是在运行后才知道的,已经进去运行了还怎么保证独占呢?除非有机制可以在处理sql请求之前评估可能需要的时间。

    作者回复: 根据业务来估算的,有些业务本身就很慢,就判定为慢sql

    2020-05-15
    4
  • 小晏子
    课堂讨论, 1. 把代码“User cachedUser = CacheManager.getInstance().getUser(userId);”单独提取出来做成一个单独的函数,这样这个函数就可以进行mock了,进而方便测试validateCachedUser。 2. 可以判断传进来的参数和已经存在的instance里面的两个成员变量的值,如果全部相等,就直接返回已经存在的instance,否则就新创建一个instance返回。示例如下: public synchronized static Singleton getInstance(int paramA, int paramB) { if (instance == null) { instance = new Singleton(paramA, paramB); } else if (instance.paramA == paramA && instance.paramB == paramB) { return instance; } else { instance = new Singleton(paramA, paramB); } return instance; }
    2020-02-07
    21
    122
  • J.Smile
    模式没有对错,关键看你怎么用。这句话说的很对,所以其实所谓单例模式的缺点这种说法还是有点牵强!
    2020-02-07
    2
    81
  • Richie
    课堂讨论第2点,我认为应该先搞清楚需求,为什么需要在getInstance()方法中加参数,想要达到什么目的? 这里分两种情况讨论一下: 1. 如果的确需要一个全局唯一的类,并且这个类只能被初始化一次,那么应该采用文中提到的第三种解决思路,即将所需参数放到全局的配置文件中,从而避免多次初始化参数被忽略或者抛出运行时异常的问题; 2. 如果是要根据不同参数构造出不同的对象,并且相同参数的对象只被构造一次,那么应该改成在Singleton类中维护一个HashMap,然后每次调用getInstance()方法的时候,根据参数去判断对象是否已经存在了(可以采用双重检测),存在则直接返回,不存在再去创建,然后存储,返回。个人理解,这应该是单例+简单工厂的结合。
    2020-03-08
    3
    67
  • 李小四
    设计模式_42: # 作业 1. 可以把单例的对象以依赖注入的方式传入方法; 2. 第二次调用时,如果参数发生了变化,应该抛出异常。 # 感想 坦白讲,一直以使用双重检测沾沾自喜。。。现在看来,要不要使用单例要比使用那种单例的实现方式更需要投入思考。
    2020-02-22
    1
    31
  • 林子er
    由于单例本身存在的一系列缺点,而单例一般又都是全局的,因而一般我们项目中很少直接使用单例,而是通过容器注入,让容器充当单例和工厂。有时候我们甚至使用伪单例,即类本身并不是单例的,而是通过容器保证单例性,实际编程中按照约定只通过容器获取该实例。 参数化单例实际中是通过Map解决的,即同样的参数才返回同一个实例,不同的参数返回不同的实例,为了保证实例不会太多,一般可传的参数我们会事先做了限制,比如只能使用配置文件中配置的(如数据库连接池的名称)
    2020-04-23
    21
  • webmin
    1. 如果项目中已经用了很多单例模式,比如下面这段代码,我们该如何在尽量减少代码改动的情况下,通过重构代码来提高代码的可测试性呢? CacheManager.getInstance(long userId)中增加Mock开关,如: private User mockUser; public CacheManager.setMockObj(User mockUser) public User getInstance(long userId) { if(mockUser != null && mockUser.getUserId() == userId) { return mockUser } } 2. 在单例支持参数传递的第二种解决方案中,如果我们两次执行 getInstance(paramA, paramB) 方法,第二次传递进去的参数是不生效的,而构建的过程也没有给与提示,这样就会误导用户。这个问题如何解决呢? 第一次构造Instance成功时需要记录paramA和paramB,在以后的调用需要匹配paramA与paramB构造成功Instance时的参数是否一至,不一至时需要抛出异常。
    2020-02-07
    3
    8
  • 小麦
    不太能理解的使用方式违背了基于接口而非实现的设计原则,比如 spring 中的 service 类一般也是单例的,也是继承接口,controller 的调用也是基于接口,不觉得有什么问题啊,如果实现类变了,也只是改注入而已啊。
    2020-04-03
    3
    6
  • 黄林晴
    打卡
    2020-02-07
    6
  • @二十一大叔
    1. public class Demo { private UserRepo userRepo; // 通过构造哈函数或IOC容器依赖注入 private CacheManager cacheManager; // 将获取CacheManager对象提出来,通过依赖注入的方式初始化 public Demo(CacheManager cacheManager){ this.cacheManager = cacheManager; } public boolean validateCachedUser(long userId) { User cachedUser = getCachedUser(userId); User actualUser =userRepo.getUser(userId); // 省略核心逻辑:对比cachedUser和actualUser... } public User getCachedUser(long userId){ return cacheManager.getInstance().getUser(userId); } static class MockManager extends CacheManager { private static MockManager mockManager; private MockManager(){} public static MockManager getInstance(){ //todo return mockManager; } public static User getUser(long userId){ // 返回mock数据 return new User(userId); } } public static void main(String[] args) { CacheManager cacheManager = MockManager().getInstance(); Demo demo = new Demo(cacheManager); User user = demo.getCachedUser(123L); }
    2022-09-27归属地:上海
    1
    5
收起评论
显示
设置
留言
80
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部