常用設計模式之白話精簡理解及應用-上

前置條件

基礎知識

學習設計模式之前我們需要具備一些基礎知識,首先需要熟悉面向對象軟體開發經歷的三個階段,即OOA(面向對象分析)、OOD(面向對象設計)、OOP(面向對象編程)。

  • OOP中有兩個最基礎概念是類和對象,還有封裝、繼承、多態、抽象四大面向對象編程的特性。
  • OOAD里需要會掌握使用UML(統一建模語言,簡單、統一、圖形化表達軟體設計中動態和靜態資訊)常用的圖如用例圖、類圖、狀態圖、活動圖、時序圖、構建圖、部署圖,UML是我們用來描述面向對象或設計模式的設計思路。

UML2.0

UML2.0包括以下14類圖,本篇只是提出但不詳細學習這些圖,需要各自掌握類、介面、類圖,並能運用自如畫圖,後續我們有時間再專門針對UML詳細展開。

image-20220102173543834

類圖表示方法

  • 類使用包含類名、屬性和方法且帶有分割線的矩陣來表示,如下圖Employee類,它包含name,age和adress這三個屬性,以及work()方法

image-20220108132443249

  • 屬性/方法名稱前加的加號和減號表示了這個屬性/方法的可見性,UML類圖中表示可見符號有三種:
    • +:表示public
    • -:表示private
    • #:表示protected
  • 屬性的完整表示方式是:
    • 可見性 名稱 : 類型 [ = 預設值 ]
  • 方法的完整表示方式是:
    • 可見性 名稱(參數列表) [ : 返回類型],注意:
    • 中括弧中的內容表示可選
    • 也有將類型放在變數名後面,返回類型放在方法名前面

類與類之間關係

在軟體設計中類與類之間關係包括泛化、 實現、組合 、 聚合 、 關聯 、 依賴 。

  • 泛化、實現依賴關係強度相同,這兩種關係也是依賴最強的系;泛化體現的是類與類的縱向關係,是父類與子類的繼承關係,是is-a關係,採用實線+空心三角箭頭;實現體現的是類與介面的縱向關係,實現採用虛線+空心三角箭頭

  • 其他四種關係體現的是類和類、或者類與介面的橫向關係,較難區分,強弱程度依次為:組合>聚合>關聯>依賴,依賴的關係程度最強。

    • 依賴關係是一種使用的關係,也是一種臨時性關係,通常在程式碼層面體現為某個類通過局部變數、方法的參數、靜態方法來調用訪問另一個類(被依賴的類)中某些方法;採用帶箭頭虛線,箭頭指向被依賴的類
    • 關聯關係是對象之間的一種引用關係,是類與類之間最常用的一種關係;分為一般關聯、組合、聚合關係;在程式碼中通常將一個類的對象作為另外一個類的成員變數(全局變數)來實現。
      • 一般關聯:也即是通常說的關聯關係,可以單向的,也可以雙向的;雙向關聯採用兩個箭頭或者沒有箭頭的實線,單向關聯採用帶一個箭頭的實線,箭頭從使用類指向被關聯的類。
      • 聚合:關聯關係中的一種,整體和部分關係,也即是has-a關係,部分可以脫離整體單獨存在。採用帶空心菱形的實線,菱形指向整體
      • 組合:也是關聯關係中的一種,整體和部分關係,也即是contains-a關係是一種更強烈的聚合關係,部分不能脫離整體單獨存在。採用帶實心菱形的實線,菱形指向整體

    下面通過一張圖涵蓋上面6種類之間的關係的運用示例並加深理解。

image-20220102235219949

七大設計原則

總體原則

軟體設計有七大設計原則,分別是開閉原則、單一職責原則、介面隔離原則、里氏替換原則、依賴倒置原則、合成復用原則、迪米特法則,設計原則作為設計模式的指導思想,而設計模式則是基於設計原則實踐發展沉澱的經驗,目的是為了提高程式可靠性、可維護性、可復用性、可擴展性,實現面向對象編程的解耦,類與類做到低耦合高內聚;軟體編程過程需綜合考慮並盡量遵守這些設計原則。引用一句口訣:訪問加限制、函數要節儉、依賴不允許、動態加介面、父類要抽象、擴展不更改。

