白話講解建立型設計模式:單例、原型,構建

語言: CN / TW / HK

寫在前面

  • 分享一些設計模式的筆記。陸續整理,按照設計模式型別,建立型,結構型,行為型釋出

  • 博文會用通俗的話梳理一些自己的理解,結合開發中的實際場景,

  • 關於設計模式,個人覺得,在理解上要站在問題域的角度,而不是它的實現方式,因為學完全部的設計模式,你會感覺,好像大多設計模式實現上基本一樣。往往有這一種被欺騙的感覺....哈

  • 理解不足小夥伴幫忙指正,虛心接受 ^_^

傍晚時分,你坐在屋檐下,看著天慢慢地黑下去,心裡寂寞而淒涼,感到自己的生命被剝奪了。當時我是個年輕人,但我害怕這樣生活下去,衰老下去。在我看來,這是比死亡更可怕的事。--------王小波

在 23 種設計模式中,建立型的設計模式有了 5 種,分別為:單例、原型、建造、工廠方法和抽象工廠。

今天要溫習的是前三個

單例

關於單例的實現方式,先不講,聊聊為什麼需要單例?單例的優點是什麼,有哪些地方使用了單例?

單例用通俗的話講, 即在某個作用域內,不管如何操作,某個類的例項只能是同一個 ,建立的這種類例項稱為單例模式。

為什麼需要單例?

  • 一種情況是有些例項 沒理由重新建立或者丟棄它 ,它需要一直存在著,同時,在記憶體裡面只有一個例項,可以 減少記憶體開銷 ,頻繁的建立和銷燬例項需要考慮 GC 等其他的的問題,比如常見的一些 工廠類例項 ,只是希望通過他來生成一些例項,沒必多個例項存在,沒必要建立銷燬,而且多次重建,編碼角度考慮,是很壞的編碼,比如一些 ORM 框架中生成 SqlSessionSqlSessionFactory 例項,一般使用單例模式或者靜態單例模式,粗了減少載入配置的同時,考慮資料庫連線數效能問題。

  • 二是某些例項,希望它在整個生命週期內是不可變得,全域性唯一的。不管在什麼情況下,它的構成能夠被嚴格的控制,可以始終保證他是共享且安全的。同時使用單例可以避免那些儲存唯一例項的全域性變數汙染名稱空間。比如 Java 中某個類對應的 class例項 ,都是單例模式,一個 Class 例項用於描述一個類載入到記憶體中的資料,只描述一個類,即一個類只有一個 Class 例項。同時它沒有建構函式,不能主動例項化,而是在類在載入時由 java 虛擬機器通過類載入器中的 defineClass 自動構造的。

如何實現單例

對於單例,本質的問題是如何保證 只能被例項化一次 ,所以不管如何實現都需要 建構函式私有化 .或者沒有建構函式由 JVM 自動構造

最簡單的實現是 餓漢式單例 ,singleton 作為類變數並且直接得到了初始化,即類中所有的變數都會被初始化 singleton 作為類變數在初始化的過程中會被收集進 <clinit>()方法 中,雖然這樣能夠百分之百的保證同步,但是因為不是懶載入,singleton 被載入後可能很長一段時間不被使用,即例項所開闢的空間會存在很長時間,記憶體角度考慮,不是好的實現。

private Singleton(){ }private  static final Singleton singleton = new Singleton();public static  Singleton getInstance(){        return singleton;  }

複製程式碼

所以為了實現懶載入,有了 懶漢式單例模式 ,雖然懶漢式可以保證懶載入,但是執行緒不安全, 當有兩個執行緒訪問時,不能保證單例的唯一性

private Singleton(){ }private static  Singleton singleton =null;public static  Singleton getInstance(){    if (singleton == null) {                singleton = new Singleton();    }                return singleton;}

複製程式碼

為了保證執行緒安全,有了 懶漢式+同步方法 ,即能保證懶載入,又可以保證 singleton 例項的唯一性,但是 synchronizeed 關鍵字的排他性導致 getInstance() 方法只能在同一時間被一個執行緒訪問。效能低下。

private Singleton(){ }private static  Singleton singleton =null;public static synchronized Singleton getInstance(){        if (singleton == null) {            singleton = new Singleton();        }        return singleton;}

複製程式碼

