2021-08-25学习总结

单例存在哪些问题?

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

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.getIntance().getId();
//...
}
}
public class User {
	public void create(...) {
// ...
		long id = IdGenerator.getInstance().getId();
// 需要将上面一行代码,替换为下面一行代码
		long id = UserIdGenerator.getIntance().getId();
	}
}

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

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

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

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

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

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();

第二种解决思路是:将参数放到 getIntance() 方法中。具体的代码实现如下所示:

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;
	}
}
Singleton singleton = Singleton.getInstance(10, 50);

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

public class Config {
	public static final int PARAM_A = 123;
	public static fianl int PARAM_B = 245;
}
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;
	}
}

有何替代解决方案?

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。

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

不过,静态方法这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加不灵活,比如,它无法支持延迟加载。我们再来看看有没有其他办法。实际上,单例除了我们之前讲到的使用方法之外,还有另外一个种使用方法。具体的代码如下所示:

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值