乐趣区

关于设计模式:百度工程师教你玩转设计模式单例模式

想要写好代码,设计模式(Design Pattern)是必不可少的基本功,设计模式是对面向对象设计(Object Oriented Design)中重复呈现的问题的解决方案,本篇从最简略的单例模式(Singleton Pattern)开讲。

单例模式属于创立型模式(Builder Pattern),用意在于保障一个类仅有一个实例,并提供一个拜访它的全局拜访点。单例模式在内存中仅创立一次对象,即便屡次实例化该类,也只返回第一次实例化后的实例对象,不仅能缩小不必要的内存开销,并且在缩小全局的函数和变量也具备重要的意义。

实现形式上,次要有懒汉式(Lazy Singleton)、饿汉式(Eager Singleton),多线程场景下须要留神线程平安。

场景上,最罕用于全局配置管理,其次在 IO 操作、前端交互等须要保障对象惟一的场景,也能够应用。

01 单例模式的实现形式

在 golang 中单例模式的实现形式有多种,这里介绍下应用 init 和 sync.Once 形式实现线程平安的单例。

其中 init 函数是在文件包首次被加载的时候执行并且只执行一次(Eager Singleton,饿汉模式),sync.Once 是在代码运行须要的时候执行且只执行一次(Lazy Singleton,懒汉模式)。init 函数形式间接创立好对象,不须要判断对象是否为空并继续占有在内存中,sync.Once 是 Go 语言实现的一种对象,其绝对高效且并发平安的实现原理次要是依赖于 sync/atomic 包的原子操作,在 sync.Once 的底层实现中,应用一个变量 done 来记录函数的执行状态,应用 sync.Mutex 和 sync.atomic 来保障线程平安的读取 done,以保障某种行为只会被执行一次。须要留神的是 once.Do(f func()) 办法不能嵌套,若 f 在执行过程中也会调用 once.Do,会导致死锁。

在 golang 一些 server 业务场景利用中,通常会用到一些 resource,如罕用的:DB、Redis、Logger 等,这些资源的实例化对象会在每个申请中频繁的应用,如果在每个申请的解决过程中频繁创立和开释这些资源对象,则会造成较大的系统资源开销,但如果应用单例的形式创立这些资源对象则能防止这些问题,通常理论应用场景中会在 main 主过程中的 HTTPServer 携程启动前,通过 init 或 sync.One 的形式创立单例对象提供各 HTTPServer 携程应用,从而保障各个申请解决过程中应用同一个实例对象。

......
var (
  oResource sync.Once

  initFuncList = []initFunc{
    mustInitLoggers,  // 初始化 Log
    mustInitServicer, // 初始化 servicer 以及 ral
    mustInitGorm,     // 初始化 mysql gorm
    mustInitRedis,    // 初始化 redis
    mustInitOther,    // 初始化 other
  }
)

type initFunc func(context.Context) func() error

// MustInit 按程序初始化 app 所需的各种资源
func MustInit(ctx context.Context) (f func() error) {oResource.Do(func() {callbackList := make([]func() error, 0, len(initFuncList))
    for _, f := range initFuncList {callbackList = append(callbackList, f(ctx))
    }

    f = func() (err error) {for i := len(callbackList) - 1; i >= 0; i-- {if callbackList[i] != nil {e := callbackList[i]()
          if err == nil {err = e}
        }
      }
      return
    }
  })

  return
}

......

02 单例模式在配置管理中的利用

在 Python 中,一个很广泛的利用场景就是利用单例模式来实现全局的配置管理。对于大部分的零碎,通常都会有一个或者多个配置文件用于寄存零碎运行时所依赖的各种配置信息,在零碎运行的过程中通过代码读取并解析配置文件从而取得对应的配置信息,而且在运行过程中当配置文件产生变更当前还须要实时更新对应的配置信息。

在这个场景外面,如果每次应用从新读取和加载配置,会有以下问题:

  • 减少耗时:带来额定的工夫开销,额定的开销工夫和读取次数成正比。
  • 减少内存:带来额定的内存开销,额定的内存占用和对象的实例个数成正比。

在这个场景外面,有一个典型的特色:须要重复获雷同配置文件的内容,配置文件的内容可能会产生变更,所以这个场景就比拟适合通过单例模式来实现。即在零碎初始化或者首次应用配置的时候加载文件并解析生成一个配置类对象,同时这个对象会实时监听文件内容变更并更新对象的对应属性,后续每次都间接应用这个对象获取文件内容即可。这样即可解决重复读取文件初始化对象以及监听文件变更所来的额定工夫和空间开销。

以下为基于 Python 实现的 Demo:

# runtime_conf.py

class RuntimeConf(object):
    """
    单例模式实现的运行时全局配置类
    1、用于解析配置文件并封装成对象属性方便使用
    2、继续监听配置文件,产生变更后自动更新实例对象属性
    """

    def __new__(cls):
        if not hasattr(cls, '_instance'):
            # 1、初始化实例对象
            cls._instance = super(RuntimeConf, cls).__new__(cls)

            # 2、加载配置文件
            cls._instance.load_config()

            # 3、继续开启一个新线程继续监听文件变动,文件产生变更当前更新实例属性
            cls._instance.__watch_async()

        return cls._instance

    def __watch_async(self):
        """
        公有的监听配置文件办法,如果配置文件产生变更,从新读取配置文件并加载到 self.__data 属性
        :return:
        """
        # 以下仅为示例思路,具体实现文件监听可复用第三方框架,例如 pyinotify
        changed = False

        # ......

        # 如果文件产生变更,从新加载
        if changed:
            self.__load_config()

    def __load_config(self):
        """
        公有读取配置文件并加载到对象属性中
        :return:
        """
        # 读取配置文件并存储到 self.__data 属性
        self.__data = {
            "key1": 1,
            "key2": 2
        }
        print("load config success")

    def get(self, key):
        """
        读取配置
        :param key:
        :return:
        """
        return self.__data.get(key, None)


