世界上本來(lái)沒(méi)有設(shè)計(jì)模式。用的人多了,也就成了設(shè)計(jì)模式。所以,我們不是嚴(yán)格按照它的定義去執(zhí)行,可以根據(jù)自己的實(shí)際場(chǎng)景、需求去變通。領(lǐng)悟了其中的思想,實(shí)現(xiàn)屬于自己的設(shè)計(jì)模式。
你肯定有過(guò)這樣的體會(huì)。某某時(shí)候,聽(tīng)人說(shuō)起**模式。這么牛逼,回去得看看。結(jié)果仔細(xì)一看原來(lái)自己早就是這么用了,只是不知道它還有個(gè)這么高大上的名字。當(dāng)然,專業(yè)的名字方便我們業(yè)內(nèi)交流和教學(xué),對(duì)技術(shù)的發(fā)展和傳播起著重要的作用。

廢話不多說(shuō),和我一起來(lái)學(xué)習(xí)這些高大上的術(shù)語(yǔ)吧。本系列《設(shè)計(jì)模式學(xué)習(xí)》,通過(guò)對(duì)傳統(tǒng)面向?qū)ο缶幊陶Z(yǔ)言C#和函數(shù)為第一等的元素的javascript語(yǔ)言來(lái)對(duì)比學(xué)習(xí)加深對(duì)設(shè)計(jì)模式的領(lǐng)悟和運(yùn)用。

定義

單例模式
個(gè)人理解:只能存在一個(gè)實(shí)例
官方解釋:保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。

C#代碼示例

示例1

public static class Singleton{   //TODO}

表激動(dòng),它確實(shí)不是我們平時(shí)使用的單例模式。它只是個(gè)靜態(tài)對(duì)象。但是,我覺(jué)得也是可以當(dāng)成單例來(lái)使用的。
當(dāng)然,肯定有它的不足,不然也不會(huì)去搞個(gè)單例模式了。

  • 致命的缺點(diǎn),不能繼承類也不能實(shí)現(xiàn)接口。

  • 靜態(tài)類中所有方法、字段必須是靜態(tài)的。

  • 你無(wú)法控制它的初始化。

  • 靜態(tài)類我們一般都是用來(lái)編寫和業(yè)務(wù)無(wú)關(guān)的基礎(chǔ)方法、擴(kuò)展方法。而單例類是一個(gè)實(shí)例類,一般和業(yè)務(wù)相關(guān)。

示例2

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

其實(shí)它是個(gè)假單例

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

且有缺點(diǎn)

  • 在類被加載的時(shí)候就自動(dòng)初始化了singleton

  • singleton應(yīng)該被定義為只讀屬性

示例3

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

這是一個(gè)比較簡(jiǎn)單的單例,但是自動(dòng)化初始變量還是存在

示例4

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

如此一來(lái),我們就可以在調(diào)用GetSingleton方法的時(shí)候再去實(shí)例話了。注意:實(shí)例化之后singleton變量值不能再被GC回收了,因?yàn)樗莻€(gè)靜態(tài)變量。
如此就算完事了嗎?不,如果多線程同時(shí)執(zhí)行的時(shí)候還是會(huì)出現(xiàn)多個(gè)實(shí)例。

public class Singleton{    public static Singleton singleton = null;    public static Singleton GetSingleton()    {        if (singleton == null) //線程二執(zhí)行到這里singleton == null為true,會(huì)繼續(xù)下面實(shí)例Singleton
        {           //線程一執(zhí)行到這里
            Thread.Sleep(1000);//假設(shè)這還有段耗時(shí)邏輯(也可以理解并發(fā)極限)
            singleton = new Singleton();
        }        return singleton;
    }    private Singleton()//禁止初始化
    {
    }
}

所以還需要繼續(xù)改進(jìn)

示例5

