• 框架中的单例模式


    上一节我们介绍了单例模式模板
    本节来讨论下,在框架代码中,怎样设计单例模式


    考虑这种场景:
    框架的开发者写了一个类 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;
        }
        
        static void setGlobalConfig(Config* ins)
        {
        	m_ins = ins;
        }
    	
    	int getValue() {}
    	virtual void restoreValue() {}
    
    protected:
        Config() {}
        ~Config() {}
    
    static Config* m_ins;
    };
    
    //应用程序对Config类做扩展
    class CustomConfig: public Config 
    {
    	// 特定于CustomConfig的函数和数据
    public:
        CustomConfig() {}
        ~CustomConfig() {}
    	void getCustomValue() {}
    	virtual void restoreValue() override {}
    };
    
    
    	//使用时
        std::unique_ptr<CustomConfig> cfg = std::make_unique<CustomConfig>();
        Config::setGlobalConfig(cfg.get());
        
    	Config::getInstance()->getValue();		//使用Config 类提供的基础函数
    	Config::getInstance<CustomConfig>()->getCustomValue(); // 使用 CustomConfig类中的特有函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    用过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; }
    
    • 1
    • 2

    进一步思考:

    上述例子中,我们演示的是用程序开发者对Config类做了扩展,使用了自定义的类CustomConifg类对象作为全局单例

    思考:那如果我们设计的Config类就足够满足应用程序的使用场景,那么应用程序开发者就不需要对Config类做扩展,他希望直接使用Config类对象就够了,那么代码又该如何写呢?

        std::unique_ptr<Config> cfg = std::make_unique<Config>();	//主动创建
        Config::setGlobalConfig(cfg.get());	//设置全局单例对象
        
    	Config::getInstance()->getValue();		//使用Config 类提供的基础函数
    
    • 1
    • 2
    • 3
    • 4

    这样是否增加了应用程序开发者的使用负担了呢? 对于应用程序的开发者,他希望直接就能通过Config::getInstance()函数获取到全局的 Config类对象,而不是还需要他自己去创建对象,并且设置给全局,因为他并没有对Config类做任何扩展

    回到框架设计者的角度,那么这个Config类又该如何设计呢?

    回答:可否在第一次使用Config::getInstance()函数时,如果类对象没有指定,那么就创建一个默认的对象?

    	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;
        }
    
    //使用
    Config::getInstance()->getValue();	//第一次调用时,会主动创建一个Config类对象作为全局单例
    Config::getInstance<CustomConfig>()->getCustomValue(); //第一次调用时,会主动创建一个CustomConfig类对象作为全局单例
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这样就免去了应用程序使用时,主动创建对象,并将对象设置为全局对象的过程;变成了在应用程序第一次使用 Config::getInstance<>()模板函数时,去创建对象,也就是通俗的懒汉式单例模式;


    继续思考

    思考:如果这里的Config构造函数需要参数,或者CustomConfig类构造函数需要参数,那么框架中的getInstance()函数又该如何设计?

    回答:只需要将getInstance()模板函数设计成支持可变参数的模板即可,我们这里假设CutomConfig类需要两个int类型的参数:

        template<typename T = Config, typename ...Args>
        static T* getInstance(Args&&... args)
        {
            if (m_ins == nullptr)
            {
                std::lock_guard<std::mutex> lock(mu);
                if (m_ins == nullptr)
                {
                    m_ins = new T(std::forward<Args>(args)...);
                }
            }
            return static_cast<T*>(m_ins);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    使用:

    	Config::getInstance<CustomConfig>(10, 20)->getCustomValue(); //使用两个int类型参数,构造全局CustomConfig类对象,并调用自定义函数
    
    • 1

    到这里,我们框架中的这个Config类是否已经足够完美了呢?


    思考: 站在应用程序开发者的角度, 以上的方式,除了在首次创建CustomConfig类对象时,需要传递参数,并且应用程序开发者想要使用CustomConfig类对象时,都需要传递参数,并且后续传入的参数已经失去了作用,因为全局对象已经使用首次的参数创建好了,后续即使传递参数,也不会再创建新的对象,这样明显不合理了; 如果能做到仅仅在第一次创建对象时传递参数,后续使用不需要再传构造参数,那么还可以接受;

    那么框架设计者应该如何更进一步设计这个 Config 类呢?
    参考上一节,我们同样可以使用一个不带参数的 getInstance()函数作为重载:

        template<typename T = Config, typename ...Args>
        static T* getInstance(Args&&... args)
        {
            if (m_ins == nullptr)
            {
                std::lock_guard<std::mutex> lock(mu);
                if (m_ins == nullptr)
                {
                    m_ins = new T(std::forward<Args>(args)...);
                }
            }
            return static_cast<T*>(m_ins);
        }
    
    	//重载版本,无需传递构造参数的 getInstance() 函数
        template<typename T = Config>
        static T* getInstance()
        {
            return static_cast<T*>(m_ins);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    使用:

     Config::getInstance<CustomConfig>(10, 20)->getCustomValue();	//首次使用参数创建全局类对象
     Config::getInstance<CustomConfig>()->getCustomValue();	//需后续无需参数,使用类对象
    
    • 1
    • 2

    返璞归真

    从头到尾,思考和改动了这么多次,读者你认为哪种方式最好呢?
    我还是觉得,使用 setGlobalConfig()的方式最好,我们后面的思考和改进,都是在针对应用程序开发者会怎样使用这个Config类? 应用程序开发者在使用Config类时会有哪些可能的槽点?而发起的;于是我们站在框架设计者的角度针对可能的使用场景,做出相应的改进。

    我认为无论应用程序开发者觉得最开始的Config类是否足够使用,他都应该在应用程序代码中,主动的创建Config (或子类)对象,并调用setGlobalConfig函数,将其设置为全局单例。

    结尾

    非常感谢你能认真看完这篇文章,如果它对你有所帮助,我将倍感荣幸。

  • 相关阅读:
    初识Java 13-1 异常
    【Python】一文详细介绍 plt.rcParamsDefault 在 Matplotlib 中的原理、作用、注意事项
    vue-element-admin 常用工具、命令、安装及报错处理方法、注意事项等
    【微信小程序】关于页面中引入背景的两种方式
    免费享受企业级安全:雷池社区版WAF,高效专业的Web安全的方案
    【前端】JavaScript
    达梦数据库使用中遇到的问题和解决方案
    小程序中如何导出会员卡的档案信息
    SpringBoot SpringBoot 开发实用篇 5 整合第三方技术 5.22 RabbitMQ 安装
    数据化运营18 营收:如何通过交叉营销提升用户营收贡献?
  • 原文地址:https://blog.csdn.net/weixin_41502364/article/details/137994866