if __name__ == '__main__':
    # 初始化两个对象,输入对象的内存地址,能够发现两个变量都是指向同一个内存地址,即是同一个对象
    conf_1 = RuntimeConf()
    conf_2 = RuntimeConf()
    print(conf_1)
    print(conf_2)
    print(conf_1.get("key1"))
    print(conf_2.get("key2"))

03 单例模式在 IO 操作的利用

在 PHP 中,单例模式一个典型的利用场景,就是 IO 操作,典型的有数据库、文件操作等,作用在于保护一个全局变量,去治理连贯实例。

以典型的 PHP 站点为例,在规范的 MVC 构造下,单次网络申请相应过程中,会波及到多个不同 Model 的实例化,而每个 Model 实例又须要进行数据库操作,这里就须要保护全局惟一的数据库连贯实例,个别用单例模式进行保护。如果每个 Model 在实例化时,都建设新的连贯,显然是不合理的,会有以下问题:

  • 资源节约:频繁建设连贯,减少网络耗时、CPU& 内存开销。
  • 无奈提交事务:多条 SQL 语句,不是一个连贯提交,无奈残缺的提交数据库事务。

在这个场景下,咱们能够用单例模式解决,单例类能够具备公有的构造函数,并且提供静态方法供外界获取它的实例,内部首次获取时,每次获取到的是同一个对象,由这个对象保护数据库连贯。

以下为基于 PHP 实现的 Demo:

class DBHandler {
  private static $instance = null; // 公有实例
  public $conn; // 数据库连贯

  // 公有构造函数
  private function __construct() {$this->conn = new PDO('hostname', 'account', 'password');
  }

  // 静态方法,用于获取公有实例
  public static function getInstance() {if (self::$instance == null) {self::$instance = new DBHandler();
    }
    return self::$instance;
  }
 
    public function fetch() {...}
}

class ModelA {
    private $dbHandler;
    
    public function __construct() {$this->dbHandler = DBHandler->getInstance();
    }
    
    public function getA() {return $this->dbHandler->fetch($sql);
    }
}

class ModelB {
    private $dbHandler;
    
    public function __construct() {$this->dbHandler = DBHandler->getInstance();
    }
    
    public function getB() {return $this->dbHandler->fetch($sql);
    }
}

$modelA = new ModelA();
$modelB = new ModelB();
$modelA->getA();
$modelB->getB();

04 单例模式在前端交互的利用

在前端开发中,单例模式的应用非常常见,很多第三方库和框架都利用了单例模式。比方最罕用的 js 库 jQuery,它裸露了一个 jQuery 实例,屡次援用都只会应用该实例对象。这样的模式,缩小了全局变量的创立,并且可能防止变量抵触。

实现单例模式常见的形式有:首先创立一个类,这个类蕴含一个静态方法,用于创立这个类的实例对象;还存在一个标记,标识实例对象是否曾经创立过,如果没有,则创立实例对象并返回;如果创立过,就间接返回首次创立的实例化对象的援用。

在理论利用中,咱们常应用单例模式来治理页面中的弹窗,防止页面中同时展示多个相互重叠的弹窗:能够创立一个 Toast 弹窗类,并初始化弹窗节点。这个类提供一个静态方法 getInstance 来创立并返回实例对象,这样业务在创立弹窗时就不须要再进行实例化的操作。业务能够通过 show 和 hide 办法来管制弹窗的展示和暗藏,但即便执行屡次 show 办法,也只会展示一个弹窗,因为业务应用的是同一个实例对象。这个类在页面运行时会始终存在,除非没有了对这个类的援用,它则会被垃圾回收。

以下为基于 JavaScript 实现的 Demo:

// 弹窗组件 toast.js
class Toast {constructor() {this._init();
    }
    // 公有办法,业务不容许间接调用该办法来创立弹窗
    _init(){const toast = document.createElement('div');
        toast.classList.add('toast');
        toast.innerHTML = '这是一个弹窗';
        document.body.append(toast);
    }
    show() {document.querySelector('.toast').style.display = 'block';
    }
    hide() {document.querySelector('.toast').style.display = 'none';
    }
    // 静态方法,业务操作弹窗时不须要再实例化
    static getInstance() {if(!this.instance) {this.instance = new Toast();
        }
        return this.instance;
    }
}
// 在组件中把对惟一的实例对象 loginToast 的援用裸露进来
const toast = Toast.getInstance();
export default toast;

// 业务调用
import toast from './xxx/toast';
toast.show();

———-  END  ———-

举荐浏览【技术加油站】系列:

百度程序员 Android 开发小技巧

Chrome Devtools 调试小技巧

人工智能超大规模预训练模型浅谈

退出移动版