在上一节。我们总结了设计单例时要做到的原则

  1. 严格控制对外暴露的API,与外界交互只通过静态方法。
  2. 严格控制单例的使用范围
  3. 严格控制单例对象的生命周期(创建、初始化、销毁)

这一节我们重点讨论第二条

严格控制单例的使用范围

严格控制单例的使用范围,要做到这一点,只需要记住一句话就好:不要让单例类的职责太多

在设计单例类不要提供太多API,要非常谨慎地提供API。如果一个逻辑需要调用3个API完成,那么我们就要考虑,可否把这三个API合并成一个API,而把原来地三个API迁移到mInstance内部实现(参见上一节的示例代码)。

严格控制单例的使用范围,一般体现于类的命名上。

比如GameManager,这个单例的作用范围,可能是玩家开始游戏的阶段(开始、暂停、继续、游戏结束)等,但是它的名字太大了,类的名字应该体现类的职责。GameManager名字太模糊,要进一步明确,比如GameStateManager/GameStatusManager,来做游戏状态的管理,GamePlayManager,游戏玩法的管理等等。

再比如,QFramework的UIKit,其作用范围比较广,只要是UI界面相关的逻辑它都管,但是它是框架层的工具。

作为框架的部分,对单例设计的要求不太一样,对于整体框架来说,提供的概念应该尽可能少,UIKit一个类搞定一切,而不是UI层级管理用UILayerMgr,UI生命周期管理用UILifeMgr,UI事件管理用UIEventMgr,减轻使用负担。

项目结构

对于决定版架构来说,UI/Logic是表现层,业务模块可以视为Model层,框架/公共模块/工具是Utility层

这张图为一般的单例项目结构,基本上分为三层。

  1. UI/Logic控制逻辑、表现:大量的死逻辑无法复用,都是一些控制脚本还有UI界面的控制逻辑,主要处理玩家的输入和控制游戏的流程
  2. 业务模块:一些业务模块/系统,有一定的复用性,比如GameManager,可以在多个脚本调用。
  3. 框架/公共模块/工具:第三方插件、原生SDK、跨项目复用的框架、公共模块等,为项目提供最基本的支持。

我们可以把每个业务模块设计成一个单例,这样使用的范围就能确定,比如商店模块,装备模块、战斗模块等,对应的名字应该为ShopManager、EquipManager、BattleManager等。

而每个业务模块提供极少的API,比如:

  • ShopManager

    • Buy(int id):买物品
    • Sell(int id): 卖物品
  • EquipManager

    • Euip(position,id):穿装备
    • UnEquip(position,id):卸装备
  • BattleManager

    • Attack(id):攻击
    • Defence:防御

    如果每个模块能够设计成这样,能够提供极少数的API就能实现模块本身的功能,那么整个项目结构则会非常清晰,而使用单例的危害会讲到最低,项目的结构会一直保持一个比较健康的状态。

    模块之间可以有依赖,但是要尽量保持树形(自顶向下,单向依赖),而不是互相依赖,OK,这是关于模块划分的内容,我们以后会重点讲这个部分的内容。

    小结

    严格控制单例的使用范围,即单例的职责要尽可能地少,类名要准确描述其职责,不能太大,框架层例外

    决定版补充

    我们目前接触的决定版架构,分为四个层级:

    • 表现层
    • 系统层
    • 模型层
    • 工具层

    而单例的提供一般是提供两种类型的单例:

    • C#类单例
    • Mono单例

    在决定版架构中,C#类的单例就完全没有必要使用了,因为我们可以用架构中的系统层来替代,架构本身就是一个像单例一样的东西,而且也完全符合上一篇介绍的单例的最佳实践。

    但是Mono单例并不能完全抛弃掉,因为Mono单例确实在Unity里很方便,它可以提供生命周期委托,也可以作为GameObject的管理器,也可以开启协程,带来了很多便利。这些提供的功能,很明显都是表现层需要用到的功能。

    所以Mono单例我们把它算作我们架构中表现层的内容,它像其他表现层一样,只需要实现一个IController接口就可与我们架构的底层进行交互。这样既保留了Mono单例的优点,有削弱了Mono单例的职责,从而改善Mono单例职责过大的问题。相对地,我们的决定版架构几乎都是C#类,并没有太多与Mono相关的内容,如果一个项目只使用决定版架构,那么表现层会有很多工作是比较难进行的,因为一般的项目需要通过Mono单例提供统一的生命周期函数、提供协程、提供GameObject管理功能等,而将Mono单例算作决定版架构中的表现层,恰好填补了这块空缺。

    下图是Mono单例配合架构的简单用例

    Mono单例配合架构用例

Mono单例可以提供很多便利,而使用了决定版架构,可以避免单例职责过大,再加上一篇介绍的单例最佳实践,可以获得Mono单例的便利的同时还可以降低Mono单例的风险。