這個時候,有人對 懶漢式+同步方法 做了改進, 雙重校驗鎖單例(Double-Check)+Volatile ,當有兩個執行緒發現 singleton 為 null 時,只有一個執行緒可以進入到同步程式碼塊裡。即滿足了懶載入,又保證了執行緒的唯一性,不加 volition 的缺點,有時候可能會報 NPE,(JVM 執行指令重排序),有可能例項例項的變數未完成例項化其他執行緒去獲取到 singleton 變數。未完成初始化的例項呼叫其方法會丟擲空指標異常。

private Singleton(){ }
private static volatile Singleton singleton = null;public static Singleton getInstance() {
if (singleton == null){ synchronized (Singleton.class){ if (singleton ==null){ singleton = new Singleton(); } } } return singleton2;}

複製程式碼

有人覺得這樣寫好麻煩,有些有沒有簡單的寫法,當然有, 靜態內部類單例 ,靜態內部類的單例模式在 Singleton 類初始化時並不會建立 Singleton 例項,在靜態內部類中定義了 singleton 例項。 當給靜態內部類被主動建立時則會建立 Singleton 靜態變數,是 最好的單例模式之一 ,;類似於靜態工廠,將類的建立延遲到靜態內部類,外部類的初始化不會例項化靜態內部類的靜態變數。

private Singleton(){ } private  static class Singtetons{    private static  Singleton SINGLETON = new Singleton();   /* static {         final Singleton SINGLETON = new Singleton();    }*/
}public static Singleton getInstance(){ return Singtetons.SINGLETON;}

複製程式碼

當然還有其他的一些寫法,比如基於列舉的單例等,感興趣小夥伴可以去了解下

原型

為什麼需要原型?

原型模式 ,用通俗的話講, 即在原有的例項的基礎上建立多個新的例項,減少對多例項和複雜例項建立的記憶體消耗原型模式享元模式 有些類似,都是嘗試重用現有的同類例項,但是他們本質是不相同的,原型模式對現有例項的再加工,比如 JS 裡的原型設計,原型鏈,或者克隆當前例項;而享元模式是對現有例項的重複使用,比如 Java裡的整形池、字串池(String Pool) ,另一個角度,原型是建立型設計模式,而享元是結構型設計模式。

所以可以這樣理解記憶,如果希望重用現有例項,再加工或者直接克隆,屬於例項建立是原型模式,如果直接使用,屬於結構型享元模式。

原型模式一般用於克隆生成例項,會結合工廠模式使用,換一種角度考慮,其實和一種叫 寫時複製(copy-on-write) 的技術特別類似,在容器、虛擬化技術中都有應用。

容器技術 中,應用級別考慮,核心共用本身就是一種原型設計,同時一個執行的容器分為映象層和容器層,這裡的映象層可以理解為原型層,在對容器層的資料進行修改時,如果是 update 會把檔案複製到容器層 update,如果是新增會在容器層新增,如果刪除,會遮蔽映象層的檔案。如果讀取,會在由容器層到映象層自上而下的查詢。

虛擬化技術 中,如果系統級別考慮,硬體資源共享本身也是一種原型設計,同時 OpenStack 利用其 Glance元件 ,把虛機的分為原始後端盤和增量前端盤,這裡的原始後端盤即可以理解為原型盤,一個標準系統映象,在建立的虛機裡。當修改系統檔案的時候,會複製原始的檔案到增量盤修改。建立的檔案只在增量盤建立

原型設計有許多和 抽象工廠建造者 一樣的效果:它隱藏了具體的例項類,因此減少了例項的數目。可以在執行時刻增加和刪除例項,通過改變結構、改變值來指向新的例項。減少了子類的構造,也減少複雜例項的重複構建。

如何實現原型?

整體上講,原型設計模式的應用有兩種:

  • 第一種是通過改變結構、改變值來指向新的例項,把例項中公共的部分作為原型,比如 Js 例項中的一些事件函式,這種情況下,可以減少對同類例項中相同部分的建立,減少記憶體開銷,比如 JS 的原型設計

  • 另一種情況是複雜例項的克隆,這種情況一般用於例項構建需要依賴外部資料,涉及第三方資料,比如資料庫,IO 讀取等情況,構建相對複雜耗時,所以把整個例項作為原型,重寫克隆方法實現對例項的克隆

