面向对象编程思想(1)--单例模式

世界上本来没有设计模式。用的人多了,也就成了设计模式。所以,我们不是严格按照它的定义去执行,可以根据自己的实际场景、需求去变通。领悟了其中的思想,实现属于自己的设计模式
你肯定有过这样的体会。某某时候,听人说起**模式。这么牛逼,回去得看看。结果仔细一看原来自己早就是这么用了,只是不知道它还有个这么高大上的名字。当然,专业的名字方便我们业内交流和教学,对技术的发展和传播起着重要的作用。

废话不多说,和我一起来学习这些高大上的术语吧。本系列《设计模式学习》,通过对传统面向对象编程语言C#和函数为第一等的元素的javascript语言来对比学习加深对设计模式的领悟和运用。

定义

单例模式
个人理解:只能存在一个实例
官方解释:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

C#代码示例

示例1

public static class Singleton
{
   //TODO
}

表激动,它确实不是我们平时使用的单例模式。它只是个静态对象。但是,我觉得也是可以当成单例来使用的。
当然,肯定有它的不足,不然也不会去搞个单例模式了。

  • 致命的缺点,不能继承类也不能实现接口。
  • 静态类中所有方法、字段必须是静态的。
  • 你无法控制它的初始化。
  • 静态类我们一般都是用来编写和业务无关的基础方法、扩展方法。而单例类是一个实例类,一般和业务相关。

示例2

public class Singleton
{
    public static Singleton singleton = new Singleton();   
}
Console.WriteLine(Singleton.singleton.Equals(Singleton.singleton));//true

其实它是个假单例

Singleton s1 = new Singleton();
Singleton s2 = new Singleton();
Console.WriteLine(s1.Equals(s2));//false

且有缺点

  • 在类被加载的时候就自动初始化了singleton
  • singleton应该被定义为只读属性

示例3

public class Singleton
{
    public static readonly Singleton singleton = new Singleton();//自读字段
    private Singleton()//禁止初始化
    {
    }
}

这是一个比较简单的单例,但是自动化初始变量还是存在

示例4

public class Singleton
{
    public static Singleton singleton = null;

    public static Singleton GetSingleton()
    {
        if (singleton == null)
        {
            singleton = new Singleton();
        }
        return singleton;
    }
    private Singleton()//禁止初始化
    {
    }
}

如此一来,我们就可以在调用GetSingleton方法的时候再去实例话了。注意:实例化之后singleton变量值不能再被GC回收了,因为它是个静态变量。
如此就算完事了吗?不,如果多线程同时执行的时候还是会出现多个实例。

public class Singleton
{
    public static Singleton singleton = null;

    public static Singleton GetSingleton()
    {
        if (singleton == null) //线程二执行到这里singleton == null为true,会继续下面实例Singleton
        {
           //线程一执行到这里
            Thread.Sleep(1000);//假设这还有段耗时逻辑(也可以理解并发极限)
            singleton = new Singleton();
        }
        return singleton;
    }
    private Singleton()//禁止初始化
    {
    }
}

所以还需要继续改进

示例5

public class Singleton
{
    public static Singleton singleton = null;
    private static object obj = new object();

    public static Singleton GetSingleton()
    {
        if (singleton == null) //下面有锁了为什么还要判断,因为锁会阻塞线程。而singleton被实例化后这个判断永远为false,不在需要锁。
        {
            lock (obj)
            {
                //这里代码只可能存在一个线程同时到达
                if (singleton == null)
                {
                    Thread.Sleep(1000);
                    singleton = new Singleton();
                } 
            } 
        }
        return singleton;
    }
    private Singleton()//禁止初始化
    {
    }
}

这就是我们常见的单例类代码了。当然你也可以改成读取属性的方式。但区别不大。

