在文章<<谈谈单例模式>>中介绍过单例模式, 一个类全局只会生成一个实例化的对象。而本文将从一些工程经验中,抽象出问题的场景,来谈一谈多例模式,便于还不熟悉的读者更好的理解。
设计模式是前辈们通过丰富的工程实践后总结出的经验。所以笔者认为对于设计模式的理解,并不仅仅在于看懂这个模式的实现方式,更重要的是明白什么时候该使用什么设计模式,而要做到这个,离不开的是不断地实践。注意这里的实践,并不是说手写一遍设计模式的实现,而是多做一些项目,从项目中汲取经验,才能理解的更加深刻,变为自己能够灵活运用的知识。
首先多例模式算是单例模式的一种扩展。在理解多例模式之前,本文我们换一个例子,Redis连接池,再来加深下对单例模式的认识。一般在工程实践中,为了减少TCP连接带来的额外时间消耗,以及可能存在的认证过程,一般采用长连接(Persistent Connection)的方式维护和利用连接,即将连接存放在一个池子中(连接池, 一般可以采用容器存储),当应用访问redis的时候,从池子里取出一个连接对象,然后复用这个连接。当然本文重点讲解的不是这个连接池,想了解的同学可以看一看<<对象池的使用场景以及自动回收技术>>。
以上就是一个场景的铺垫,当一个应用程序第一次引入Redis的时候,会使用如下图所示,这个模块会调用Redis连接池
,访问Redis。
这样的使用一点都没有问题, 但当又有个同学写了另一个模块发现也要用Redis,然后就变成了这样。当项目复杂之后,多人协作,就容易写出下面的方式,每一个模块都使用了一个连接池。如果是小型程序,完全没有关系,但是如果你的服务是一个可以自动伸缩扩展机器的SaaS服务呢?又或者某天另一个同学又实现了一个类似的需要访问Redis的模块n
. 那么会发生什么?一般连接池会保持一个最小连接数,假设这个最小连接数是3,那么有n个模块一台机器就创建了n3个连接。那如果是m台机器,那么就会有mn*3个连接,这样也许会把单点的Redis连接数量给撑满,从而导致服务使用问题。
理论上一个应用程序使用一个Redis连接池,一般就足够了,而不需要每个模块都使用一个连接池。那么这就提到了本章节的主人公单例模式
,如果第一个写Redis连接池
的同学,将其实现为单例模式是不是,就可以从防御性编程角度出发,避免其他同学实现其他模块的时候,也只会用同一个实例化的Redis连接池
,这就是我认为单例的好处之一。模式变为了如下:
我们还是从连接池的角度出发,接着看一个多例模式的场景。
有了上一个章节做铺垫,我们应该理解了单例模式的重要任务,就是只实例化一个对象。而多例模式顾名思义,就是实例化多个对象。这么一听,是不是就是可以直接实例化对象了?不是的。多例模式一般实例化的对象是有有限的数量,每一个实例的对象一般都关联一个索引的Key
,根据Key
的只会构造/对应一个实例化对象。是不是这么说还有点抽象,我们用一个样例来阐述阐述下。
在SaaS
环境中,经常会切分为多个服务,在不同的机器运行,当你所编写的服务要访问另一个服务A
,http
是常见的一种方式,于是你实现了一个http的连接池
(准确来说应该tcp连接池,http相当于基于tcp的一个应用封装),这个连接池只实例化了一个对象在应用程序
中专门用来访问服务A
,于是这位同学先将这个连接池做成了一个单例模式。
但是这个时候又出现了一个需求,需要访问服务B
于是结构变成了这样, 两个连接池。这两个连接池有什么共同特点?其实对于对象的接口和方法实现都是一样的,唯一不一样的就是连接地址
。那么这个时候,单例模式肯定不能满足了,单例模式本来是为了整个进程只有一个实例访问服务A
,那么这个时候是不是就可以有两个实例了,一个连接池实例用来访问服务A
,一个连接池实例访问服务B
。这也就是我们要实现的多例模式,而上面所说的Key
,你就可以定义为ServiceA
和ServiceB
, 分别对应两个实例化的连接池,内部采用map
存储查找。
根据上面描述,可以实现为如下图所示。关于代码实现也比较简单,我们接着来看下一个章节:
先画出类图,主要差别于单例模式的是,采用unordered_map
存储多个实例,然后使用strInstanceKey
去查询关联的实例,如果没有创建则创建相应的实例。
以下是实现的一个线程安全的多例模式实现。这个实现展现了多例的细节,并不会包含HttpPool
的具体实现(不是本文的重点)。代码就不逐行解析了主要挑几个点说一下:
GetInstance
函数,第一个参数strInstanceKey
就是用来表示一个实例的名字GetInstance
函数可变参数,通过std::forward
实现完美转发,在GetInstance
内部作为HttpPoolMultiton
的构造函数参数。当然也可以将这些可变参数,直接写成和构造函数一样,这种完美转发主要常见于模板的实现。shared_ptr
管理其生命周期,通过unordered_map
容器存储这些实例;相同的strInstanceKey
获取的实例对象是一样的。#include <iostream>
#include <mutex>
#include <unordered_map>
#include <memory>
class HttpPoolMultiton
{
public:
using HttpPoolMultitonSPtr = std::shared_ptr<HttpPoolMultiton>;
//Args is the constructor parameters
template<typename... Args>
static HttpPoolMultitonSPtr GetInstance(const std::string& strInstanceKey, Args&&... args)
{
std::lock_guard<std::mutex> guard(m_mutex);
if (m_mapObjects.count(strInstanceKey) == 0)
{
m_mapObjects.emplace(strInstanceKey, new HttpPoolMultiton(std::forward<Args>(args)...));
}
return m_mapObjects[strInstanceKey];
}
~HttpPoolMultiton() { ; };
private:
HttpPoolMultiton(const std::string& strHost, const unsigned short uhPort)
{
//Here need to do some initialization
};
HttpPoolMultiton(const HttpPoolMultiton&) = delete;
HttpPoolMultiton& operator= (const HttpPoolMultiton&) = delete;
private:
static std::unordered_map<std::string, HttpPoolMultitonSPtr> m_mapObjects;
static std::mutex m_mutex;
};
std::unordered_map<std::string, HttpPoolMultiton::HttpPoolMultitonSPtr> HttpPoolMultiton::m_mapObjects;
std::mutex HttpPoolMultiton::m_mutex;
int main()
{
auto httpPoolA = HttpPoolMultiton::GetInstance("ServiceA", std::string("192.168.1.1"), 7777);
auto httpPoolA1 = HttpPoolMultiton::GetInstance("ServiceA", std::string("192.168.1.1"), 7777);
auto httpPoolB = HttpPoolMultiton::GetInstance("ServiceB", std::string("192.168.1.2"), 8888);
if (httpPoolA != httpPoolB)
{
std::cout << "httpPoolA is not be the same with httpPoolB!" << std::endl;
}
if (httpPoolA == httpPoolA1)
{
std::cout << "httpPoolA is the same with httpPoolA1!" << std::endl;
}
return 0;
}
本文并没有采用模板的方式实现,便于经验还不够丰富的同学查看。如果想要学习模板实现可以参照qicosmos
所写的<<c++11改进我们的模式之改进单例模式>>: https://www.cnblogs.com/qicosmos/p/3145019.html。
多例模式简单来说就是单例模式的扩展,可以根据使用场景,实例化指定数量的实例化对象,通过Key
来查找相应的对象,如果查找不到,则创建一个新的对象,即每个Key
关联一个实例化的对象。
真正的理解设计模式,一定要多进行项目实践,才能体会的更加深刻。