開閉原則

定義

開閉原則(Open-Closed Principle,OCP)是規範我們程式設計師編程最基本、最重要的原則,強調對擴展開放、對修改關閉,即軟體實體應盡量在不修改原有程式碼的情況下進行擴展。開閉原則是其他設計原則的總綱,是面向對象的可復用設計的首要基石,其他設計原則本質是圍繞開閉原則在不同角度去展開。

例子

下面舉一個違反開閉原則的例子:支援顯示各種類型的圖表,如餅狀圖和柱狀圖等,類圖設計如下:

image-20220103201305985

在ChartDisplay類的display()方法中有如下if else if的處理邏輯程式碼

if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();

在上面ChartDisplay類程式碼中如果需要增加一個新的圖表類如折線圖LineChart,則需要修改ChartDisplay類的display()方法的源程式碼,增加新的判斷邏輯,這樣就違反了開閉原則;比如可採用策略模式的設計模式來解決上面這種違反開閉原則會變化if else的問題。

單一職責原則

概述

  • 單一職責(Single Responsibility Principle,SRP),一個類只有一個引起它變化的原因。
  • 主要是控制類的粒度大小實現高內聚和低耦合,它是最簡單但又是最難運用的原則,單一職責是指如何定義一個模組、類、方法和實現其封裝,可以說是比較依賴經驗。
  • 我們要根據需求和實際情況來靈活運用單一職責原則,需要設計人員有較強的面向對象業務分析能力和豐富實踐經驗進行職責分離,將不同變化原因的職責封裝到不同的類中。
  • 一個類不要太累,如果類承擔職責過多則復用的可能性就越小,且多職責耦合一起當其中一個或幾個職責發生變化可能影響其它職責的運行。
  • 在實際過程中如果生搬硬套會引起類的增多,添加額外的維護成本,其實是很難全面做到類級別單一職責原則,如果程式碼的邏輯足夠簡單時,我們可以在程式碼級別違反單一職責原則,當類中的方法數量少,並且業務邏輯不是特別複雜時,可以降級到方法級別單一原則。

反例

吃類Eat.java,如果後續增加吃的方式那麼就需要在Eat類修改,這樣就有可能影響了原有功能,違反開閉原則。

package cn.itxs.principle.srp;

public class Eat {
    public void doEat(String animal){
        if ("獅子".equals(animal)){
            System.out.println(animal+"在大口吃肉!");
        }else if ("黃牛".equals(animal)) {
            System.out.println(animal+"在細嚼慢咽吃草!");
        }
    }
}

測試類SrpMain.java

package cn.itxs.principle.srp;

public class SrpMain {

    public static void main(String[] args) {
        Eat animal = new Eat();
        animal.doEat("獅子");
        animal.doEat("黃牛");
    }
}

正例

  • 類級別

吃肉類EatMeet.java

package cn.itxs.principle.srp;

public class EatMeet {
    public void doEat(String animal){
        System.out.println(animal+"在大口吃肉!");
    }
}

吃草類EatGrass.java

package cn.itxs.principle.srp;

public class EatGrass {
    public void doEat(String animal){
        System.out.println(animal+"在細嚼慢咽吃草!");
    }
}

測試類SrpMain.java

package cn.itxs.principle.srp;

public class SrpMain {

    public static void main(String[] args) {
        EatMeet animalMeet = new EatMeet();
        animalMeet.doEat("獅子");

        EatGrass animalGrass = new EatGrass();
        animalGrass.doEat("黃牛");
    }
}
  • 方法級別

吃類Eat.java

package cn.itxs.principle.srp;

public class Eat {
    public void doEatMeet(String animal){
        System.out.println(animal+"在大口吃肉!");
    }

    public void doEatGrass(String animal){
        System.out.println(animal+"在細嚼慢咽吃草!");
    }
}

