這一篇文章是關於設計模式大冒險系列的第四篇文章,這一系列的每一篇文章我都但願可以經過通俗易懂的語言描述或者平常生活中的小例子來幫助你們理解好每一種設計模式。今天這篇文章來跟你們一塊兒學習一下單例模式。相信讀完這篇文章以後,你確定會有所收穫的。javascript
關於單例模式,這應該是設計模式中最簡單的一種了。你們若是學習過設計模式,可能不少設計模式長時間不用就忘記了,可是對於單例模式來講,你確定不會忘記。由於它的理論知識比較簡單,實踐起來也很方便。html
可是,你真的會正確的使用單例模式嗎?你知道單例模式在什麼狀況下使用是合適的,什麼狀況下使用會形成不少麻煩嗎?仍是你只是把它當作一個全局變量去使用,只是由於這樣開發很方便,不用寫不少的代碼。今天這篇文章咱們就來一塊兒好好學習一下單例模式。讓咱們開始吧。前端
首先咱們先來看一下單例模式的定義是什麼。所謂的單例模式,就是指對於一個具體的類來講,它有且只有一個實例,這個類負責建立惟一的實例,而且對外提供一個全局的訪問接口。vue
單例模式的UML類圖能夠用下圖表示:java
那麼咱們爲何要使用單例模式呢?舉一個生活中的場景,在平時你過馬路的時候,給你信號提示你能不能穿過馬路的交通訊號燈是否是隻有一個?由於在這種狀況下,若是同時有兩個信號燈的話,你是不知道該不應在此時穿過馬路的。git
因此類比到咱們的軟件開發中,也是這麼一個道理。在一個系統中,某種用途的實例會存在惟一的一個。這個實例可能用來保存應用中的一些狀態,或者執行某些任務。好比在前端開發中,咱們經常會使用一些應用的狀態管理庫,好比Vuex或者Redux。那麼在咱們的應用中,對於管理狀態的實例也只能有一個,若是有多個的話就會讓應用的狀態出現問題,從而致使應用發生一些錯誤。github
接下來咱們來看一下單例模式是如何實現的。經過上面的UML類圖,咱們能夠知道,對於一個類來講,咱們須要一個靜態變量來保存實例的引用,還須要對外提供一個獲取實例的靜態方法。若是使用 ES6 的類的語法來實現的話,能夠簡單的用下面的代碼來表示:web
class Singleton { // 類的靜態屬性 static instance = null; // 類的靜態方法 static getInstance() { if (this.instance === null) { this.instance = new Singleton(); } return this.instance; } } const a = Singleton.getInstance(); const b = Singleton.getInstance(); console.log(a === b); // true
上面的代碼仍是比較簡單的,相信你們看一下就知道怎麼實現了。須要注意的一點是,在類的靜態方法中,this指的是類,而不是實例。vuex
下面咱們再使用函數的方式來實現一次:redux
const Singleton = (function() { let instance; // 初始化單例對象的方法 function initInstance() { return {}; } return { getInstance() { if (instance === null) { instance = initInstance(); } return instance; }, }; })(); const a = Singleton.getInstance(); const b = Singleton.getInstance(); console.log(a === b); // true
上面這兩種方法的實現都是差很少的,你能夠根據本身的喜愛選擇不一樣的實現方式。
做爲Web前端開發者來講,由於咱們使用的開發語言基本上是JavaScript,又由於JavaScript是一種單線程語言,因此咱們通常不會遇到在多線程環境中使用單例模式會遇到的一些問題。
那麼咱們若是在多線程的環境中使用單例模式須要注意什麼呢?首先在單例尚未初始化的時候,若是有多個線程訪問建立單例模式的代碼,在沒有作額外處理的狀況下,就有可能會建立多個單例。
固然也有解決的方法,一種方法就是咱們在類初始化的時候就把單例生成了,這樣之後經過獲取單例的接口獲取到的都是最開始生成的那個單例。可是這樣就失去了延時初始化單例的好處。若是單例的初始化須要花費的資源或者時間比較少,這種方法是能夠的。反之,這樣作有就有一些浪費了。由於可能在整個應用的運行過程當中,這個單例一次也沒有被使用過。
另外一種方式就是在建立單例的時候須要加鎖,保證同時只能有一個線程在建立單例。這樣的話咱們就保證了建立的單例是惟一的。固然具體的操做還跟實現單例模式選擇的語言有關係,這裏就不在深刻討論了。
單例模式適合用在這樣的場景中:系統中須要一個惟一的對象去控制、管理和分享系統的狀態,或者執行某一個特定的任務又或是實現某一個具體的功能。在咱們的前端開發中,最多見的就是應用的狀態管理對象,好比 Vuex 和 Redux。又或者是打印日誌的對象,或者是某一個功能插件等等。總之單例模式在咱們平時的開發中仍是比較常見的。
那麼單例模式的優點有哪些呢?下面簡單列舉了一些:
雖然單例模式的優點很突出,可是它的缺點但是一點都很多,甚至有些開發者以爲它是反模式的。因此咱們使用單例模式的時候必定要好好思考一下,肯定是否是必需要使用單例模式。由於單例模式的不恰當使用會給整個應用的測試,開發和維護帶來很大的困難。咱們接下來就來看看單例模式有哪些缺點。
好比會增長代碼的耦合性,由於單例模式全局都是能夠訪問到的,那麼咱們就頗有可能在不少個地方使用這個惟一的對象,這樣也就形成了代碼的耦合。
由於程序中使用到這個單例對象的地方均可以對全局的狀態進行修改,因此一旦程序在這裏出現了問題,你可能要在不少個地方進行排查,這就增長了調試和排查問題的難度。
爲何說單例模式對測試來講是一個災難呢?由於若是代碼中使用了單例,那麼咱們須要在進行代碼測試的時候,提早把單例初始化好。這致使了咱們不可以在單例沒有初始化好的時候對代碼進行單元測試。
並且由於單例模式產生的實例只有一個,這就致使了對相同代碼進行屢次測試的時候容易出現問題,由於實例的狀態極可能在上一次測試的時候發生了改變,從而致使了下一次測試的失敗或者異常。
因此說單例模式增長了測試的難度與複雜度,增長了測試代碼的工做量。
這個比較容易理解,由於通常狀況下,對於一個類來講它只負責這個類的實例具備什麼功能;可是對於單例模式來講,單例模式的類還須要負責只可以產生一個實例。這違背了軟件設計的單一職責原則,類應該只負責其實例的具體功能,而不該該對類產生的實例個數負責。
可是對於這個缺點來講,你們可能會有不一樣的見解。顯而易見的是這樣作確實更加方便,設計實現上也相對簡單一些。
對於通常的類來講,若是咱們的類依賴了其它的類,通常狀況下,咱們能夠經過類的構造函數將依賴的類顯式的表示出來。這樣咱們在初始化具體的類的實例的時候就知道這個類須要那些依賴。
可是對於單例模式來講,它把它的依賴封裝在內部,對於外部的使用者來講它是一個黑盒。使用者並不知道初始化這個單例須要那些依賴,因此很容易在初始化單例的時候把單例所須要的依賴忘記掉,進而致使單例初始化失敗。
有時就算咱們知道了初始化單例須要那些依賴,可是這些依賴也許是有前後的順序的。咱們也很容易在導入和使用依賴的時候把順序搞錯了,從而致使單例的初始化出現問題。
從上面的內容咱們已經知道單例模式是一把雙刃劍,因此你在使用的時候必定要考慮清楚。先從場景的需求上考慮,是否是必定要使用單例模式纔可以解決當前的問題,有沒有其它的方案。若是必定要使用單例模式的話,如何規範單例模式的使用,如何在程序的開發,可維護性,可拓展性以及測試的簡易性上作好平衡,是一個值得考慮的問題。
文章到這裏就結束了,若是你們有什麼問題和疑問歡迎你們在文章下面留言,或者在這裏提出來。也歡迎你們關注個人公衆號關山不難越,獲取更多關於設計模式講解的內容。
下面是這一系列的其它的文章,也歡迎你們閱讀,但願你們都可以掌握好這些設計模式的使用場景和解決的方法。若是這篇文章對你有所幫助,那就點個贊,分享一下吧~
參考連接: