模式补全
这一节按 八面剖析以理解一个概念 / ljg-learn 的思路整理为 Markdown 版本:先定锚,再用八个方向切开概念,最后压缩成公式、例子、类图和检验题。
定锚
单例模式(创建型)的通行定义:保证一个类只有一个实例,并提供全局访问点。
常见误解:单例不是全局变量的漂亮名字;滥用单例会隐藏依赖、污染测试。
核心词素:Singleton = single + ton,强调唯一性和访问入口。
八刀
历史
它来自日志器、配置中心、连接池等共享资源场景。后来因为全局状态副作用,成为最需要克制使用的模式之一。
辩证
反面是随处创建多个共享资源实例。更高理解是:单例只适合确实存在唯一语义的对象。
现象
像整栋楼只有一个总电闸,所有房间都可以找到它,但不能各自造一个。
语言
Singleton = single + ton,强调唯一性和访问入口。
形式
instance = Singleton.getInstance()。当对象有用户态、请求态或测试态变化时,这个公式会制造耦合。
存在
它让人正视“唯一资源”的存在,也提醒人警惕全局可变状态。
美感
它美在孤峰:所有路径最终汇到同一个实例。
元反思
我们用“唯一”隐喻理解它,容易忽略生命周期。换成“公共基础设施”隐喻,会更自然地考虑初始化、释放和测试替换。
内观
我只有一个。你可以找我,但不要把所有东西都塞给我。我的存在应该来自领域约束,而不是偷懒。
八刀共同指向的深层结构:单例模式不是为了“炫技”,而是在某个变化点上建立边界,让稳定部分继续稳定,让变化部分有自己的位置。
压缩
公式:单例 = 私有构造 + 静态实例 + 全局访问点
一句话:单例适合表达真正唯一的资源,不适合掩盖依赖传递。
结构图:
Client -> Singleton.getInstance() -> instance动机/意图
当某个对象代表系统中真正唯一的资源或协调中心时,多实例会导致状态冲突、重复连接或全局配置不一致。单例模式的意图是限制类只能产生一个实例,并提供一个稳定访问点。
结构/角色
Singleton:单例类,隐藏构造入口,保存唯一实例。getInstance():全局访问点,负责懒加载或返回已存在实例。Client:客户端通过访问点取得实例。Instance:唯一对象本身,承载全局状态或唯一资源。
典型 UML
classDiagram class Client class Singleton { -instance: Singleton -Singleton() +getInstance(): Singleton +operation(): void } Client ..> Singleton : getInstance() Singleton --> Singleton : instance
使用场景
- 资源天然唯一,例如进程内配置中心、日志器、连接管理器。
- 多实例会破坏一致性或造成资源浪费。
- 需要延迟初始化一个重资源对象。
- 注意不要把单例当作全局变量替代品,尤其在测试和依赖注入场景中要谨慎。
正例:TypeScript
class IdentityCardNoGenerator {
private static instance: IdentityCardNoGenerator | undefined
private currentNo = 100000
private constructor() {}
static getInstance() {
if (!this.instance) {
this.instance = new IdentityCardNoGenerator()
}
return this.instance
}
generateNumber() {
this.currentNo += 1
return `ID${this.currentNo}`
}
}
class Person {
private idCardNo?: string
constructor(public readonly name: string) {}
applyIdCard() {
if (!this.idCardNo) {
const generator = IdentityCardNoGenerator.getInstance()
this.idCardNo = generator.generateNumber()
}
return this.idCardNo
}
}
const zhangSan = new Person("Zhang San")
const firstApply = zhangSan.applyIdCard()
const reissueApply = zhangSan.applyIdCard()
console.log(firstApply) // ID100001
console.log(reissueApply) // ID100001正例:UML 类图
classDiagram class IdentityCardNoGenerator { -instance: IdentityCardNoGenerator -currentNo: number -IdentityCardNoGenerator() +getInstance(): IdentityCardNoGenerator +generateNumber(): string } class Person { -idCardNo: string +name: string +applyIdCard(): string } IdentityCardNoGenerator --> IdentityCardNoGenerator : static instance Person ..> IdentityCardNoGenerator
反例:TypeScript
class Person {
constructor(public readonly name: string) {}
applyIdCard() {
return this.generateNumber()
}
private generateNumber() {
return `ID${Math.floor(Math.random() * 1000000)}`
}
}
const liSi = new Person("Li Si")
const firstApply = liSi.applyIdCard()
const reissueApply = liSi.applyIdCard()
console.log(firstApply) // 可能是 ID483920
console.log(reissueApply) // 可能又变成另一个号码反例:UML 类图
classDiagram class Person { +name: string +applyIdCard(): string -generateNumber(): string } Person ..> Person : generate each time
案例
在现实生活中,居民身份证号码具有唯一性,同一个人不允许有多个身份证号码,第一次申请身份证时将给居民分配一个身份证号码,如果之后因为遗失等原因补办时,还是使用原来的身份证号码,不会产生新的号码。现使用单例模式模拟该场景
在操作系统中,打印池(Print Spooler)是一个用于管理打印任务的应用程序,通过打印池用户可以删除、中止或者改变打印任务的优先级,在一个系统中只允许运行一个打印池对象,如果重复创建打印池则抛出异常。现使用单例模式来模拟实现打印池的设计。
掌握检验
- 如何判断一个对象是真的需要唯一实例?
- 单例为什么会让单元测试变难?
- 依赖注入容器里的 singleton 生命周期和单例模式有什么不同?