一、引言
单例,一个入门设计模式,每个码农都熟的烂在心里了。无论设计怎样的框架,你总逃避不了单例的思路。
大部分人应该和我一样,讨厌static。最直接的原因是,要多写一个修饰词;在被策划的催逼的混沌状态下,更无奈静态方法中无法访问类成员变量而飙升怒气值,急躁驱生了单例。
当项目进行到了高潮,需求突然变化,“改改改…”。变化更大的是程序员的心理,“MMP,老子刚写完就要改。。。”。
能不改么?不能~~~!
努力去翻过去的代码。。。嗡嗡响的不只是苍蝇般的催逼,你膨胀的脑袋里装的也不只是要改的内容。。。
到处是长长的 类名 + GetInstance + 方法名,马丹啊…我想静静。。。。
我这个功能变了,我想改类名,玛蛋啊,到处有引用,想静静。。。
二、单例的用处
1、保证同一个进程中只有一个实例。
单例的初心。有别的想法都是移情别恋,会带来宫斗的。
2、为了方便调用
绝大多数人的用法。一个UI,一个公共类,一个功能类。。。
三、单例的实现
1、懒汉式
线程不安全
2、饿汉式
静态初始化,线程安全。由运行环境负责初始化
3、线程安全懒汉-单锁
同样利用了静态初始的特性,可以在构造中编写复杂逻辑。
单锁实现了线程安全,但是在复杂初始化内容下,性能上有影响。
4、线程安全懒汉-双锁
5、延迟初始
实现线程安全,又可以在初始前进行一些逻辑处理
四、Unity对象单例使用注意
a)如果单例是继承自MonoBehaviour, 初始化不能再构造函数中进行,要在Awake或Start中进行,但这只能通过技巧实现运行时单例。
Unity文档提及:“避免使用构造函数 不要在构造函数中初始化任何变量,使用Awake或Start实现这个目的。即使是在编辑模式中Unity也自动调用构造函数,这通常发生在一个脚本被编译之后,因为需要调用构造函数来取向一个脚本的默认值。构造函数不仅会在无法预料的时刻被调用,它也会为预设或未激活的游戏物体调用。”
MonoBehaviour类的实例化已经被Unity托管,并且在Editor模式下启动时,回被调用两次,游戏停止也会调用一次,会导致不必要的逻辑错误。
MonoBehaviour单例实现如下:
19行、44行通过运行时异常的方式,保证单例。好在MonoBehaviour不能运行在子线程。
40行保证该对象不销毁
MonoBehaviour对象避免在子线程访问使用,如果出现子线程访问,尽量报出异常,禁止使用。
如果通过反射来调用私有构造,那就是 无下限的做法了。
单例定义好后,需要挂载到特定的GameObject。如果不需要挂在特定的GameObject上,可以使用通用的单例模板。
b)Unity通用单例模板:
需要单例化的类继承改类,并把类型作为模板参数。
不用考虑线程安全,子线程下实例化GameObject会被Unity报错。(新版本如果支持子线程访问的话,可以改进一下模板)
五、分界线
六、单例带来的便利性
1、方便控制实例数量。禁止了随意实例化,造成不必要的内存申请,同时避免逻辑错误
。
2、方便数据共享,方便通用代码的调用。
3、编写简单,方便调用。一个项目有很多模块,为了便捷,都使用单例,写起来快速无脑。
七、带来的问题
-
- 单例过多,相互调用,增加模块依赖,增加维护成本,降低代码可读性.
- 单行代码偏长:ClassName.Instance.MethodName(Param1…)。降低可读性
- 结构随性,不宜控制
- 需要转换为多例时,要重新编写调用接口和之前调用的地方
- 不方便复用和重构
- …..
八、改进方法-引入小三中介
1、分类管理。
比如表格数据,建个表格的管理类。通过该管理器进行创建和访问。
比如UI,建个UI的管理器。
角色管理器、场景管理器、AI管理器、资源管理器、任务管理器。。。。
可以大大减少单例的数量。
2、管理器也很多,需要对管理器进行管理
创建管理器的管理类,ManagerCenter。结构雏形:
九、解决调用获取问题-引入小三
1、基类是基础。引入各模块通用的基类,吧通用的功能在抽象出来。
比如在基类中定义各个管理器的的引用。
比如在基类中定义通用的调用接口。
2、定义项目唯一单例-Façade,简化框架的核心接口的调用。
把访问不方便、最基础最常用的接口。
把GameFacade的实例作为基类的属性方便子类调用,注意析构时的处理。
3、工具类中使用项目接口
1 避免使用。不建议使用。
2 特殊情况下,通过GameFacade.Instance来调用。
3 定义静态Façade类,包装GameFacade.Instance接口
十、解决模块间调用
1、细化模块粒度和大模块粘合
模块过大,不方便模块间的掉用。模块单位除了以Manager方式组织外,需要引入Controller对模块细化的编写。
还可以通过Controller粘合各个大模块。
2、订阅+通知(或隐喻事件)
使用观察者模式实现。
类似事件系统,NotificatinCenter对事件主题Subject(世界分类)进行管理,并提供便捷的接口托管。
Subject事件主题,实现树形结构,实现树形逻辑的交换。
Observer事件接收方,需要注册到事件主题。
Notification事件内容。
基本是Windows事件机制。
3、使用Command模式粘合
当要控制主体或模块较多时,而又需要规避状态的复杂性(无状态),最为适用。也可结合事件系统实现。
但在调用时回产生执行对象,一般不大量使用,或将Command装进对象池。
十一、分割下
十二、复合
回过头发现,虽然描述的系统不多,但使用起来会相互交叉,本来为了简化结构,降低耦合,却加大了编码复杂度。
这时需要对基础框架进行简化。
- 前面已经提到过使用Façade模式,封装复杂性。
- 使用代理、组合方式,让基类具有多种功能,比如让Manager、Controller托管事件系统的接口
十三、结语
自此,我们初略完成了基础框架的搭建,规避了单例的各项缺点,融入单例的优点,便捷编码。同时使结构清晰,也可以无脑编码了。
但真正在编程过程中,会有很多特殊情况,或更细节的内容。
其实大家只要记住内聚、解耦的目的,使用小三的基本方法,多去实验,就可以用实现结构清晰、简单、便于维护的代码结构
- 后续
后面打算结合实战内容,讲一下MVC这个能撑起整个结构设计思想。
在讲一下《设计模式的本质》,《状态模式、表驱动与状态机》。