測試類SrpMain.java

package cn.itxs.principle.srp;

public class SrpMain {
    public static void main(String[] args) {
        Eat animal = new Eat();
        animal.doEatMeet("獅子");
        animal.doEatGrass("黃牛");        
    }
}

介面隔離原則

概述

  • 上面單一職責原則強調的是職責,站在業務邏輯的角度;介面隔離原則(Interface Segregation Principle,ISP)則是強調介面的方法盡量少。
  • 設計介面精簡單一介面,可理解為介面的單一職責;使用多個專門的介面比使用單一的總介面要好,也即是介面粒度最小化,不要實現與之無關的介面包括空實現。
  • 介面是用來擴展類的功能,凸顯的是功能方法層面,其中抽象化也是開閉原則的關鍵

反例

Behavior行為介面

package cn.itxs.principle.isp;

public interface Behavior {
    void work();
    void eat();
}

Man類實現行為介面

package cn.itxs.principle.isp;

public class Man implements Behavior{
    @Override
    public void work() {
        System.out.println("man do work!");
    }

    @Override
    public void eat() {
        System.out.println("man eat something!");
    }
}

Robot類實現行為介面

package cn.itxs.principle.isp;

public class Robot implements Behavior{
    @Override
    public void work() {
        System.out.println("robot do work!");
    }
	//機器人不需要吃飯,空實現
    @Override
    public void eat() {}
}

正例

WorkBehavior介面

package cn.itxs.principle.isp;

public interface WorkBehavior {
    void work();
}

EatBehavior介面

package cn.itxs.principle.isp;

public interface EatBehavior {
    void eat();
}

Man類實現Work和Eat行為介面

package cn.itxs.principle.isp;

public class Man implements WorkBehavior,EatBehavior{
    @Override
    public void work() {
        System.out.println("man do work!");
    }

    @Override
    public void eat() {
        System.out.println("man eat something!");
    }
}

Robot類只實現WorkBehavior介面

package cn.itxs.principle.isp;

public class Robot implements WorkBehavior{
    @Override
    public void work() {
        System.out.println("robot do work!");
    }
}

里氏替換原則

概述

  • 里氏替換原則(Liskov Substitution principle,LSP)強調的是設計與實現要依賴於抽象而非具體;子類只能去擴展基類,而不是隱藏或者覆蓋基類。
  • 里氏替換原則是實現開閉原則的一個方式,是描述類之類關係原則,強調不要破壞繼承體系,不要輕易去修改父類的方法,表現的結果是所有使用父類的地方用子類替換後程式依然能夠正常的運行;
  • 里氏替換原則可以用來規範我們對於繼承的使用,保證了父類的復用性,同時也能夠降低系統出錯誤的故障,防止誤操作,最終目的是為了增強程式健壯性。

反例

計算器類Caculator.java

package cn.itxs.principle.lsp;

public class Caculator {
    public int add(int a,int b){
        return a+b;
    }
}

特殊計算器類SpecialCaculator.java

package cn.itxs.principle.lsp;

public class SpecialCaculator extends Caculator{
    @Override
    public int add(int a, int b) {
        return a-b;
    }
}

測試類ISPMain.java

package cn.itxs.principle.lsp;

public class ISPMain {
    public static void main(String[] args) {
        Caculator caculator = new SpecialCaculator();
        System.out.println("100+20="+caculator.add(100, 20));
    }
}

輸出結果為100+20=80,我們發現原來運行正常的加法功能發生了錯誤,原因子類SpecialCaculator可能無意重寫Caculator的add方法,造成原本運行加法功能的程式碼調用了類SpecialCaculator的重寫後的方法而導致出現了錯誤。

正例

特殊計算器類SpecialCaculator.java

package cn.itxs.principle.lsp;

public class SpecialCaculator extends Caculator{
    public int sub(int a, int b) {
        return a-b;
    }
}

測試類ISPMain.java

package cn.itxs.principle.lsp;

