OOD&OOP-单例模式

单例模式

为什么要使用单例?什么时候使用单例?

当可能出现线程不安全、资源访问冲突以及某些类对象数据只应保存一份时应使用单例

1.处理资源访问冲突

我们自定义实现了一个往文件中打印日志的Logger类:

class FileWriter {};
class Logger
{
private:
	fstream file;
public:
	Logger() {
		file.open("/Users/test/log.txt",ios::in | ios::out);
	}
	void log(string message)
	{
		file.write(message.c_str(),message.length());
	}
};
class UserController {
public:
	UserController(Logger logger_):logger(logger_){}
	void log(string id, string password) {
		logger.log(id + "login");
	}
private:
	Logger logger;
};

class Order {};
class UserController {
public:
	UserController(Logger logger_):logger(logger_){}
	void log(Order order) {
		logger.log("log" + order.toString());
	}
private:
	Logger logger;
};

在上面的代码中,我们注意到,所有的日志都写入到同一个文件/Users/test/log.txt 中。在 UserControllerOrderController 中,我们分别创建两个 Logger 对象。在多线程环境下,如果两个线程同时分别执行 login()函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。

那如何来解决这个问题呢?我们最先想到的就是通过加锁的方式:给 log() 函数加互斥锁,同一时刻只允许一个线程调用执行 log()函数。

不过,你仔细想想,这真的能解决多线程写入日志时互相覆盖的问题吗?答案是否定的。这是因为,这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行 log() 函数,锁并不会起作用,仍然有可能存在写入日志互相覆盖的问题。

那我们该怎么解决这个问题?我们需要把对象级别的锁,换成类级别的锁就可以了。让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用log() 函数,而导致的日志覆盖问题。

除了使用类级别锁之外,实际上,解决资源竞争问题的办法还有很多,分布式锁是最常听到的一种解决方案。不过,实现一个安全可靠、无 bug、高性能的分布式锁,并不是件容易的事情。除此之外,并发队列也可以解决这个问题:多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂。

相对于这两种解决方案,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄。将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,也就避免了多线程情况下写日志会互相覆盖的问题。

2.表示全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。

比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。以及,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

单例存在哪些问题?

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

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

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

  3. 单例对代码的扩展性不友好
    我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?
    实际上,这样的需求并不少见。我们拿数据库连接池来举例解释一下。
    在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
    如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

  4. 单例对代码的可测试性不友好
    单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
    除此之外,如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。

  5. 单例不支持有参数的构造函数
    单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。针对这个问题,我们来看下都有哪些解决方案。
    第一种解决思路是:创建完实例之后,再调用 init() 函数传递参数。需要注意的是,我们在使用这个单例类的时候,要先调用 init() 方法,然后才能调用 getInstance() 方法,否则代码会抛出异常。具体的代码实现如下所示:

class Singleton
{
private:
	Singleton(int paraA, int paraB) :paraA(paraA), paraB(paraB) {}
	~Singleton() = default;
	Singleton (const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

private:
	static std::unique_ptr<Singleton> instance;
	static std::once_flag onceFlag;
	int paraA;
	int paraB;
public:
	static std::unique_ptr<Singleton> getInstance(int paraA, int paraB)
	{
		std::call_once(onceFlag, [&] {instance.reset(new Singleton(paraA, paraB)); });
		return instance;
	}

	static void init(int paraA, int paraB)
	{
		if (instance != nullptr)
		{
			perror("Singleton has been created!");
			return;
		}
		instance.reset(new Singleton(paraA, paraB));
		
	}
};
std::unique_ptr<Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::onceFlag;

第二种解决思路是:将参数放到 getIntance() 方法中。不过这种实现方法的问题是。如果我们两次执行getInstance() 方法,那获取到的singleton1和singleton2 相同,第二次的参数没有起作用,而构建的过程也没有给与提示,这样就会误导用户。

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

class Config
{
public:
	static const int paraA = 123;
	static const int paraB = 456;
};

class Singleton
{
private:
	Singleton(int paraA, int paraB) :paraA(Config::paraA), paraB(Config::paraB) {}
	~Singleton() = default;
	Singleton (const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

private:
	static std::unique_ptr<Singleton> instance;
	static std::once_flag onceFlag;
	int paraA;
	int paraB;
public:
	static std::unique_ptr<Singleton> getInstance(int paraA, int paraB)
	{
		std::call_once(onceFlag, [&] {instance.reset(new Singleton(paraA, paraB)); });
		return instance;
	}

	static void init(int paraA, int paraB)
	{
		if (instance != nullptr)
		{
			perror("Singleton has been created!");
			return;
		}
		instance.reset(new Singleton(paraA, paraB));
		
	}
};
std::unique_ptr<Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::onceFlag;

有什么替代单例模式的方案?

可以使用静态方法以依赖注入的方式。
基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。

//老的使用方式
long function()
{	//...
	long id = idGenerator.getInstance().getId();
    //...
}
//新的方式:依赖注入
long function(IdGenerator idGenerator)
{
	long id = idGenerator.getId();
    return id;
}
//外部调用function()的时候,传入idGenerator
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值