public class Singleton{    public static Singleton singleton = null;    private static object obj = new object();    public static Singleton GetSingleton()    {        if (singleton == null) //下面有鎖了為什么還要判斷,因?yàn)殒i會(huì)阻塞線程。而singleton被實(shí)例化后這個(gè)判斷永遠(yuǎn)為false,不在需要鎖。
        {            lock (obj)
            {                //這里代碼只可能存在一個(gè)線程同時(shí)到達(dá)
                if (singleton == null)
                {
                    Thread.Sleep(1000);
                    singleton = new Singleton();
                } 
            } 
        }        return singleton;
    }    private Singleton()//禁止初始化
    {
    }
}

這就是我們常見(jiàn)的單例類代碼了。當(dāng)然你也可以改成讀取屬性的方式。但區(qū)別不大。

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#使用場(chǎng)景

上面用了那么多的筆墨分析單例模式的使用,可是我們?cè)谑裁磮?chǎng)景下使用單例呢?
最典型的就是配置文件的讀取,通常我們的配置文件是在程序第一次啟動(dòng)的時(shí)候讀取,運(yùn)行中是不允許修改配置文件的。

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 = "農(nóng)碼一生";
                        singleton.EmailPass = "***********";
                    }
                }
            }            return singleton;
        }
    }    public string Email { get; private set; }    public string EmailUser { get; private set; }    public string EmailPass { get; private set; } 

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

調(diào)用

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

好了,C#中的單例模式大概就這樣了。

JS代碼示例

js和C#是不同的,一個(gè)是"無(wú)類"語(yǔ)言,一個(gè)是傳統(tǒng)的面向?qū)ο笳Z(yǔ)言。而在js中的單例就比較簡(jiǎn)單了。比如我們熟悉的window對(duì)象。
那么我們?cè)趺丛趈s中實(shí)現(xiàn)自己的單例模式呢?方法很多,先來(lái)個(gè)簡(jiǎn)單的:

示例1

var Singleton = {
    name: "農(nóng)碼一生",
    getName: function () {        return this.name;
    }  
}

這就是一個(gè)最簡(jiǎn)單的單例,通過(guò)字面量創(chuàng)建一個(gè)對(duì)象。看著是不是非常像C#中的靜態(tài)類?但是,它不存在靜態(tài)類中的缺點(diǎn)。
繼承毫無(wú)壓力:

var Person = {
    age: 27}var Me = Person;
Me.name = "農(nóng)碼一生";
Me.getName = function () {    return this.name;
}
Me.getAge = function () {    return this.age;
}

雖然如此,但它并不完美。按理說(shuō)字段不應(yīng)該被外界隨意修改的。可是js“無(wú)類”,更別說(shuō)私有字段了。幸運(yùn)的是js中有無(wú)處不在的閉包。
示例2

var Singleton = (function () {    var name = "農(nóng)碼一生";   
    return {
        getName: function () {            return name;
        }
    }
})();

如此一來(lái),我們就實(shí)現(xiàn)了一個(gè)單例模式。經(jīng)過(guò)前面對(duì)C#單例的分析,我們希望在使用的時(shí)候才去實(shí)例話對(duì)象怎么辦?(且不要小看這個(gè)惰性加載,在實(shí)際開(kāi)發(fā)中作用可大著呢。)
示例3

var Singleton = (function () {    var Person = function () {        this.name = "農(nóng)碼一生";
    }
    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中比較標(biāo)準(zhǔn)的單例模式了??赡苡型瑢W(xué)會(huì)問(wèn),之前C#的時(shí)候我記得加了lock鎖的啊。這里怎么就算比較標(biāo)準(zhǔn)了呢。不要忘記,==js天生的單線程,后臺(tái)天生的多線程==。這就是區(qū)別。
為了職責(zé)的單一,我應(yīng)該改寫成
示例4

var Person = function () {    this.name = "農(nóng)碼一生";
}
Person.prototype.getName = function () {    return this.name;
};    
var Singleton = (function () {    var instance;    return {
        getInstance: function () {            return instance ||  (instance = new Person(););//簡(jiǎn)化if判斷
        }
    }
})();

我們很多時(shí)候都會(huì)使用到單例,那我們可否把一個(gè)對(duì)象變成單例的過(guò)程抽象出來(lái)呢。如下:
示例5

//通用的創(chuàng)建單例對(duì)象的方法var getSingle = function (obj) {    var instance;    return function () {        return instance || (instance = new obj());
    }
};var PersonA = function () {    this.name = "農(nóng)碼一生";
}var PersonB = function () {    this.name = "農(nóng)碼愛(ài)妹子";
} 

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);//trueconsole.log(a3 === a4);//trueconsole.log(a1 === a3);//false

