1. 程式人生 > >常用設計模式及其 Java 實現

常用設計模式及其 Java 實現

設計模式是在不斷出現的特定情境下,針對特定問題,可以重複使用的特定解決模式(套路)。本文按照建立型、結構型、行為型三大類,總結了常見的 24 種設計模式的使用要點,包括適用場景、解決方案、及其相應的 Java 實現。

作者:王克鋒
出處:https://kefeng.wang/2018/04/16/design-patterns/
版權:自由轉載-非商用-非衍生-保持署名,轉載請標明作者和出處。

1 概述

1.1 概念

設計模式,是在某個不斷出現的“情境(Context)”下,針對某個“問題”的某種“解決方案”:

  • “問題”必須是重複出現的,“解決方案”必須是可反覆應用的;
  • “問題”包含了“一個目標”和“一組約束”,當解決方案在兩者之間取得平衡,才是有用的模式;
  • 設計模式不是法律準則,只是指導方針,實際使用時可以根據需要微調,只是要作好註釋,以便他人清楚;
  • 很多看似的新模式,實質上是現有模式的變體;
  • 模式的選用原則:儘量用最簡單的方式設計,除非為了適應未來確實可能的變化,才採用設計模式,因為設計模式會引入更多類更復雜的關係,不要為了使用模式而使用模式。

1.2 六大原則



將六大原則的英文首字母拼在一起就是SOLID(穩定的),所以也稱之為 SOLID 原則。

1.2.1 單一職責原則(Single Responsibility Principle)

There should never be more than one reason for a class to change.
一個類只有一個職責,而不是多個職責耦合在一個類中(比如介面與邏輯要分離)。

1.2.2 開放封閉原則(Open Closed Principle)

Software entities like classes, modules and functions should be open for extension but closed for modifications.
對擴充套件開放,對修改關閉,使用介面和抽象類。

1.2.3 里氏替換原則(Liskov Substitution Principle)

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
確保父類可以出現的地方,子類一定可以出現,這是繼承複用的基石。

1.2.4 最少知道原則(Least Knowledge Principle)

Only talk to you immediate friends.
低依賴,各實體儘量獨立,儘量減少相互作用。

1.2.5 介面隔離原則(Interface Segregation Principle)

The dependency of one class to another one should depend on the smallest possible interface.
客戶(client)應該不依賴於它不使用的方法。儘量使用多個介面分工合成,而不是單個介面耦合多種功能。

1.2.6 依賴倒置原則(Dependency Inversion Principle)

High level modules should not depends upon low level modules.
Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
要依賴於抽象(介面或抽象類),而不是具體(具體類)。

1.3 價值

