GoF解释:
确保类只有一个实例对象,并提供一个全局方法去调用这个对象。
模式说明:
单例模式通常被使用于一些全局一定唯一的,只关心调用功能,而不关心这个从哪里生成的类,并且这个类通常不应该参与到实际游戏交互子系统的协同工作,负责的功能大量单一且需要统一调度。在项目中,符合这个需求的一般是工具类,如音效管理,UI管理,本地表数据读取等等。
案例说明:音效管理,现有MusicManager类统一管理音效的播放
1.不使用单例:每个对象都需要持有一个MusicManager对象,这个对象可以是自己New出来,也可以是初始化时使用依赖注入的方法传入。这样我们每个需要使用音效播放的类都需要维护一个MusicManager对象。会让代码变得繁琐,而且稍有不慎就会多创建MusicManager对象,这样唯一性被破坏。有些需要全局统一调度的流程就不在起效。比如为了优化,同一时间播放的同种音效个数有上限,一旦多加入一个MusicManager对象,那么这个上限就会被破坏。代码所需的严谨程度增加,但是维护成本也增加了。
假设使用依赖注入得到音效管理类
class Hero{
private MusicManager mMusicManager ;
public Hero(MusicManager musicManager){
mMusicManager = musicManager;
}
void Scream(){
mMusicManager.playSound('Scream');
}
}
这样的代码阅读和维护都有一点难度
2.使用单例模式:首先创建MusicManager单例
class MusicManager{
private static MusicManager instance = null;
public static MusicManager Instance{
get{
if(instance==null){
instace = new MusicManager();
}
return instace;
}
}
//为了保证全局唯一,私有化构造函数
private MusicManager(){ }
public void playSound(string soundName ){
//TODO
}
}
Hero调用实现
class Hero{
void Scream(){
MusicManager.Instance.playSound('Scream');
}
}
代码一下精简了很多,也不需要多维护一个变量。
提到了单例一般还会提到单例模版,但是由于并不建议大量的使用单例,所以单例模版的意义也就很小了。这里也介绍一下,并且会看一下上面的音效单例,其实还有一个问题,线程安全,当单例没有生成的时候,两个线程同时去调用MusicManager单例会出现什么情况?会同时生成两个单例对象。所以下面基于线程安全的情况下介绍单例模版。
public class Singleton<T> where T:class ,new(){
private static readonly object syslock = new object();
private static T instance = null;
public static T Instance{
get{
if(instance == null){//两层空判断,保证在安全模式下生成单例对象之后,没有额外消耗
lock (syslock){
//先获得锁,在判断是否需要new对象,
//但是每次都锁再去判断,是会增加消耗的,所以在外面再加一层空判断,
if(instance == null){
instance = new T();
}
}
}
return instance;
}
}
}
此时音乐单例的实现:
public class MusicManager:Singleton<MusicManager>{
public void playSound(string soundName){
//TODO
}
}
这样代码又精简了不少,但是使用单例模版又会产生一个很严重的问题:使用泛型T制作单例模版,意味着类需要有公共的无参构造函数。无法私有化构造函数,也就无法从代码层面强制规范全局只有一个,协同开发中需要额外的协调说明。这个如果有解决方法,也可以在评论中告诉博主。
关于单例模式是否应该应用于项目,不同的开发者有不同的结论,因为滥用单例会造成项目设计思考不周,破坏项目结构合理性问题。但是在某些模块适当的使用单例,同样也能精简代码,降低耦合。我的结论是结合项目整体情况使用、慎用。