public class Singleton
{
    private static Singleton singleton = null;
    private static object obj = new object(); 
    public static Singleton Instance
    {
        get
        {
            if (singleton == null)
            {
                lock (obj)
                {
                    if (singleton == null)
                    {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        } 
    }
    private Singleton()//禁止初始化
    {
    }
}

C#使用场景

上面用了那么多的笔墨分析单例模式的使用,可是我们在什么场景下使用单例呢?
最典型的就是配置文件的读取,通常我们的配置文件是在程序第一次启动的时候读取,运行中是不允许修改配置文件的。

public class ConfigInfo
{
    private static ConfigInfo singleton = null;
    private static object obj = new object();
    public static ConfigInfo Instance
    {
        get
        {
            if (singleton == null)
            {
                lock (obj)
                {
                    if (singleton == null)
                    {
                        singleton = new ConfigInfo(); 
                        //从配置文件读取并赋值
                        singleton.Email = "zhaopeiym@163.com";
                        singleton.EmailUser = "农码一生";
                        singleton.EmailPass = "***********";
                    }
                }
            }
            return singleton;
        }
    }

    public string Email { get; private set; }
    public string EmailUser { get; private set; }
    public string EmailPass { get; private set; } 

    private ConfigInfo()//禁止初始化
    {
    }
}

调用

var emailInfo = ConfigInfo.Instance;
EmailSend(emailInfo.Email,emailInfo.EmailUser,emailInfo.EmailPass);

好了,C#中的单例模式大概就这样了。

JS代码示例

js和C#是不同的,一个是"无类"语言,一个是传统的面向对象语言。而在js中的单例就比较简单了。比如我们熟悉的window对象。
那么我们怎么在js中实现自己的单例模式呢?方法很多,先来个简单的:

示例1

var Singleton = {
    name: "农码一生",
    getName: function () {
        return this.name;
    }  
}

这就是一个最简单的单例,通过字面量创建一个对象。看着是不是非常像C#中的静态类?但是,它不存在静态类中的缺点。
继承毫无压力:

var Person = {
    age: 27
}

var Me = Person;
Me.name = "农码一生";
Me.getName = function () {
    return this.name;
}
Me.getAge = function () {
    return this.age;
}

虽然如此,但它并不完美。按理说字段不应该被外界随意修改的。可是js“无类”,更别说私有字段了。幸运的是js中有无处不在的闭包。
示例2

var Singleton = (function () {
    var name = "农码一生";   
    return {
        getName: function () {
            return name;
        }
    }
})();

如此一来,我们就实现了一个单例模式。经过前面对C#单例的分析,我们希望在使用的时候才去实例话对象怎么办?(且不要小看这个惰性加载,在实际开发中作用可大着呢。)
示例3

var Singleton = (function () {

    var Person = function () {
        this.name = "农码一生";
    }
    Person.prototype.getName = function () {
        return this.name;
    };
    var instance;
    return {
        getInstance: function () {
            if (!instance) {
                instance = new Person();
            }
            return instance;
        }
    }
})();
var person1 = Singleton.getInstance();
var person2 = Singleton.getInstance();
console.log(person1 === person2);//true

这算是js中比较标准的单例模式了。可能有同学会问,之前C#的时候我记得加了lock锁的啊。这里怎么就算比较标准了呢。不要忘记,==js天生的单线程,后台天生的多线程==。这就是区别。
为了职责的单一,我应该改写成
示例4

var Person = function () {
    this.name = "农码一生";
}
Person.prototype.getName = function () {
    return this.name;
};
    
var Singleton = (function () {
    var instance;
    return {
        getInstance: function () {
            return instance ||  (instance = new Person(););//简化if判断
        }
    }
})();

我们很多时候都会使用到单例,那我们可否把一个对象变成单例的过程抽象出来呢。如下:
示例5

//通用的创建单例对象的方法
var getSingle = function (obj) {
    var instance;
    return function () {
        return instance || (instance = new obj());
    }
};

var PersonA = function () {
    this.name = "农码一生";
}

var PersonB = function () {
    this.name = "农码爱妹子";
} 

var singlePersonA = getSingle(PersonA);//获取PersonA的单例
var singlePersonB = getSingle(PersonB);//获取PersonB的单例
var a1 = singlePersonA();
var a2 = singlePersonA();
var a3 = singlePersonB();
var a4 = singlePersonB();
console.log(a1 === a2);//true
console.log(a3 === a4);//true
console.log(a1 === a3);//false 

有没有头晕晕的,习惯就好了。你会说,我直接用最开始的全局变量字面量对象得了,可你不要忘记会造成变量名的污染。

JS使用场景

我们在做Tab也切换的时候就可以用到单例模式。在此,我们做个非单例和单例的比较
示例6非单例:

//获取tab1的html数据
var getTab1Html = function () {
    this.url = "/tab/tab1.json";
    //$.get(this.url, function (data) {
    //    //这里获取请求到的数据,然后加载到tab页面
    //}, "json");
    console.log("执行");
}

var getTab2Html = function () {
    this.url = "/tab/tab2.json";
    //$.get(this.url, function (data) {
    //    //这里获取请求到的数据,然后加载到tab页面
    //}, "json");
    console.log("执行");
} 

//点击tab1的时候加载tab1的数据
$("#tab1").on("click", function () {
    getTab1Html();
})
$("#tab2").on("click", function () {
    getTab2Html();
})

我们发现没点击一次tab的时候会请求一次后台数据,然后加载页面。这是不是有点傻。正确的姿势应该是第一次点击的时候加载,后面不在请求加载。那么我们就可以使用单例模式了。
示例7单例:

//获取tab1的html数据
var getTab1Html = function () {
    this.url = "/tab/tab1.json";
    //$.get(this.url, function (data) {
    //    //这里获取请求到的数据,然后加载到tab页面
    //}, "json");
    console.log("执行");
}

var getTab2Html = function () {
    this.url = "/tab/tab2.json";
    //$.get(this.url, function (data) {
    //    //这里获取请求到的数据,然后加载到tab页面
    //}, "json");
    console.log("执行");
} 

var loadTab1 = getSingle(getTab1Html);
var loadTab2 = getSingle(getTab2Html);

//点击tab1的时候加载tab1的数据
$("#tab1").on("click", function () {
    loadTab1();
})
$("#tab2").on("click", function () {
    loadTab2();
})

此时,我们无论点击多少此tab。它也只会在第一次点击的时候请求加载页面数据了。
 

注意:

  • JS中不建议使用全局变量来达到单例的效果
    • 其一,会引起变量名的全局污染
    • 其二,不能惰性加载。
  • C#中不建议使用静态类来达到单例的效果
    • 其一,不能继承类和接口
    • 其二,内部变量和方法必须静态。
  • 单例模式中实例变量要慎用。因为一个单例很可能被多处操作(修改了变量),从而影响的预期效果。
     

设计模式之所以能成为设计模式,也是在不断尝试、改进后得到的最佳实践而已。所以,我们不需要生搬硬套,适合的才是最好的。在此,关于单例模式的学习到此结束。谢谢您的阅读。
本文已同步至索引目录:《设计模式学习》
本文demo:https://github.com/zhaopeiym/BlogDemoCode