設計模式(Design Pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。
設計模式看似簡單問題複雜化。但“簡單”的設計靈活性差,在當前專案中不便擴充套件,拿到其他專案更是無法使用,相當於“一次性程式碼”。而設計模式的程式碼,結構清晰,當前專案中便於擴充套件,拿到其他專案也適用,是通用的設計。
很多程式設計師接觸到設計模式之後,都有相見恨晚的感覺,感覺自己脫胎換骨,達到了新的境界,設計模式可以作為程式設計師劃分水平的標準。
不過我們也不能陷入模式的陷阱,為了使用模式而去套模式,那樣會陷入形式主義。

1.4 選用方法

  • 每個設計模式,都隱含了幾個OO原則,當沒有合適的設計模式可選時,可迴歸到OO原則來取捨;
  • 使用模式最好的方式:腦子裡裝著各種模式,看已有設計或程式碼中,哪裡可以使用這些模式,以複用經驗;
  • 共享設計模式詞彙(包括口頭叫法、程式碼中類與方法的命名)的威力:
    (1)與他人溝通時,提到設計模式名稱,就隱含了其模式;
    (2)使用模式觀察軟體系統,可以保持在設計層次,而不會被停留在瑣碎的物件細節上;
    (3)團隊間用設計模式交流,彼此看法不容易誤解。

1.5 重要書籍

作者:埃裡希·伽瑪(Erich Gamma), Richard Helm , Ralph Johnson,John Vlissides,後以“四人幫”(Gang of Four,GoF)著稱。有兩本書:

1.5.1 《Head First 設計模式》

強烈推薦閱讀。英文書名是《Head First Design Patterns》。
信耶穌的人都要讀聖經,信OO(面向物件)的人都要讀四人組的《Head First 設計模式》,官方網站 Head First Design Patterns。2004 該書榮獲Jolt獎(類似於電影領域的奧斯卡獎)。

  • 是首次將模式歸類的功臣,開啟了軟體領域的一大躍進;
  • 模式的模板:包括名稱、類目、意圖、動機、適用性、類圖、參與者及其協作、結果、實現、範例程式碼、已知應用、相關模式等。

1.5.2 《設計模式:可複用面向物件軟體的基礎》

英文書名是《Design Patterns: Elements of Reusable Object-Oriented Software》。也是四人組所著。
是軟體工程領域有關軟體設計的一本書,提出和總結了對於一些常見軟體設計問題的標準解決方案,稱為軟體設計模式。這本書在1994年10月21日首次出版,至2012年3月已經印行40刷。

2 分類與定義

設計模式可分為三個大類,每個大類又包含若干具體的模式。
容易混淆的幾種模式:簡單工廠S / 抽象工廠A / 工廠方法F / 模板方法T

  • “工廠”字樣的:帶的只用來建立例項,比如 S/A/F;不帶的則不限,比如 T;
  • “方法”字樣的:帶的無需額外的客戶端參與,可以獨立運轉,比如 F/T;不帶的需要額外的客戶端呼叫,比如 S/A。

2.1 建立型(Creational Patterns)

用於物件的建立,把建立物件的工作放在另一個物件中,或者推遲到子類中。

2.1.1 單例(Singleton)

確保一個類只有一個例項,並提供一個全域性的訪問點。
需要注意的是,多個類載入器下使用單例,會導致各類載入器下都有一個單例例項,因為每個類載入器都有自己獨立的名稱空間。
JDK 中的單例有 Runtime.getRuntime()NumberFormat.getInstance()
下面總結了四種執行緒安全的 Java 實現方法。每種實現都可以用 Singleton.getInstance().method(); 呼叫。

2.1.1.1 餓漢方式

關鍵思路:作為類的靜態全域性變數,載入該類時例項化。
缺點是真正使用該例項之前(也有可能一直沒用到),就已經例項化,浪費資源。
對於 Hotspot VM,如果沒涉及到該類,實際上是首次呼叫 getInstance() 時才例項化。

/**
 * @author: kefeng.wang
 * @date: 2016-06-07 10:21
 **/

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    // 基於 classLoader 機制,自動達到了執行緒安全的效果
    public static Singleton getInstance() {
        return instance;
    }

    public void method() {
        System.out.println("method() OK.");
    }
}
2.1.1.2 懶漢方式

關鍵思路:在方法 getInstance() 上實現同步。
缺點是每次呼叫 getInstance() 都會加鎖,但實際上只有首次例項化時才需要,後續的加鎖都是浪費,導致效能大降。

/**
 * @author: kefeng.wang
 * @date: 2016-06-07 10:21
 **/

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

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

    public void method() {
        System.out.println("method() OK.");
    }
}
2.1.1.3 懶漢方式(雙重檢查加鎖)

關鍵思路:不同步的情況下檢查到尚未建立,再同步檢查到尚未例項化時,才例項化。以便大大減少同步的情況。
缺點是:要求 JDK5+,否則許多 JVM 對 volatile 的實現導致雙重加鎖失效。不過現在極少開發者會用 JDK5,所以該缺點關係不大。

/**
 * @author: kefeng.wang
 * @date: 2016-06-07 10:21
 **/

