創(chuàng)建型模式主要解決對象的創(chuàng)建問題,封裝復(fù)雜的創(chuàng)建過程,解耦對象的創(chuàng)建代碼和使用代碼。
單例模式
單例設(shè)計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類,這種設(shè)計模式就叫作單例設(shè)計模式,簡稱單例模式。
為什么要使用單例?
- 處理資源訪問沖突
- 表示全局唯一類
如何實現(xiàn)一個單例?
- 餓漢式。
餓漢式的實現(xiàn)方式,在類加載的期間,就已經(jīng)將 instance 靜態(tài)實例初始化好了,所以,instance 實例的創(chuàng)建是線程安全的。不過,這樣的實現(xiàn)方式不支持延遲加載實例。
- 懶漢式。
懶漢式相對于餓漢式的優(yōu)勢是支持延遲加載。這種實現(xiàn)方式會導(dǎo)致頻繁加鎖、釋放鎖,以及并發(fā)度低等問題,頻繁的調(diào)用會產(chǎn)生性能瓶頸。
- 雙重檢測。
雙重檢測實現(xiàn)方式既支持延遲加載、又支持高并發(fā)的單例實現(xiàn)方式。只要 instance 被創(chuàng)建之后,再調(diào)用 getInstance() 函數(shù)都不會進入到加鎖邏輯中。所以,這種實現(xiàn)方式解決了懶漢式并發(fā)度低的問題。
- 靜態(tài)內(nèi)部類。
利用 Java 的靜態(tài)內(nèi)部類來實現(xiàn)單例。這種實現(xiàn)方式,既支持延遲加載,也支持高并發(fā),實現(xiàn)起來也比雙重檢測簡單。
- 枚舉。
最簡單的實現(xiàn)方式,基于枚舉類型的單例實現(xiàn)。這種實現(xiàn)方式通過 Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性。
單例存在哪些問題?
- 單例對 OOP 特性的支持不友好
- 單例會隱藏類之間的依賴關(guān)系
- 單例對代碼的擴展性不友好
- 單例對代碼的可測試性不友好
- 單例不支持有參數(shù)的構(gòu)造函數(shù)
單例有什么替代解決方案?
通過工廠模式、IOC 容器(比如 Spring IOC 容器)來保證全局唯一類,亦可由程序員自己來保證(自己在編寫代碼的時候自己保證不要創(chuàng)建兩個類對象)。
如何實現(xiàn)線程唯一的單例?
通過一個 HashMap 來存儲對象,其中 key 是線程 ID,value 是對象。這樣就可以做到,不同的線程對應(yīng)不同的對象,同一個線程只能對應(yīng)一個對象。實際上,Java 語言本身提供了 ThreadLocal 并發(fā)工具類,可以更加輕松地實現(xiàn)線程唯一單例。
工廠模式
第一種情況:代碼中存在 if-else 分支判斷,動態(tài)地根據(jù)不同的類型創(chuàng)建不同的對象。針對這種情況,我們就考慮使用工廠模式,將這一大坨 if-else 創(chuàng)建對象的代碼抽離出來,放到工廠類中。
還有一種情況,盡管不需要根據(jù)不同的類型創(chuàng)建不同的對象,但是,單個對象本身的創(chuàng)建過程比較復(fù)雜。在這種情況下,我們也可以考慮使用工廠模式,將對象的創(chuàng)建過程封裝到工廠類中。
簡單工廠(Simple Factory)
將代碼中根據(jù) if-else 分支判斷,動態(tài)地創(chuàng)建不同的對象的邏輯剝離到一個獨立的類中,讓這個類只負責(zé)對象的創(chuàng)建。
工廠方法(Factory Method)
利用多態(tài)和 Map 將簡單工廠中的 if-else 邏輯去掉。
抽象工廠(Abstract Factory)
一個工廠負責(zé)創(chuàng)建多個不同類型的對象。
判斷要不要使用工廠模式的最本質(zhì)的參考標(biāo)準(zhǔn):
- 封裝變化:創(chuàng)建邏輯有可能變化,封裝成工廠類之后,創(chuàng)建邏輯的變更對調(diào)用者透明。
- 代碼復(fù)用:創(chuàng)建代碼抽離到獨立的工廠類之后可以復(fù)用。
- 隔離復(fù)雜性:封裝復(fù)雜的創(chuàng)建邏輯,調(diào)用者無需了解如何創(chuàng)建對象。
- 控制復(fù)雜度:將創(chuàng)建代碼抽離出來,讓原本的函數(shù)或類職責(zé)更單一,代碼更簡潔。
依賴注入框架
依賴注入框架,或者叫依賴注入容器(Dependency Injection Container),簡稱 DI 容器。
DI 容器底層最基本的設(shè)計思路就是基于工廠模式的。DI 容器相當(dāng)于一個大的工廠類,負責(zé)在程序啟動的時候,根據(jù)配置(要創(chuàng)建哪些類對象,每個類對象的創(chuàng)建需要依賴哪些其他類對象)事先創(chuàng)建好對象。當(dāng)應(yīng)用程序需要使用某個類對象的時候,直接從容器中獲取即可。
一個簡單的 DI 容器的核心功能一般有三個:配置解析、對象創(chuàng)建和對象生命周期管理。
Builder 模式
如果一個類中有很多屬性,為了避免構(gòu)造函數(shù)的參數(shù)列表過長,影響代碼的可讀性和易用性,我們可以通過構(gòu)造函數(shù)配合 set() 方法來解決。但是,如果存在下面情況中的任意一種,我們就要考慮使用建造者模式了。
我們把類的必填屬性放到構(gòu)造函數(shù)中,強制創(chuàng)建對象的時候就設(shè)置。如果必填的屬性有很多,把這些必填屬性都放到構(gòu)造函數(shù)中設(shè)置,那構(gòu)造函數(shù)就又會出現(xiàn)參數(shù)列表很長的問題。如果我們把必填屬性通過 set() 方法設(shè)置,那校驗這些必填屬性是否已經(jīng)填寫的邏輯就無處安放了。
如果類的屬性之間有一定的依賴關(guān)系或者約束條件,我們繼續(xù)使用構(gòu)造函數(shù)配合 set() 方法的設(shè)計思路,那這些依賴關(guān)系或約束條件的校驗邏輯就無處安放了。
如果我們希望創(chuàng)建不可變對象,也就是說,對象在創(chuàng)建好之后,就不能再修改內(nèi)部的屬性值,要實現(xiàn)這個功能,我們就不能在類中暴露 set() 方法。構(gòu)造函數(shù)配合 set() 方法來設(shè)置屬性值的方式就不適用了。
原型模式
如果對象的創(chuàng)建成本比較大,而同一個類的不同對象之間差別不大(大部分字段都相同),在這種情況下,我們可以利用對已有對象(原型)進行復(fù)制(或者叫拷貝)的方式來創(chuàng)建新對象,以達到節(jié)省創(chuàng)建時間的目的。這種基于原型來創(chuàng)建對象的方式就叫作原型設(shè)計模式(Prototype Design Pattern),簡稱原型模式。
原型模式的兩種實現(xiàn)方法
原型模式有兩種實現(xiàn)方法,深拷貝和淺拷貝。淺拷貝只會復(fù)制對象中基本數(shù)據(jù)類型數(shù)據(jù)和引用對象的內(nèi)存地址,不會遞歸地復(fù)制引用對象,以及引用對象的引用對象。而深拷貝得到的是一份完完全全獨立的對象。所以,深拷貝比起淺拷貝來說,更加耗時,更加耗內(nèi)存空間。