对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows 任务管理器,通常情况下,无论我们启动任务管理多少次,Windows 系统始终只能弹出一个任务管理器窗口,也就是说在一个 Windows 系统中,任务管理器存在唯一性。
回到实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。
概述
单例模式定义如下:
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。单例模式结构如图所示:
单例模式结构图中只包含一个单例角色:
- Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。
案例
Sunny 软件公司承接了一个服务器负载均衡 (Load Balance) 软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。
Sunny 公司开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器,结构图如下:
将负载均衡器 LoadBalancer 设计为单例类,其中包含一个存储服务器信息的集合 serverList,每次在 serverList 中随机选择一台服务器来响应客户端的请求,实现代码如下所示:
<?php
final class LoadBalancer
{
// 私有静态成员变量,存储唯一实例
private static $instance = null;
// 服务器集合
private $serverList = [];
// 私有构造函数
private function __construct()
{ }
// 公有静态成员方法,返回唯一实例
public static function getLoadBalancer()
{if (static::$instance == null) {static::$instance = new static();
}
return static::$instance;
}
// 增加服务器
public function addServer($server)
{$this->serverList[] = $server;
}
// 删除服务器
public function removeServer($server)
{if ($key = array_search($server, $this->serverList)) {unset($this->serverList[$key]);
}
}
// 随机获取服务器
public function getServer(): string
{$index = mt_rand(0, count($this->serverList));
return (string)$this->serverList[$index];
}
// 禁止克隆
private function __clone()
{ }
// 禁止反序列化
private function __wakeup()
{}}
总结
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
主要优点
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
主要缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言 (如 Java、C#) 的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
适用场景
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。