有沒(méi)有頭暈暈的,習(xí)慣就好了。你會(huì)說(shuō),我直接用最開(kāi)始的全局變量字面量對(duì)象得了,可你不要忘記會(huì)造成變量名的污染。

JS使用場(chǎng)景

我們?cè)谧鯰ab也切換的時(shí)候就可以用到單例模式。在此,我們做個(gè)非單例和單例的比較
示例6非單例:

//獲取tab1的html數(shù)據(jù)var getTab1Html = function () {    this.url = "/tab/tab1.json";    //$.get(this.url, function (data) {
    //    //這里獲取請(qǐng)求到的數(shù)據(jù),然后加載到tab頁(yè)面
    //}, "json");
    console.log("執(zhí)行");
}var getTab2Html = function () {    this.url = "/tab/tab2.json";    //$.get(this.url, function (data) {
    //    //這里獲取請(qǐng)求到的數(shù)據(jù),然后加載到tab頁(yè)面
    //}, "json");
    console.log("執(zhí)行");
} 

//點(diǎn)擊tab1的時(shí)候加載tab1的數(shù)據(jù)$("#tab1").on("click", function () {
    getTab1Html();
})
$("#tab2").on("click", function () {
    getTab2Html();
})

我們發(fā)現(xiàn)沒(méi)點(diǎn)擊一次tab的時(shí)候會(huì)請(qǐng)求一次后臺(tái)數(shù)據(jù),然后加載頁(yè)面。這是不是有點(diǎn)傻。正確的姿勢(shì)應(yīng)該是第一次點(diǎn)擊的時(shí)候加載,后面不在請(qǐng)求加載。那么我們就可以使用單例模式了。
示例7單例:

//獲取tab1的html數(shù)據(jù)var getTab1Html = function () {    this.url = "/tab/tab1.json";    //$.get(this.url, function (data) {
    //    //這里獲取請(qǐng)求到的數(shù)據(jù),然后加載到tab頁(yè)面
    //}, "json");
    console.log("執(zhí)行");
}var getTab2Html = function () {    this.url = "/tab/tab2.json";    //$.get(this.url, function (data) {
    //    //這里獲取請(qǐng)求到的數(shù)據(jù),然后加載到tab頁(yè)面
    //}, "json");
    console.log("執(zhí)行");
} 

var loadTab1 = getSingle(getTab1Html);var loadTab2 = getSingle(getTab2Html);//點(diǎn)擊tab1的時(shí)候加載tab1的數(shù)據(jù)$("#tab1").on("click", function () {
    loadTab1();
})
$("#tab2").on("click", function () {
    loadTab2();
})

此時(shí),我們無(wú)論點(diǎn)擊多少此tab。它也只會(huì)在第一次點(diǎn)擊的時(shí)候請(qǐng)求加載頁(yè)面數(shù)據(jù)了。
 

注意:

  • JS中不建議使用全局變量來(lái)達(dá)到單例的效果


    • 其一,會(huì)引起變量名的全局污染


    • 其二,不能惰性加載。

  • C#中不建議使用靜態(tài)類來(lái)達(dá)到單例的效果


    • 其一,不能繼承類和接口


    • 其二,內(nèi)部變量和方法必須靜態(tài)。

  • 單例模式中實(shí)例變量要慎用。因?yàn)橐粋€(gè)單例很可能被多處操作(修改了變量),從而影響的預(yù)期效果。
     

設(shè)計(jì)模式之所以能成為設(shè)計(jì)模式,也是在不斷嘗試、改進(jìn)后得到的最佳實(shí)踐而已。所以,我們不需要生搬硬套,適合的才是最好的。在此,關(guān)于單例模式的學(xué)習(xí)到此結(jié)束。謝謝您的閱讀。