public class Singleton {
    private volatile static Singleton instance = null; // 注意 volatile

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) { // 初步檢查:尚未例項化
            synchronized (Singleton.class) { // 再次同步(對 Singleton.class)
                if (instance == null) { // 確認尚未例項化
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }

    public void method() {
        System.out.println("method() OK.");
    }
}
2.1.1.4 內部靜態類方式(推薦!)

關鍵思路:全域性靜態成員放在內部類中,只有該內部類被引用時才例項化,以達到延遲例項化的目的。這是個完美方案:

  • 確保延遲例項化至 getInstance() 的呼叫;
  • 無需加鎖,效能佳;
  • 不受 JDK 版本限制。
/**
 * @author: kefeng.wang
 * @date: 2016-06-07 10:21
 **/

public class Singleton {
    private static class InstanceHolder { // 延遲載入例項
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }

    public void method() {
        System.out.println("method() OK.");
    }
}

2.1.2 生成器(Builder)

將物件的建立過程,封裝到一個生成器物件中,客戶按步驟呼叫它完成建立。
Java 實現請參考 StringBuilder 的原始碼,這裡給出其使用效果:

StringBuilder sb = new StringBuilder();
sb.append("Hello world!").append(123).append('!');
System.out.println(sb.toString());

2.1.3 簡單工廠(Simple Factory) ★

不是真正的“設計模式”。自身是工廠實現類,直接提供建立方法(可多個),可以是靜態方法。JDK 中有 Boolean.valueOf(String)Class.forName(String)

/**
 * @author: kefeng.wang
 * @date: 2016-06-09 19:42
 **/

public class DPC3_SimpleFactoryPattern {
    private static class SimpleFactory {
        public CommonProduct createProduct(int type) { // 工廠方法,返回“產品”介面,形參可無
            if (type == 1) {
                return new CommonProductImplA(); // 產品具體類
            } else if (type == 2) {
                return new CommonProductImplB();
            } else if (type == 3) {
                return new CommonProductImplC();
            } else {
                return null;
            }
        }
    }

    private static class SimpleFactoryClient {
        private SimpleFactory factory = null;

        public SimpleFactoryClient(SimpleFactory factory) {
            this.factory = factory;
        }

        public final void run() {
            CommonProduct commonProduct1 = factory.createProduct(1);
            CommonProduct commonProduct2 = factory.createProduct(2);
            CommonProduct commonProduct3 = factory.createProduct(3);
            System.out.println(commonProduct1 + ", " + commonProduct2 + ", " + commonProduct3);
        }
    }

    public static void main(String[] args) {
        SimpleFactory factory = new SimpleFactory(); // 工廠例項
        new SimpleFactoryClient(factory).run(); // 傳入客戶類
    }
}

2.1.4 抽象工廠(Abstract factory) ★

一個抽象類,定義建立物件的抽象方法。繼承後的多個實現類中,實現建立物件的方法。
客戶端靈活選擇實現類,完成物件的建立。
JDK 中採用此模式的有 NumberFormat.getInstance()

2.1.5 工廠方法(Factory method) ★

建立方法的對於抽象類和實現類的分工,與“抽象工廠”類似。
區別在於:本模式無需客戶端,自身方法即可完成物件建立前後的操作。

2.1.6 原型(Prototype)

當建立例項的過程很複雜或很昂貴時,可通過克隆實現。比如 Java 的 Object.clone()

2.2 結構型(Structural Patterns)

用於類或物件的組合關係。

2.2.1 介面卡(Adapter)

將一個介面適配成期望的另一個介面,可以消除介面不匹配所造成的相容性問題。
比如把 Enumeration<E> 適配成 Iterator<E>Arrays.asList()T[] 適配成 List<T>

2.2.2 橋接(Bridge) ★

事物由多個因子組合而成,而每個因子都有一個抽象類和多個實現類,最終這多個因子可以自由組合。
比如多種遙控器+多種電視機、多種車型+多種路況+多種駕駛員。JDK 中的 JDBCAWT

2.2.3 組合(Composite) ★

把物件的“部分/整體”以樹形結構組織,以便統一對待單個物件或多個物件組合。
比如多級選單、二叉樹等。

2.2.4 裝飾(Decorator)

執行時動態地將職責附加到裝飾者上。
擴充套件功能有兩種方式,類繼承是編譯時靜態決定,而裝飾者模式是執行時動態決定,有獨特優勢。
比如 StringReaderLineNumberReader 裝飾後,為字元流擴展出了 line 相關介面。

2.2.5 外觀(Facade) ★

提供了一個統一的高層介面,用來訪問子系統中的一群介面,讓子系統更容易使用。
比如電腦的啟動(或關閉),是呼叫CPU/記憶體/磁碟各自的啟動(或關閉)介面。

2.2.6 享元 / 蠅量(Flyweight)

運用共享技術有效地支援大量細粒度的物件。
比如文字處理器,無需為每個字元的多次出現而生成多個字形物件,而是外部資料結構中同一字元的多次出現共用一個字形物件。
JDK 中的 Integer.valueOf(int) 就採用此模式。

2.2.7 代理(Proxy)

proxy 建立並持有 subject 的引用,client 呼叫 proxy 時,proxy 會轉發給 subject。
比如 Java 裡的 Collections 集合檢視、RMI/RPC 遠端呼叫、快取代理、防火牆代理等。

2.3 行為型(Behavioral Patterns)

用於類或物件的呼叫關係。

2.3.1 責任鏈(Chain of responsibility)

一個請求沿著一條鏈傳遞,直到該鏈上的某個處理者處理它為止。
比如 SpringMVC 中的過濾器。

2.3.2 命令(Command)

將命令封裝為物件,可以隨意儲存/載入、傳遞、執行/撤消、排隊、記錄日誌等,將“動作的請求者”從“動作的執行者”中解耦。
參與方包括 Invoker(呼叫者) => Command(命令) => Receiver(執行者)。
比如定時任務、執行緒任務 Runnable

2.3.3 直譯器模式(Interpreter)

用於建立簡易的語言直譯器,可處理指令碼語言和程式語言,為每個規則建立一個類。
比如 JDK 中的 java.util.Patternjava.text.Format

2.3.4 迭代器(Iterator)

提供一種方法,順序訪問一個聚合物件中的各個元素,而無需暴露其內部表現。
比如 JDK 中的 java.util.Iteratorjava.util.Enumeration

2.3.5 中介者(Mediator)

使用一箇中介物件,封裝一系列的物件互動,中介物件使各物件無需顯式相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的互動。
比如 JDK 中的 java.util.Timerjava.util.concurrent.ExecutorService.submit()

2.3.6 備忘錄(Memento)

備忘錄物件用來儲存另一個物件的內部狀態的快照,並可在外部儲存起來,之後可還原到當初的狀態。比如 Java 序列化。
比如 JDK 中的 java.util.Datejava.io.Serializable

2.3.7 觀察者(Observer)

物件間一對多的依賴,被觀察者狀態改變時,觀察者都會收到通知。
參與方包括 Observable(被觀察者) / Observer(觀察者)。
比如 RMI 中的事件、java.util.EventListener

2.3.8 狀態(State)

物件的內部狀態變化時,其行為也隨之改變。其內部實現是,定義一個狀態父類,為每種狀態擴展出狀態子類,當物件內部狀態變化時,所選的狀態子類也跟著切換,但外部只需與該物件互動,而不知道狀態子類的存在。
比如視訊播放器的停止/播放/暫停等狀態。

2.3.9 策略(Strategy)

定義一組演算法,分別封裝起來,獨立於客戶之外,演算法更改時不影響客戶使用。
比如遊戲中的不同角色,可以使用各種裝備,這些裝備可以策略的方式封裝起來。
比如 JDK 中的 java.util.Comparator#compare()

2.3.10 模板方法(Template method) ★

抽象類中定義頂級的邏輯框架(叫做“模板方法”),一些步驟(可以建立例項或其他操作)延遲到子類實現,自身可獨立運轉。
當子類實現的操作是建立例項時,模板方法就變成了工廠方法模式,所以說工廠方法是特殊的模板方法。

2.3.11 訪問者(Visitor) ★

在不修改被訪問者資料結構的前提下,訪問者中封裝訪問操作,關鍵點是被訪問者中提供被訪問的介面。
適用場景是被訪問者穩定但訪問者靈活多變,或者訪問者有多種不同類的操作。

2.4 複合模式(Compound)

結合兩個或更多模式,組成一個解決方案,解決經常發生的一般性問題。
使用案例:MVC模式(Model/View/Controller),使用了觀察者(Observer)、策略(Strategy)、組合(Composite)、工廠(Factory)、裝飾器(Decorator)等模式。
使用案例:家用電器=介面+資料+邏輯控制,商場=店面+倉庫+邏輯控制。

3 參考文件