public class ISPMain {
    public static void main(String[] args) {
        SpecialCaculator specialCaculator = new SpecialCaculator();
        System.out.println("100+20="+specialCaculator.add(100, 20));
        System.out.println("80-30="+specialCaculator.sub(80, 30));
    }
}

依賴倒置原則

概述

  • 依賴倒置原則(Dependence Inversion Principle,DIP)指的是面向介面編程,或者說是抽象編程和思維。抽象不應該依賴細節、細節依賴抽象。
  • 依賴倒置原則是是開閉原則的基礎,主要體現在成員變數、方法的參數、方法的返回值、局部變數這些方面進行依賴倒置,只要是聲明的類型都基本建議使用介面或者抽象類型來聲明,而不要使用實現類型聲明;依賴倒置原則實現的3種方式如下:
    • 通過構造方法聲明依賴對象。
    • 設值注入(setter注入),在類中通過Setter方法聲明依賴關係。
    • 介面注入,在介面方法中聲明依賴對象。

反例

Doc文檔讀取類ReadDoc.java

package cn.itxs.principle.dip;

public class ReadDoc {
    public void read(){
        System.out.println("read doc!");
    }
}

Excel文件讀取類ReadExcel.java

package cn.itxs.principle.dip;

public class ReadExcel {
    public void read(){
        System.out.println("read excel!");
    }
}

ReadXml文檔讀取類ReadXml.java

package cn.itxs.principle.dip;

public class ReadXml {
    public void read(){
        System.out.println("read xml!");
    }
}

讀取工具類ReadTool.java

package cn.itxs.principle.dip;

public class ReadTool {
    public void read(ReadXml r){
        r.read();
    }

    public void read(ReadDoc r){
        r.read();
    }

    public void read(ReadExcel r){
        r.read();
    }
}

上面的設計如果需要擴展讀取其他文件內容,需要在ReadTool類中增加相應類型的read方法,這就違反了開閉原則。

正例

先設計一個讀介面

package cn.itxs.principle.dip;

public interface Readable {
    void read();
}

然後Excel、Doc、Xml實現類都實現Readable讀介面

public class ReadExcel implements Readable{
    @Override
    public void read(){
        System.out.println("read excel!");
    }
}
package cn.itxs.principle.dip;

public class ReadXml implements Readable{
    @Override
    public void read(){
        System.out.println("read xml!");
    }
}
package cn.itxs.principle.dip;

public class ReadDoc implements Readable{
    @Override
    public void read(){
        System.out.println("read doc!");
    }
}

下面我們選擇set方法設值注入的方式來演示

package cn.itxs.principle.dip;

public class ReadTool {

    private Readable readable;

    public void setreadable(Readable readable) {
        this.readable = readable;
    }

    public void read(){
        readable.read();
    }
}

讀取工具類ReadTool.java

package cn.itxs.principle.dip;

public class DIPMain {
    public static void main(String[] args) {
        ReadTool readTool = new ReadTool();
        readTool.setreadable(new ReadXml());
        readTool.read();
        readTool.setreadable(new ReadExcel());
        readTool.read();
        readTool.setreadable(new ReadDoc());
        readTool.read();
    }
}

這樣我們依賴的是抽象,增加讀取類型的時候只需要增加實現類即可,ReadTool就無需再進行修改了。

合成復用原則

概述

  • 合成復用原則(Composite Reuse Principle,CRP)也是描述類之類關係原則,盡量使用對象組合而不是使用繼承來達到復用的目的。
  • 合成復用原則從執行層面也即是說要優先考慮使用組合或聚合關係的復用,次用或少用繼承關係復用。而如果只能使用繼承關係那就要遵循里氏替換原則。
  • 繼承作為面向對象三大特性之一,在給程式設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程式帶來侵入性,程式的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能會產生故障。
  • 繼承是靜態復用,而通過聚合或組合的復用是動態復用。所謂的靜態復用是在編碼階段已經明確了類之間的關係;動態復用則是在程式運行階段,根據實際要求注入相應的對象完成復用的,動態復用比靜態復用更具有靈活性。

