单例模式
本文档用于说明 Fink Framework 中对「单例模式」的统一设计理念、使用场景划分以及推荐实现方式。
单例在框架中属于基础设施级别的存在,主要用于管理全局状态、系统服务与跨场景生命周期对象。
因此,本框架明确区分不同形态的单例,避免“一种 Singleton 用到死”的设计反模式。
1. 单例模式的意义
在 Unity 项目中,以下对象天然具备“全局唯一”的语义:
- 系统级管理器(Event / Audio / Input / Setting)
- 服务型模块(Localization / Save / ResManager)
- 生命周期跨 Scene 的核心对象
如果不使用单例,通常会导致:
- 多实例状态冲突
- 生命周期不可控
- 依赖注入成本过高
- 使用方需要自行维护引用关系
因此,单例在框架层是合理且必要的。
2. Fink Framework 中的单例分类
Fink Framework 不采用“万能单例”,而是按 生命周期与创建方式 进行区分。
纯逻辑单例(不继承 Mono)
适用于不依赖 Unity 生命周期、不访问场景对象的模块。
特征:
- 不继承
MonoBehaviour - 使用私有无参构造函数
- 通过
Singleton<T>获取实例 - 可用于任意线程(不操作 Unity API)
典型用途:
- 数据管理器
- 配置系统
- 纯计算 / 逻辑服务
自动挂载型 Mono 单例
适用于全局系统级 Manager,不希望手动拖拽到场景。
特征:
- 继承
MonoBehaviour - 第一次访问
Instance时自动创建 GameObject - 自动
DontDestroyOnLoad - 场景中存在实例则优先复用
典型用途:
- EventManager
- AudioManager
- InputManager
- SettingManager
设计目标:
- 使用成本最低
- 框架即插即用
- 不依赖场景作者操作
挂载式 Mono 单例
适用于希望由场景明确控制生命周期的对象。
特征:
- 继承
MonoBehaviour - 推荐由场景手动挂载
- 允许在场景中进行参数配置
- 同时保证全局唯一性
典型用途:
- GameManager
- SceneManager
- 需要 Inspector 配置的系统
3. AutoMono 和 Mono 的意义
这是很多 Unity 项目中最容易被忽视的一点。
如果只有一种 Mono 单例,往往会出现以下问题:
- 所有系统都变成“自动创建”
- 场景结构不可读
- 隐式依赖严重
- 生命周期不可控
Fink Framework 通过 Auto / Manual 两种 Mono 单例,明确表达设计意图:
- AutoMono:系统级、框架级、不可缺失
- Mono:场景级、逻辑级、可被替换
这是一个语义层面的约束,不是技术细节。
4. 单例使用规范(强制)
推荐做法
- Manager / Service 一律使用单例
- 单例内部只做“管理”,不承载具体业务逻辑
- 对外只暴露必要接口
- 单例之间尽量避免互相在构造阶段访问
禁止做法
- 把 UI Panel 做成单例
- 在
Awake中访问其他单例的状态 - 让单例承担大量状态机逻辑
- 用单例替代模块拆分
5. 关于测试与扩展性
Fink Framework 的单例设计遵循以下原则:
- 单例 ≠ 静态类
- 单例可以被 Mock / 替换
- 单例是“访问方式”,不是“耦合手段”
在需要测试或替换实现时,可以通过:
- 接口抽象
- 子类重载
- 编辑器条件编译
实现可控扩展。
6. 总结
在 Fink Framework 中:
- 单例是工具
- 不是默认解法
- 更不是架构偷懒的借口
只有当对象 语义上全局唯一 时,才值得使用单例。
否则,请用普通对象、组件或依赖注入。