上一节我们介绍了单例模式模板
本节来讨论下,在框架代码中,怎样设计单例模式
考虑这种场景:
框架的开发者写了一个类 Config
用来管理整个程序运行周期中的配置文件,整个程序中应该只有一个配置文件类,所以站在框架开发者的角度,这个类就应该是全局单例;(这是一个典型的单例模式应用场景,单例模式还有一些其他的应用场景,比如:日志记录,线程池管理等)
框架开发者为这个Config
类提供了很多通用函数,也有一部分虚函数(站在框架设计者的角度,我们只能尽可能的考虑更多场景,提供一系列的通用函数;但是并不知道这个Config
类能否满足所有的情况下的需求,如果可以满足需求,那么应用程序直接使用Config
类的对象作为全局对象即可;如果满足不了需求,那么就需要应用程序的开发者,主动继承这个类,重写虚函数,或者是添加一些自定义函数,然后将应用程序开发者的设计的类的对象作为全局对象),当然Config
类还有一个和其他单例类一样的静态函数 static Config* getInstance();
用来获取全局单例对象
站在框架开发者
的角度,是提供了一些通用的功能,也支持对类Config
的扩展(继承Config
,重写虚函数)
在程序运行时(应用程序开发者
),能通过框架设计者提供的 static Config* getInstance();
来获取到全局单例对象,这个对象可能是Config
的实例,也可能是Config
的子类的实例
那么框架设计者的这个类Config
应该如何设计呢?
- 框架开发者考虑到应用程序在运行时,不一定是直接使
Config
类的对象,有可能还会使用Config
的子类对象,所以可以将getInstance()
设计为模板函数, 并需要在类Config
中添加一个静态成员变量,保存类的指针,如果应用程序未对Conifg
类做扩展,那么这个指针指向Config
类的对象,如果应用程序做出扩展,那么这个指针指向Config
的子类对象:
//框架中的 Config 类
Config* Config::m_ins = nullptr;
class Config
{
public:
template <typename T=Config>
static T* getInstance()
{
if (m_ins != nullptr)
{
return dynamic_cast<T*>(m_ins);
}
return nullptr;
}
void setGlobalConfig(Config* ins)
{
m_ins = ins;
//此处可以调用一些(纯)虚函数进行初始化操作
initValue();
}
void getValue() {}
virtual void initValue() //模拟初始化一些变量(虚函数或者纯虚函数)
{
m_value = 10;
}
public:
virtual ~Config() {}
protected:
Config() {}
private:
int m_value = -1;
static Config* m_ins;
};
//应用程序对Config类做扩展
class CustomConfig: public Config
{
// 特定于CustomConfig的函数和数据
CustomConfig() {}
public:
~CustomConfig() {}
void getCustomValue() {}
virtual void init() override
{
m_value = 20;
}
};
//使用时, main函数开始时设置全局对象
std::unique_ptr<CustomConfig> cfg = std::make_unique<CustomConfig>(); //需要智能指针管理内存
cfg->setGlobalConfig(cfg.get());
Config::getInstance()->getValue(); //使用Config 类提供的基础函数
Config::getInstance<CustomConfig>()->getCustomValue(); // 使用 CustomConfig类中的特有函数
用过Qt的同学们可以回过头想想,Qt的qApp
是不是也是类似,源码如下;这里的QCoreApplication
就对应着我们的Config
类,并且Qt和我们一样,并不知道当前设计的类是否足够用户使用;这里的QApplication
就对应着CustomConfig
类
我们可以在程序中使用 qApp
获取到全局的 QApplication
对象,就对应着我们可以使用getInstance<>()
模板函数获取到全局的Custom
类对象,
这里的self
指针,就是我们上面的 m_ins
指针,只不过QCoreApplication
中没有一个类似于setGlobalConfig()
的函数为self
赋值,self
指针的赋值操作,是Qt框架内部完成的, 所以才导致qApp
这个宏定义是在QApplicaiton
中,而不是在QCoreApplication
中,这么一比较下来,我们当前的设计,比Qt具有更高的扩展性
#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))
static QCoreApplication *instance() { return self; }
进一步思考:
上述例子中,我们演示的是应用程序开发者对Config
类做了扩展,使用了自定义的类CustomConifg
类对象作为全局单例
思考:那如果我们设计的Config
类就足够满足应用程序的使用场景,那么应用程序开发者就不需要对Config
类做扩展,他希望直接使用Config
类对象就够了,那么代码又该如何写呢?
std::unique_ptr<Config> cfg = std::make_unique<Config>(); //主动创建
cfg->setGlobalConfig(cfg.get()); //设置全局单例对象
Config::getInstance()->getValue(); //使用Config 类提供的基础函数
这样是否增加了应用程序开发者的使用负担了呢? 对于应用程序的开发者,他也许希望直接就能通过Config::getInstance()
函数获取到全局的 Config
类对象,而不是还需要他自己去创建对象,并调用setGlobalConfig()
设置到全局,因为他并没有对Config
类做任何扩展
我们可否在第一次使用Config::getInstance()
函数时,创建一个Config
或者其子类
的对象,作为全局对象呢?
Config* Config::m_ins = nullptr; //类外初始化静态变量
template <typename T=Config>
static T* getInstance()
{
if (m_ins == nullptr) //创建Config类对象,或者是子类对象
{
static std::mutex mu;
std::lock_guard<std::mutex> lock(mu);
if (m_ins == nullptr)
{
m_ins = new T();
}
}
if (m_ins != nullptr)
{
return dynamic_cast<T*>(m_ins);
}
return nullptr;
}
void init()
{
initValue();
}
//使用
Config::getInstance()->getValue(); //第一次调用时,会主动创建一个Config类对象作为全局单例
Config::getInstance<CustomConfig>()->getCustomValue(); //第一次调用时,会主动创建一个CustomConfig类对象作为全局单例
Config::getInstance()->init(); //初始化
这样就免去了应用程序使用时,主动创建对象,并调用setGlobalConfig
函数的过程;变成了在应用程序第一次使用 Config::getInstance<>()
模板函数时,根据模板参数去创建对应的对象;
需要注意,这里虽然省去了setGlobalConfig
函数的调用,带来了方便,但是也失去了setGlobalConfig
函数调用initValue()
进行初始化的能力; 我们这样做的话,就需要额外提供一个init
类似的函数,在其中调用initValue()
进行初始化操作,因为构造函数中无法调用虚函数; 此种方式比较适合不需要调用(纯)虚函数进行初始化的场景
继续思考
思考:如果这里的Config
构造函数需要参数,或者CustomConfig
类构造函数需要参数,如何支持?
只需要将getInstance()
模板函数设计成支持可变参数的模板即可,我们这里假设CutomConfig
类需要两个int
类型的参数:
Config* Config::m_ins = nullptr; //类外初始化静态变量
template<typename T = Config, typename ...Args>
static T* getInstance(Args&&... args)
{
if (m_ins == nullptr)
{
static std::mutex mu;
std::lock_guard<std::mutex> lock(mu);
if (m_ins == nullptr)
{
m_ins = new T(std::forward<Args>(args)...);
}
}
if (m_ins != nullptr)
{
return dynamic_cast<T*>(m_ins);
}
return nullptr;
}
使用:
Config::getInstance<CustomConfig>(10, 20)->getCustomValue(); //使用两个int类型参数,构造全局CustomConfig类对象,并调用自定义函数
到这里,我们框架中的这个Config
类是否已经足够完美了呢?
思考: 站在应用程序开发者的角度
, 以上的方式,除了在首次创建CustomConfig
类对象时,需要传递参数,并且应用程序开发者想要使用CustomConfig
类对象时,都需要传递参数,并且后续传入的参数已经失去了作用,因为全局对象已经使用首次的参数创建好了,后续即使传递参数,也不会再创建新的对象,这样明显不合理了; 如果能做到仅仅在第一次创建对象时传递参数,后续使用不需要再传构造参数,那么还可以接受;
如果应用程序
开发者说,没关系,我可以定义一个这样的宏:
#define INST_CustonConfig Config::getInstance<CustomConfig>(10, 20)
//使用
INST_CustonConfig->getCustomValue();
INST_CustonConfig->XXXX();
在应用程序中只使用这个宏, 那固然可以解决我们刚才说的问题;但是做为框架开发者
,我们更希望能彻底解决这个问题,而不是将问题抛给应用程序去规避,该怎么做呢?
参考上一节,我们同样可以使用一个不带参数的 getInstance()
函数作为重载, 完善后伪码如下:
Config* Config::m_ins = nullptr; //类外初始化静态变量
template<typename T = Config, typename ...Args>
static T* getInstance(Args&&... args)
{
if (m_ins == nullptr)
{
static std::mutex mu;
std::lock_guard<std::mutex> lock(mu);
if (m_ins == nullptr)
{
m_ins = new T(std::forward<Args>(args)...);
}
}
if (m_ins != nullptr)
{
return dynamic_cast<T*>(m_ins);
}
return nullptr;
}
//重载版本,无需传递构造参数的 getInstance() 函数
template<typename T = Config>
static T* getInstance()
{
return dynamic_cast<T*>(m_ins);
}
使用:
Config::getInstance<CustomConfig>(10, 20); //首次使用参数创建全局类对象
Config::getInstance<CustomConfig>()->getCustomValue(); //后续使用无需参数
到这里,其实还有一些问题,比如:
双检查锁的CPU指令重排
导致的多线程问题, 这里不做过多赘述- 当程序使用完该单例,需要开发者主动去调用类似以下的
destory()
函数去释放该单例以及它所管理的资源(注意,这里不能在单例类的析构函数中调用delete,否则递归导致崩溃 参考; 这里可以用智能指针(shared_ptr 或者 unique_ptr)
解决
// 释放资源。
static void destory()
{
if (m_ins!= nullptr)
{
delete m_ins;
m_ins = nullptr;
}
}
此处使用unique_ptr
做为示例,修改后部分代码如下:
class Config
{
Config()
{
qDebug() << "construct config";
}
public:
virtual ~Config()
{
qDebug() << "destruct config";
}
template<typename T = Config, typename ...Args>
static T* getInstance(Args&&... args)
{
static std::once_flag s_flag;
//此处不能使用 make_unique或make_shared,因为这两个函数需要公有的构造函数,这违反了单例模式的规定
std::call_once(s_flag, [&]() { m_ins.reset(new T(std::forward<Args>(args)...));});
if (m_ins != nullptr)
{
return dynamic_cast<T*>(m_ins.get());
}
return nullptr;
}
//重载版本,无需传递构造参数的 getInstance() 函数
template<typename T = Config>
static T* getInstance()
{
if (m_ins != nullptr)
{
return dynamic_cast<T*>(m_ins.get());
}
return nullptr;
}
void getValue()
{
qDebug() << "config get value";
}
private:
static std::unique_ptr<Config> m_ins;
};
std::unique_ptr<Config> Config::m_ins = nullptr;
class CustomConfig : public Config
{
public:
CustomConfig(int value, int value2)
{
qDebug() << "construct custom config!" << value << value2;
}
~CustomConfig()
{
qDebug() << "destruct custom config!";
}
void getCustomValue()
{
qDebug() << "custom config get value";
}
};
返璞归真
从头到尾,思考和改动了这么多次,我们最大的前提是希望通过框架中Config类的getInstance()函数,拿到应用程序中自定义的类对象(CRTP),并作为全局单例
,如果一开始,应用程序的开发者就不希望使用这种方式去获取他自定义的单例类对象,而是他自己在CustomConfig
类中重新定义了一个getInstance()
的静态函数,用来获取全局的CustomConfig单例对象
,那么我们思考的所有都不再是问题;
应用程序中只需要这样使用就好了
CustomConfig::getInstance()->restoreValue();
我们为何一定要在Config的框架代码中拿到应用程序中自定义的全局单例对象
呢? 是因为在其他模块的框架代码中需要使用到Config::getInstance()
对象,(比如某个动作执行结束后,需要存配置;执行动作、存储配置,这些代码也属于稳定部分,写在框架中)而这部分代码无法在运行时替换为CustomConfig::getInstance()
;
回到以上的代码,读者你认为哪种方式最好呢?
笔者认为无论应用程序开发者觉得最开始的Config类是否足够使用
,他都应该在应用程序代码中,主动的创建Config (或子类)
对象,并调用setGlobalConfig
函数,将其设置为全局单例;亦或重新写一个getInstance()
函数。
结束
非常感谢你能认真看完这篇文章,如果能对你有所帮助,请不要吝啬你的点赞!