反例

漢堡抽象類Hamburger.java

package cn.itxs.principle.crp;

public abstract class Hamburger {
    abstract void meat();
}

雞肉漢堡類ChickenHamburger.java

package cn.itxs.principle.crp;

public class ChickenHamburger extends Hamburger{
    @Override
    void meat() {
        System.out.println("雞肉漢堡");
    }
}

牛肉漢堡類BeefHamburger.java

package cn.itxs.principle.crp;

public class BeefHamburger extends Hamburger{
    @Override
    void meat() {
        System.out.println("牛肉漢堡");
    }
}

芝士雞肉漢堡類CheeseChickenHamburger.java

package cn.itxs.principle.crp;

public class CheeseChickenHamburger extends ChickenHamburger{
    public void burden(){
        System.out.println("芝士");
        super.meat();
    }
}

黃油雞肉漢堡類ButterChickenHamburger.java

package cn.itxs.principle.crp;

public class ButterChickenHamburger extends ChickenHamburger{
    public void burden(){
        System.out.println("黃油");
        super.meat();
    }
}

芝士牛肉漢堡類CheeseBeefHamburger.java

package cn.itxs.principle.crp;

public class CheeseBeefHamburger extends BeefHamburger{
    public void burden(){
        System.out.println("芝士");
        super.meat();
    }
}

黃油牛肉漢堡類ButterBeefHamburger.java

package cn.itxs.principle.crp;

public class ButterBeefHamburger extends BeefHamburger{
    public void burden(){
        System.out.println("黃油");
        super.meat();
    }
}

測試類,輸出芝士牛肉漢堡

package cn.itxs.principle.crp;

public class CRPMain {
    public static void main(String[] args) {
        CheeseBeefHamburger hamburger = new CheeseBeefHamburger();
        hamburger.burden();
    }
}

上圖可以看出繼承關係實現產生了大量的子類,而且增加了新的肉類或是顏色都要修改源碼,這違背了開閉原則,顯然不可取,但如果改用組合關係實現就很好的解決了以上問題,圖例

正例

採用組合或聚合復用方式,第一步先將將配料Burden抽象為介面,並實現黃油,芝士兩個配料類,第二步將Burden對象組合在漢堡類Hamburger中,最終我們用更少的類就可以實現上面的功能(ButterChickenHamburger、ButterBeefHamburger、CheeseChickenHamburger、CheeseBeefHamburger);而且以後當增加豬肉、蝦肉漢堡或者新增加乳酪配料都不需要修改原來的程式碼,只要增加對應的實現類即可,符合開閉原則。

Burden介面Burden.java

package cn.itxs.principle.crp;

public interface Burden {
    void burdenKind();
}

芝士類Cheese.java

package cn.itxs.principle.crp;

public class Cheese implements Burden{
    @Override
    public void burdenKind() {
        System.out.println("芝士");
    }
}

黃油類Cheese.java

package cn.itxs.principle.crp;

public class Butter implements Burden{
    @Override
    public void burdenKind() {
        System.out.println("黃油");
    }
}

漢堡抽象類Hamburger.java

package cn.itxs.principle.crp;

public abstract class Hamburger {
    abstract void meat();
    private Burden burden;

    public Burden getBurden() {
        return burden;
    }

    public void setBurden(Burden burden) {
        this.burden = burden;
    }
}

測試類同樣輸出芝士牛肉漢堡

package cn.itxs.principle.crp;

public class CRPMain {
    public static void main(String[] args) {
        BeefHamburger beefHamburger = new BeefHamburger();
        Burden cheese = new Cheese();
        beefHamburger.setBurden(cheese);
        beefHamburger.getBurden().burdenKind();
        beefHamburger.meat();
    }
}

迪米特法則

