要点概论:
1. 理解单例设计模式
2. 单例模式中的懒汉式实例化
3. 模块级别的单例模式
4. Monostate单例模式
5. 单例和元类
6.单例模式Ⅰ
7. 单例模式Ⅱ
8. 单例模式的缺点
9. 总结
引言:
单例设计模式是应用开发过程中最简单和最著名的一种创建型设计模式。除此之外还会介绍 Monostate(或者Borg)设计模式,它是单例设计模式的一个变种。
1. 理解单例设计模式
在单例模式中,类有且只有一个特定类型的对象,并提供全局访问点。
通常用于日志记录或数据库操作,打印机后台处理程序等。
在程序运行过程中只能生成一个实例,以避免对同一资源产生相互冲突的请求。
例如:使用一个数据库对象对数据库进行操作,以维护数据的一致性;或者希望使用一个日志类的对象,将多项服务的日志信息按照顺序转储到一个特定的日志文件中。
单例设计模式的意图如下
1)确保类有且只有一个对象被创建
2)为对象提供一个访问点,以使程序可以全局访问该对象
3)控制共享资源的并行访问
(单例设计模式的 UML 类图)(百度百科链接)(UML 的 9 种图例解析)
实现单例模式的一个简单方法是,使构造函数私有化,并创建一个静态方法来完成对象的初始化。
这样,对象在第一次调用时创建,此后,这个类将返回一个对象。
PS:在 Python 中,因为它无法创建私有的构造函数,所以接下来,我们来看看 Python 语言如何实现单例模式。
上例中,通过覆盖 __new__ 方法(python 用于实例化对象的特殊方法)来控制对象的创建。并在创建之前,先检查对象是否已存在。
方法 hasattr 用于查看对象 cls 是否具有属性 instance ,该属性的作用是检查该类是否已经生成了一个对象。
可以看到,当 s1 被请求的时候,hasattr() 发现对象已经存在,所以,对象s1将被分配已有的对象实例(即原来的内存地址)。
2. 单例模式中的懒汉式实例化
懒汉化实例能确保在时机需要时才创建对象,所以,懒汉化实例化时一种节约资源并仅在需要时才创建它们的方式。
在上例中,执行 s = singleton() 的时候,它会调用 __init__ 方法,但没有新的对象被创建。实际的对象创建发生在调用 Singleton.getInstance() 的时候。
【在pycharm里用dubug跑一遍试试】
3. 模块级别的单例模式
默认情况下,所有的模块都是单例,这是由 python 的导入行为所决定的。
步骤如下:
1)检查一个 python 模块是否被导入
2)如果已经导入,则返回该模块的对象。如果还没有导入,则导入该模块,并实例化。
3)因此,当模块被导入的时候,它就会被初始化。接着当同一个模块被再次导入的时候,它会返回同一个对象。
4. Monostate 单例模式
不同实例占用不同的内存地址,但是共享相同的状态,称为 Monostate (单态)模式。
在上例中,将类变量 __shared_state 赋值给了变量 __dict__ (它时 python 的一个特殊变量,用于存储一个类所有对象的状态)。
因此,对象 b 的变量 x 发生了变化,这个变化也会复制到被所有对象共享的 __dict__ 变量,即 b1 的变量 x 也会变为 4。
除此之外,我们还可以通过修改 __new__ 方法本身来实现 Borg 模式:
5. 单例和元类
元类时一个类的类,这意味着该类时它的原来的实例。
在 python 中,一切皆对象。如果 a = 5 ,则 type(a) 返回 <type'int'>,这意味着 a 是 int 类型。但是,type(int) 返回<type'type'>,这表明存在一个元类,因为 int 是 type 类型的类。
类的定义由它的元类决定,所以当我们用类 A 创建一个类时, python 通过 A = type(name,bases,dict) 创建它。
● name:这是类的名称
● basa:这是基类
● dict:这是属性变量
下面是python 3.5 中的一个示例元类的实现:
在上例中,当我们使用 int(4,5) 实例化 int 类时, MyInt 元类的 __call__ 方法将被调用,这意味着现在元类控制着对象的实例化。
由于元类对类创建和对象实例化有更多的控制权,所以它同样可以用于创建单例(注意:为了控制类的创建和初始化,元类将覆盖 __new__ 和 __init__ 方法):
6.单例模式Ⅰ
下面通过一个数据库应用程序来展示单例的应用。
不妨以需要对数据库进行多种读取和写入操作的云服务为例进行讲解。
完整的云服务被分为多个服务,每个服务执行不同的数据库操作。针对 UI (Web应用程序)上的操作将导致调用 API ,最终产生相应的 DB 操作。
要注意的是,跨不同服务的共享资源是数据库本身。因此需要注意以下几点:
● 数据库操作的一致性,即一个操作不应与其他操作发生冲突。
● 优化数据库的各种操作,以提高内存和 CPU 的利用率。
这里提供一个python的实现:
import sqlite3 class MetaSingleton(type): __instances = {} def __call__(cls, *args, **kwargs): if cls not in cls.__instances: cls.__instances[cls] = super(MetaSingleton,cls).__call__(*args,**kwargs) return cls.__instances[cls] class Database(metaclass=MetaSingleton): connection = None def connect(self): if self.connection is None: self.connection = sqlite3.connect('db.sqlite3') self.cursorobj = self.connection.cursor() return self.cursorobj db1 = Database().connect() db2 = Database().connect() print('Database object db1',db1) print('Database object db2',db2) # 程序运行结果如下 # Database object db1 <sqlite3.Cursor object at 0x0000019321AA3180> # Database object db2 <sqlite3.Cursor object at 0x0000019321AA3180>
上例需要明白以下几点:
1)首先以 MetaSingleton 为名创建了一个远了【python 的特殊方法 __call__ 可以通过元类创建单例】。
2)数据库类由 MetaSingleton 类装饰后,其行为就会表现为单例。因此,当数据库类被实例化时,它只创建一个对象。
3)当 Web 应用程序对数据库执行某些操作时,它会多次实例化数据库类,但只创建一个对象。因为只有一个对象,所以对数据库的调用是同步的。
此外,这样还能够节约系统资源,并且可以避免消耗过多的内存或 CPU 资源。
PS:假如我们要开发的不是单个 Web 应用程序,而是集群化的情形,即多各 Web 应用共享单个数据库。
在这种情况下,每增加一个 Web 应用程序,就要新建一个单例,添加一个新的对象来查询数据库,这导致数据库操作无法同步,并且要耗费大量的资源,这时候,数据库连接池比实现单例要好得多。
7. 单例模式Ⅱ
接下来是一个为基础设施提供运行状况监控服务的例子。
在下面的代码中,hc1 和 hc2 对象与单例中的类相同。
class HealthCheck: __instance = None def __new__(cls, *args, **kwargs): if not HealthCheck.__instance: HealthCheck.__instance = super(HealthCheck,cls).__new__(cls,*args,**kwargs) return HealthCheck.__instance def __init__(self): self._servers = [] def addServer(self): self._servers.append('Server 1') self._servers.append('Server 2') self._servers.append('Server 3') self._servers.append('Server 4') def changeServer(self): self._servers.pop() self._servers.append('Server 5') hc1 = HealthCheck() hc2 = HealthCheck() hc1.addServer() print('第一次检查...') for i in range(4): print('Checking',hc1._servers[i]) hc2.changeServer() print('第二次检查') for j in range(4): print('Checking',hc2._servers[j]) # 程序运行结果如下: # 第一次检查... # Checking Server 1 # Checking Server 2 # Checking Server 3 # Checking Server 4 # 第二次检查 # Checking Server 1 # Checking Server 2 # Checking Server 3 # Checking Server 5
在上例中,使用 addServer() 方法将服务器添加到基础设施中,以进行运行状况检查。
首先,通过迭代对这些服务器的运行状况进行检查。之后,changeServer() 方法会删除最后一个服务器【模拟排除故障服务器】,并向计划进行运行状况检查的基础设施中添加一各新服务器。
因此,当运行状况检查进行第二次迭代时,它会使用修改后的服务器列表。
8. 单例模式的缺点
由于单例模式具有全局访问权限,因此可能会出现以下问题:
1)全局变量可能在某处已经被误改,但是开发人员仍然认为它们没有发生变化,而该变量还在应用程序的其他位置将被使用。
2)可能会对同一个对象创建多个引用。由于单例只创建一个对象,因此这种情况下会对同一个对象创建多个引用。
3)所有依赖于全局变量的类都会由于一个类的改变而紧密耦合为全局数据,从而可能在无意中影响另一个类。
9. 总结
1)在许多实际应用程序中,我们只需要创建一个对象,如线程池,缓存,对话框,注册表设置等。否则会导致资源的过度使用。
2)单例模式能够在不带来太多缺陷的情况下提供全局访问点。
3)但是,当使用全局变量或类的实例化非常耗费资源并且最终没有用到它们的情况下,单例的影响可以忽略不计。