单例模式

本文档用于说明 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 中:

  • 单例是工具
  • 不是默认解法
  • 更不是架构偷懒的借口

只有当对象 语义上全局唯一 时,才值得使用单例。

否则,请用普通对象、组件或依赖注入。