概述

  • 迪米特法則(Law of Demeter,LoD)核心是強調要降低類與類之間耦合度,儘可能減少與其他實體相互作用,提高模組相對獨立性。
  • 迪米特法則也叫作最少知識原則(The Least Knowledge Principle),一個類對於其他類知道的越少越好,就是說一個對象應當對其他對象有儘可能少的了解,只和直接的朋友通訊,不和陌生人說話。在類中朋友一般包含如下:
    • 當前對象本身this。
    • 以參數形式傳入當前對象方法中的對象。
    • 當前對象的成員對象。
    • 如果當前對象的成員是一個集合,那麼集合的元素也是朋友。
    • 當前對象所創建的對象。
  • 從依賴者角度來說只依賴應該依賴的對象;從被依賴角度來說只暴露應該暴露的方法。

反例

短影片類Video.java

package cn.itxs.principle.lod;

public class Video {
    private String title;

    public Video(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

抖音app類App.java

package cn.itxs.principle.lod;

public class App {
    public void play(Video video){
        System.out.println("播放短影片,標題為"+video.getTitle());
    }
}

手機類Phone.java

package cn.itxs.principle.lod;

public class Phone {
    private App app = new App();
    private Video video = new Video("大話設計模式");

    public void playVideo(){
        app.play(video);
    }
}

測試類,輸出「播放短影片,標題為大話設計模式」

package cn.itxs.principle.lod;

public class LODMain {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.playVideo();
    }
}

短影片和抖音app對象都在手機上,手機和短影片是沒有太大聯繫,短影片對象不應該在手機類裡面。應該修改為手機裡面有抖音APP,抖音APP裡面有短影片,這才符合迪米特法則;手機和抖音APP是朋友,抖音APP和短影片是朋友,在軟體設計里朋友的朋友不是朋友,也就是手機和短影片不是朋友,所以它們不應該有直接交集。

正例

其他不變,只修改抖音App和Phone類,抖音App類App.java

package cn.itxs.principle.lod;

public class App {
    private Video video = new Video("大話設計模式");
    public void play(){
        System.out.println("播放短影片,標題為"+video.getTitle());
    }
}

手機類Phone.java

package cn.itxs.principle.lod;

public class Phone {
    private App app = new App();
    public void playVideo(){
        app.play();
    }
}

常見設計模式

分類

  • 根據目的來分

    • 創建型模式:作用於對象的創建,將對象的創建與使用分離。其中囊括了單例、原型、工廠方法、抽象工廠、建造者5 種創建型模式。
    • 結構型模式:將類或對象按某種布局組成更大的結構,其中以代理、適配器、橋接、裝飾、外觀、享元、組合 7 種結構型模式為主。
    • 行為型模式:作用於類或對象之間相互協作共同完成單個對象無法單獨完成的任務,以及怎樣分配職責。主要包含了模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、訪問者、備忘錄、解釋器等 11 種行為型模式。
  • 作用範圍來分

    • 類模式:用於處理類與子類之間的關係,這些關係通過繼承來建立,在編譯時刻便確定下來了。工廠方法、(類)適配器、模板方法、解釋器均屬於該模式。
    • 對象模式:用於處理對象之間的關係,這些關係可以通過組合或聚合來實現,在運行時刻是可以變化的,更具動態性。除了以上 4 種,其他的都是對象模式。
  • 具體功能分類表

    image-20220108180510545

設計模式是經過大量實踐和歸納總結的範式,是可以直接運用於軟體開發中,設計模式是一種風格,從某種意義上不止上面23種,比如像MVC也可算是一個設計模式。

單例模式

概述

