01意图
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
02问题
单例模式同时解决了两个问题, 所以违反了_单一职责原则_:
1. 保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
客户端甚至可能没有意识到它们一直都在使用同一个对象。
2. 为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例。
03解决方案
所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有, 防止其他对象使用单例类的
new
运算符。 - 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
04真实世界类比
政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
05单例模式结构
06伪代码
在本例中, 数据库连接类即是一个单例。 该类不提供公有构造函数, 因此获取该对象的唯一方式是调用 获取实例
方法。 该方法将缓存首次生成的对象, 并为所有后续调用返回该对象。
// 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处 // 都能访问相同的数据库连接实例。 class Database is // 保存单例实例的成员变量必须被声明为静态类型。 private static field instance: Database // 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构 // 造方法。 private constructor Database() is // 部分初始化代码(例如到数据库服务器的实际连接)。 // ... // 用于控制对单例实例的访问权限的静态方法。 public static method getInstance() is if (Database.instance == null) then acquireThreadLock() and then // 确保在该线程等待解锁时,其他线程没有初始化该实例。 if (Database.instance == null) then Database.instance = new Database() return Database.instance // 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。 public method query(sql) is // 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以 // 在这里添加限流或缓冲逻辑。 // ... class Application is method main() is Database foo = Database.getInstance() foo.query("SELECT ...") // ... Database bar = Database.getInstance() bar.query("SELECT ...") // 变量 `bar` 和 `foo` 中将包含同一个对象。
07应用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
- 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
- 如果你需要更加严格地控制全局变量, 可以使用单例模式。
- 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
- 请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改
获取实例
方法, 即 getInstance 中的代码即可实现。
08单例模式优缺点
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
- 违反了_单一职责原则。 该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
09Python、Java 代码示例
详见次条文章
09
10、推荐UML使用工具
亿图图示
关注公众号:全栈芬达,回复:亿图图示,获取激活版。
初学者秒会的专业级UML图绘制软件。无需掌握复杂操作,可以零基础轻松绘制280+种绘图类型
Visio
11、UML 类图关系
UML类图非常简单,可以用下面的图表示一个类:
该图表示一个叫做Person的类,该类有name、age、sex三个private属性,每个属性的类型紧跟在冒号的后面。该类有walk和speak两个方法,其中walk方法是public的,而speak方法是protected的,两个方法的返回值类型紧跟在冒号的后面。
+:公有属性,其它类可以访问该属性
-:私有属性,不能被其它类访问(默认为私有)
\#:保护属性,只能被本类及其派生类访问
~:包内可见,可以被本包中的其它类访问
如果要表示一个接口,则用下面的图表示:
下面介绍类与类之间的关系。如果按照关系的紧密程度从弱到强划分,类与类之间的关系包括:
- 依赖
- 关联
- 聚合
- 组合
- 实现
- 继承
依赖关系
依赖关系是所有类间关系中最弱的一种,它用下面的图表示:
图中的箭头方向表示依赖的方向,上图表示类A依赖类B。
依赖,顾名思义表示一个实体的存在必须依赖另一个实体的存在。可以这样认为,如果类A依赖类B,那么类A只有在类B存在的情况下,才能编译通过。
下面代码是依赖的一个例子:
public class UserController { private UserService userService; public User query(Strint userId) { User user = userService.queryUser(userId); return user; } }
在这段代码,UserController类同时依赖于UserService和User两个类,可以用下面的类图表示它们的依赖关系:
可见依赖关系大量的存在于我们的代码中,但千万不要在项目设计时将全部的依赖关系都画出来,这不仅很累,而且也没有必要。当梳理依赖关系时,先要搞清楚你关注什么,想表达什么,只画出真正需要画的就可以。
关联关系
关联关系表示两个实体间存在一定的联系,这种联系比依赖关系更紧密,不仅仅只是“两个实体触碰到”这样松散的关系。例如Student和School这两个类,一个学生一定会有一个对应的学校,那么Student和School间就存在关联关系,且它们的关系是一对多的。
用下面的UML图表示:
关联关系也可以用于领域建模,例如要设计一个骰子游戏,游戏者连续投掷两次筛子,如果两次点数的总数是7,则游戏者赢,否则游戏者输。可以用下面UML图对这个问题进行领域建模,各实体间使用的就是关联关系。这也是关联关系的一种特殊用法。
聚合&组合
聚合也是一种关联关系,但是这种关联关系存在整体与部分的语义。例如大雁和大雁群,一只大雁是整个大雁群的一部分。这就是一种聚合关系,具有has-a的语义。下面的UML图用来描述聚合关系。
组合是一种强聚合关系,它表示整体和部分之间具有相同的生命周期,同生共死。例如鸟和翅膀,鸟如果死掉了,那么它的翅膀也会跟着死掉。组合关系具有contains-a的语义。下面的UML图用于表达组合关系。
记忆聚合和组合UML图画法的小技巧:菱形就相当于一个容器,容器指向的实体就是整体,所以上面图中的菱形分别指向大雁群和鸟。此外,由于组合关系的紧密程度比聚合关系更强,所以组合关系用实心菱形,聚合关系用空心菱形。
继承&实现
继承和实现都是Java中的基础,比较容易理解,它们是类与类之间关系最强的。分别用下面的UML图表示。
继承示例:
实现示例:
PS:实现关系应该用空心箭头