這裡那 JS 原生的原型模式 Demo 來看下。

    // 在原型基礎上的新例項    function Person() {         this.sex = 'man';       }    // 原型例項       Person.prototype = {          name: 'Nicholas',          age: 29,          job: "Software Engineer",          sayName : function () {              console.log(this.name)          }      }      var person1 = new Person();       person1.name = 'liruilong';      console.log(person1)           var person2 = new Person();      console.log(person2)      console.log(person1.sayName == person2.sayName);//true

複製程式碼

Person.prototype 指向原型,而 Person 本身即為原型擴充套件後的例項。通對 name 的賦值也可以看到,修改屬性會直接覆蓋原型的值。

[object Object] {  age: 29,  job: "Software Engineer",  name: "liruilong",  sayName: function () {              window.runnerWindow.proxyConsole.log(this.name)          },  sex: "man"}[object Object] {  age: 29,  job: "Software Engineer",  name: "Nicholas",  sayName: function () {              window.runnerWindow.proxyConsole.log(this.name)          },  sex: "man"}true

複製程式碼

關於重寫克隆方法的原型設計模式利用,可以通過 深度遍歷 ,或者 序列化 的方式實現,感興趣小夥伴可以下去了解下

建造者

為什麼需要建造者

建造者設計模式也被稱為為生成器模式,個人覺得,這是編碼中使用最多的一個設計模式了,用通俗的話講, 即使用多個簡單的例項一步一步構建成一個複雜的例項 ,為什麼需要建造者,通過建造者,可以 將一個複雜的構建與其表示相分離,使得同樣的構建過程可以建立不同的表示

通過一個最簡單的建造者設計模式應用體會下,比如一個 Bean 的生成,涉及的屬性較多,建構函式很不方便,可以使用建造者設計模式,通 setter 方法對屬性賦值,返回一個 this 的方式(區別於傳統的 setter 方法),每個 setter 方法的呼叫即是一次構建,返回的是不同的例項(屬性不同)。

public Bulider setD(int d) {            this.d = d;            return this;}

複製程式碼

複雜一點的,比如 Java 中 SpringBoot 系列安全元件 SpringSecurity 配置的生成,即通過建造者的設計模式構建了一個配置類,可以有選擇通過構建的方式配置鑑權,授權,比如表單驗證等,以及各種事件的處理器。關於配置的 Demo 感興趣小夥伴可以瞭解下。

開發中,需要生成一些複雜多變的東西,比如 doc,excel 等,設計策略較多,利用策略模式往往會有重疊的部分,就可以使用建造者模式來實現。比如這是一個生產中生成複雜 Excel 的 Demo: https://liruilong.blog.csdn.net/article/details/113191009

如何實現建造者

這個一個通過 建造者設計模 式生成例項的 Demo。這樣寫的好處:

  • 一是相比傳統的先使用建構函式生成例項,然後通過 setter 方法一個個賦值來講,他是一個整體,即整個構建過程是一個鏈式呼叫,傳統的構造方式,構造過程被分配到了個多個 setter 方法呼叫中,在構造過程中 Bean 可能處於一種不一致的狀態,真的出現這種情況往往很難除錯

  • 另一種情況,這樣的寫法,可以把一個 Bean 定義為 不可變得 ,而使用傳統的形式則很難實現。

package com.liruilong.common.demo; /** * @Auther Liruilong * @Date 2020/8/4 12:34 * @Description: */public class Demo {    private final int a;    private final int d;    private Demo(Bulider bulider) {        a = bulider.a;        d = bulider.d;    }    public static class Bulider {        private int a;        private int d;        public Bulider(int a) {            this.a = a;        }        public Demo build() {            return new Demo(this);        }        public Bulider setA(int a) {            this.a = a;            return this;        }        public Bulider setD(int d) {            this.d = d;            return this;        }    }     public static void main(String[] args) {        Demo build = new Demo.Bulider(3).setD(4).build();    } }

複製程式碼

嗯,建立型前三個設計模式就和小夥伴們分享到這裡,之前有時間會陸續分享剩下的 2 個,生活加油

博文參考

  • 《Java 併發程式設計詳解》

  • 《JavaScript 高階程式設計》(第 3 版)

  • 《Effective Java》 (中文版第 3 版)

  • 《Head First 設計模式》(中文版)

  • 《設計模式 可複用面向物件軟體的基礎》(中文版)