  • 單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。
    • 單例類只能有一個實例。
    • 單例類必須自己創建自己的唯一實例。
    • 單例類必須給所有其他對象提供這一實例。
  • 單例模式保證了全局對象的唯一性,比如系統啟動讀取配置文件就需要單例保證配置的一致性;其他的如執行緒池、快取、日誌對象、對話框、印表機、顯示卡的驅動程式對象也常被設計成單例。
  • 單例模式寫法只要有兩大類餓漢式和懶漢式。餓漢式由JVM載入類後初始化不存在執行緒安全問題,但會浪費記憶體一般也不推薦使用。
  • 常見使用場景
    • 需要頻繁的進行創建和銷毀的對象。
    • 創建對象時耗時過多或耗費資源過多,但又經常用到的對象。
    • 工具類對象。
    • 頻繁訪問資料庫或文件的對象。

餓漢式

package cn.itxs.pattern.singleton;

public class HungerSingleton {
    private static final HungerSingleton instance = new HungerSingleton();
    private HungerSingleton(){

    }
    public HungerSingleton getInstance(){
        return instance;
    }
}

雙層檢測鎖

package cn.itxs.pattern.singleton;

public class DoubleCheckSingleton {
    //volatile有兩個作用,其一保證instance的可見性,其二禁止jvm指令重排序優化
    private static volatile DoubleCheckSingleton instance = null;
    private DoubleCheckSingleton(){

    }
    public static DoubleCheckSingleton getInstance(){
        //第一個if主要用於非第一次創建單例後可以直接返回的性能優化
        if (instance == null){
            //採用同步程式碼塊保證執行緒安全
            synchronized (DoubleCheckSingleton.class){
                if (instance == null){
                    //這一行jvm內部執行多步,1先申請堆記憶體,2對象初始化,3對象指向記憶體地址;2和3由於jvm有指令重排序優化所以存在3先執行可能會導致instance還沒有初始化完成,其他執行緒就得到了這個instance不完整單例對象的引用值而報錯。
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

靜態內部類

靜態內部類的方式滿足懶載入,沒有執行緒安全問題,同時也十分高效,程式碼也容易讓人理解

package cn.itxs.pattern.singleton;

public class InnerSingleton {
    private InnerSingleton(){

    }
    private static class SingletonHolder{
        private static final InnerSingleton instance = new InnerSingleton();
    }
    public static InnerSingleton getInstance(){
        return SingletonHolder.instance;
    }
}

反射攻擊

不管是上面的餓漢式、懶漢式的雙層檢測鎖、靜態內部類也不能阻止反射攻擊導致單例被破壞,雖然我們一開始都對構造器進行了私有化處理,但Java本身的反射機制卻還是可以將private訪問許可權改為可訪問,依舊可以創建出新的實例對象而破壞單例。測試類如下

package cn.itxs.pattern.singleton;

import java.lang.reflect.Constructor;

public class SingletonMain {
    public static void main(String[] args) {
        //反射攻擊
        reflectAttack();
    }

    public static void reflectAttack() {
        System.out.println("正常單例獲取對象");
        InnerSingleton instanceA = InnerSingleton.getInstance();
        System.out.println(instanceA);
        InnerSingleton instanceB = InnerSingleton.getInstance();
        System.out.println(instanceB);
        Constructor<InnerSingleton> constructor = null;
        try {
            constructor = InnerSingleton.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            InnerSingleton instanceC = constructor.newInstance();
            System.out.println("反射攻擊後單例獲取對象");
            System.out.println(instanceC);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220108193051159

序列化和反序列化破壞

餓漢式、懶漢式的雙層檢測鎖、靜態內部類如果都實現序列化介面,也會被序列化和反序列化破壞單例。

在DoubleCheckSingleton.java中增加實現Serializable介面

image-20220108193944748

測試類程式碼如下:

package cn.itxs.pattern.singleton;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SingletonMain {
    public static void main(String[] args) {
        //序列化和反序列化破壞
        serializeDestroy();
    }

    public static void serializeDestroy() {
        System.out.println("正常單例獲取對象");
        DoubleCheckSingleton instanceA = DoubleCheckSingleton.getInstance();
        System.out.println(instanceA);
        DoubleCheckSingleton instanceB = DoubleCheckSingleton.getInstance();
        System.out.println(instanceB);
        try {
            ByteArrayOutputStream bos=new ByteArrayOutputStream();
            ObjectOutputStream oos=new ObjectOutputStream(bos);
            oos.writeObject(instanceB);

            ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois=new ObjectInputStream(bis);
            DoubleCheckSingleton instanceC = (DoubleCheckSingleton) ois.readObject();
            System.out.println("序列化破壞後單例獲取對象");
            System.out.println(instanceC);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220108193647759

這裡如何應對序列化和反序列化破壞,只需要在DoubleCheckSingleton.java進行小小的修改 ,添加一個方法readResolve()

image-20220108194343499

再次運行測試類輸出後三個單例獲取對象都是相同了

image-20220108194229277

枚舉

枚舉是最簡潔、執行緒安全、不會被反射創建實例的單例實現,《Effective Java》中也表明了這種寫法是最佳的單例實現模式。從Constructor構造器中反射實例化對象方法newInstance的源碼可知:反射禁止了枚舉對象的實例化,也就防止了反射攻擊,不用自己在構造器實現複雜的重複實例化邏輯了。

image-20220109005325937

EnumSingleton.java

package cn.itxs.pattern.singleton;

public enum EnumSingleton {
    INSTANCE;
}

測試類

package cn.itxs.pattern.singleton;

import java.lang.reflect.Constructor;

public class SingletonMain {
    public static void main(String[] args) {
        //反射攻擊
        reflectAttack();
    }

    public static void reflectAttack() {
        System.out.println("正常單例獲取對象");
        EnumSingleton instanceA = EnumSingleton.INSTANCE;
        System.out.println(instanceA);
        EnumSingleton instanceB = EnumSingleton.INSTANCE;
        System.out.println(instanceB);
        Constructor<EnumSingleton> constructor = null;
        try {
            constructor = EnumSingleton.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            EnumSingleton instanceC = constructor.newInstance();
            System.out.println("反射攻擊後單例獲取對象");
            System.out.println(instanceC);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220109010524358

模板方法

概述

  • 模板方法模式在一個方法中定義一個演算法骨架,並將某些步驟推遲到子類中實現。模板方法模式可以讓子類在不改變演算法整體結構的情況下,重新定義演算法中的某些步驟。
    • 演算法骨架就是「模板」
    • 包含演算法骨架的方法就是模板方法
  • 一個抽象類公開定義了執行它的方法的方式/模板。它的子類可以按需要重寫方法實現,但調用將以抽象類中定義的方式進行。
  • 常見使用場景
    • 多個子類有公有的方法,並且邏輯基本相同時。
    • 重要、複雜的演算法,可將不變的核心演算法設計為模板方法,周邊的相關細節功能、可變的行為由各個子類實現。
    • 重構時,模板方法模式是一個經常使用的模式,把相同程式碼抽取到父類,然後通過鉤子函數約束其行為。

程式碼示例

遊戲抽象類Game.java

package cn.itxs.pattern.template;

public abstract class Game {
    public final void play(){
        init();
        startPlay();
        endPlay();
    }

    //初始化遊戲
    abstract void init();
    //開始遊戲
    abstract void startPlay();
    //結束遊戲
    abstract void endPlay();
}

英雄聯盟遊戲類LoL.java

package cn.itxs.pattern.template;

public class LoL extends Game{
    @Override
    void init() {
        System.out.println("英雄聯盟手游初始化");
    }

    @Override
    void startPlay() {
        System.out.println("英雄聯盟手游激烈進行中");
    }

    @Override
    void endPlay() {
        System.out.println("英雄聯盟手游遊戲結束,恭喜三連勝");
    }
}

和平精英遊戲類Game.java

package cn.itxs.pattern.template;

public class Peace extends Game{
    @Override
    void init() {
        System.out.println("和平精英手游初始化");
    }

    @Override
    void startPlay() {
        System.out.println("和平精英手游激烈進行中");
    }

    @Override
    void endPlay() {
        System.out.println("和平精英手游遊戲結束,大吉大利今晚吃雞");
    }
}

測試類

package cn.itxs.pattern.template;

public class TemplateMain {
    public static void main(String[] args) {
        //玩英雄聯盟遊戲
        Game lol = new LoL();
        lol.play();
        //玩和平精英遊戲
        Game peace = new Peace();
        peace.play();
    }
}

image-20220109020556722

**本人部落格網站 **IT小神 www.itxiaoshen.com