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

本文深入探讨了单例模式在OOP中的局限性,包括隐藏依赖、影响扩展性和可测试性,以及不支持有参构造。提出了静态方法和工厂模式等替代方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单例存在哪些问题?

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

OOP 的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都支持得不好

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 类的地方,这样代码的改动就会比较大。

除此之外,单例对继承、多态特性的支持也不友好。这里我之所以会用“不友好”这个词,而非“完全不支持”,是因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

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

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

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

我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?

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

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

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

 

有何替代解决方案?

刚刚我们提到了单例的很多问题,你可能会说,即便单例有这么多问题,但我不用不行啊。我业务上有表示全局唯一类的需求,如果不用单例,我怎么才能保证这个类的对象全局唯一呢?为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。比如,上一节课中讲的 ID 唯一递增生成器的例子,用静态方法实现一下,就是下面这个样子

// 静态方法实现方式
public class IdGenerator {
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() { 
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类。实际上,类对象的全局唯一性可以通过多种不同的方式来保证。我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。这就类似 Java 中内存对象的释放由 JVM 来负责,而 C++ 中由程序员自己负责,道理是一样的。

重点回顾

1. 单例存在哪些问题?

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

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

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。有人把单例当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值