設計模式-Java版-全-附程式碼-超生動實例

閱讀推薦:設計模式-簡單篇

項目地址://gitee.com/zwtgit/gof23

學習網站推薦:

設計模式是針對軟體設計中常見問題的工具箱, 其中的工具就是各種經過實踐驗證的解決方案。 即使你從未遇到過這些問題, 了解模式仍然非常有用, 因為它能指導你如何使用面向對象的設計原則來解決各種問題。

演算法更像是菜譜: 提供達成目標的明確步驟。 而模式更像是藍圖: 你可以看到最終的結果和模式的功能, 但需要自己確定實現步驟。

GoF 23(分類)

  • 創建型模式:提供創建對象的機制, 增加已有程式碼的靈活性和可復用性。
  • 結構型模式:介紹如何將對象和類組裝成較大的結構, 並同時保持結構的靈活和高效。
  • 行為模式:負責對象間的高效溝通和職責委派。
不同設計模式的複雜程度、 細節層次以及在整個系統中的應用範圍等方面各不相同。 
我喜歡將其類比於道路的建造: 
如果你希望讓十字路口更加安全, 那麼可以安裝一些交通訊號燈, 或者修建包含行人地下通道在內的多層互通式立交橋。

最基礎的、 底層的模式通常被稱為慣用技巧。 這類模式一般只能在一種程式語言中使用。

最通用的、 高層的模式是構架模式。 開發者可以在任何程式語言中使用這類模式。 
與其他模式不同, 它們可用於整個應用程式的架構設計。

  • 創建型模式:
    • 單例模式,工廠模式, 抽象工廠模式, 建造者模式, 原型模式
  • 結構型模式:
    • 適配器模式, 橋接模式, 裝飾模式, 組合模式, 外觀模式, 享元模式, 代理模式
  • 行為型模式:
    • 模板方法模式, 命令模式, 迭代器模式, 觀察者模式, 中介者模式, 備忘錄模式, 解釋器模式, 狀態模式, 策略模式, 職責鏈模式, 訪問者模式

OOP 七大原則

面向對象程式設計(Object Oriented Programming,OOP)。

1、開閉原則

對擴展開放, 對修改關閉。

2、單一職責原則
每個類應該實現單一的職責,不要存在多於一個導致類變更的原因,否則就應該把類拆分。該原則是實現高內聚、低耦合的指導方針。

3、里氏替換原則(Liskov Substitution Principle)
任何基類可以出現的地方,子類一定可以出現。里氏替換原則是繼承復用的基石,只有當衍生類可以替換基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。

里氏替換原則是對開閉原則的補充。實現開閉原則的關鍵就是抽象化。而基類與子類的繼承關係就是抽象化的具體實現,所以里氏替換原則是對實現抽象化的具體步驟的規範。里氏替換原則中,子類對父類的方法盡量不要重寫和重載。因為父類代表了定義好的結構,通過這個規範的介面與外界交互,子類不應該隨便破壞它。

4、依賴倒轉原則(Dependence Inversion Principle)
面向介面編程,依賴於抽象而不依賴於具體。用到具體類時,不與具體類交互,而與具體類的上層介面交互。

5、介面隔離原則(Interface Segregation Principle)
每個介面中不存在子類用不到卻必須實現的方法,否則就要將介面拆分。使用多個隔離的介面,比使用單個介面(多個介面中的方法聚合到一個的介面)要好。

6、迪米特法則(最少知道原則)(Demeter Principle)
一個類對自己依賴的類知道的越少越好。無論被依賴的類多麼複雜,都應該將邏輯封裝在方法的內部,通過 public 方法提供給外部。這樣當被依賴的類變化時,才能最小的影響該類。

7、合成復用原則(Composite Reuse Principle)
軟體復用時,要先盡量使用組合或者聚合等關聯關係實現,其次才考慮使用繼承。即在一個新對象里通過關聯的方式使用已有對象的一些方法和功能。

Creational Pattems

Singleton

背景

保證一個類只有一個實例, 並提供一個訪問該實例的全局節點。

政府是單例模式的一個很好的示例。 一個國家只有一個官方政府。 不管組成政府的每個人的身份是什麼, 「某政府」 這一稱謂總是鑒別那些掌權者的全局訪問節點。

適用場景

  • 如果程式中的某個類對於所有客戶端只有一個可用的實例,可以使用單例模式。
  • 如果你需要更加嚴格地控制全局變數,可以使用單例模式 。

實例-實現

Java 核心程式庫中仍有相當多的單例示例:

基礎單例(單執行緒)

package com.zwt.BasicSingleton;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public final class Singleton {
    private static Singleton instance;
    public String value;

    private Singleton(String value) {
        // The following code emulates slow initialization.
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        this.value = value;
    }

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

package com.zwt.BasicSingleton;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public class DemoSingleThread {
    public static void main(String[] args) {
        System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
                "If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
                "RESULT:" + "\n");

        Singleton anotherSingleton = Singleton.getInstance("wb");
        Singleton singleton = Singleton.getInstance("zwt");

        System.out.println(singleton.value);
        System.out.println(anotherSingleton.value);
    }
}

If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)

RESULT:

wb
wb

基礎單例(多執行緒)

相同的類在多執行緒環境中會出錯。 多執行緒可能會同時調用構建方法並獲取多個單例類的實例。

package com.zwt.BasicSingleton;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public class DemoMultiThread {
    public static void main(String[] args) {
        System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
                "If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
                "RESULT:" + "\n");

        Thread threadFoo = new Thread(new Threadwb());
        Thread threadBar = new Thread(new Threadzwt());
        threadFoo.start();
        threadBar.start();

    }

    static class Threadwb implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("wb");
            System.out.println(singleton.value);
        }
    }

    static class Threadzwt implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("zwt");
            System.out.println(singleton.value);
        }
    }
}

If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)

RESULT:

wb
zwt

採用延遲載入的執行緒安全單例

為了解決這個問題, 你必須在創建首個單例對象時對執行緒進行同步。

package com.zwt.Singleton;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public final class Singleton {
    private static volatile Singleton instance;

    public String value;

    private Singleton(String value) {
        this.value = value;
    }

    public static Singleton getInstance(String value) {
        Singleton result = instance;
        if (result != null) {
            return result;
        }
        synchronized(Singleton.class) {
            if (instance == null) {
                instance = new Singleton(value);
            }
            return instance;
        }
    }
}
package com.zwt.Singleton;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public class DemoMultiThread {
    public static void main(String[] args) {
        System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
                "If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
                "RESULT:" + "\n");
        Thread threadFoo = new Thread(new ThreadFoo());
        Thread threadBar = new Thread(new ThreadBar());
        threadFoo.start();
        threadBar.start();
    }

    static class ThreadFoo implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("FOO");
            System.out.println(singleton.value);
        }
    }

    static class ThreadBar implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance("BAR");
            System.out.println(singleton.value);
        }
    }
}
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)

RESULT:

FOO
FOO

理論-實現

Eager initialization

在預先初始化中,單例類的實例是在類載入時創建的,這是創建單例類的最簡單方法,但它有一個缺點,即即使客戶端應用程式可能不會使用它,也會創建實例。

如果您的單例類沒有使用大量資源,則可以使用這種方法。但是在大多數情況下,Singleton 類是為文件系統、資料庫連接等資源創建的。除非客戶端調用該getInstance方法,否則我們應該避免實例化。此外,此方法不提供任何異常處理選項。

public class EagerInitializedSingleton {
    
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    //private constructor to avoid client applications to use constructor
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance(){
        return instance;
    }
}

Static block initialization

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton(){}
    
    //static block initialization for exception handling
    static{
        try{
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

Lazy Initialization

在單執行緒環境下工作正常,但是當涉及到多執行緒系統時,如果多個執行緒同時處於 if 條件內,則可能會導致問題。它將破壞單例模式,兩個執行緒將獲得單例類的不同實例。

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;
    
    private LazyInitializedSingleton(){}
    
    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

Thread Safe Singleton

public class ThreadSafeSingleton {

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

但由於與同步方法相關的成本,它降低了性能,儘管我們只需要它用於可能創建單獨實例的前幾個執行緒(閱讀:Java 同步)。為了每次都避免這種額外的開銷,使用了雙重檢查鎖定原則。在這種方法中,同步塊在 if 條件中使用,並進行額外檢查以確保僅創建單例類的一個實例。


public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    if(instance == null){
        synchronized (ThreadSafeSingleton.class) {
            if(instance == null){
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

Bill Pugh Singleton Implementation

在 Java 5 之前,java 記憶體模型有很多問題,並且在某些場景中,如果太多執行緒試圖同時獲取 Singleton 類的實例,上述方法會失敗。

請注意包含單例類實例的私有內部靜態類。當載入單例類時,SingletonHelper類不會載入到記憶體中,只有當有人調用getInstance方法時,才會載入這個類並創建單例類實例。

這是 Singleton 類最廣泛使用的方法,因為它不需要同步。我在我的許多項目中都使用了這種方法,而且它也很容易理解和實現。

public class BillPughSingleton {

    private BillPughSingleton(){}
    
    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    
    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}

Using Reflection to destroy Singleton Pattern

反射可用於破壞上述所有單例實現方法。讓我們用一個示例類來看看這個。

您運行上面的測試類時,您會注意到兩個實例的 hashCode 不相同,這破壞了單例模式。反射非常強大,並在很多框架中使用,如 Spring 和 Hibernate。

import java.lang.reflect.Constructor;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public class ReflectionSingletonTest {

    public static void main(String[] args) {

        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        EagerInitializedSingleton instanceThree = null;

        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();

            for (Constructor constructor : constructors) {
                //Below code will destroy the singleton pattern
                //關閉了檢查
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                instanceThree = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }


        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
        System.out.println(instanceThree.hashCode());

    }

}

Enum Singleton

public enum EnumSingleton {

    INSTANCE;
    
    public static void doSomething(){
        //do something
    }
}

Serialization and Singleton

有時在分散式系統中,我們需要在 Singleton 類中實現 Serializable 介面,以便我們可以將其狀態存儲在文件系統中並在以後的某個時間點檢索它。這是一個也實現了 Serializable 介面的小型單例類。

package com.zwt.SerializableSingleton;

import java.io.Serializable;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton() {
    }

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

    //這樣當JVM從記憶體中反序列化地"組裝"一個新對象時
    //就會自動調用這個 readResolve方法來返回我們指定好的對象了,
    protected Object readResolve() {
        return getInstance();
    }


}

序列化單例類的問題在於,無論何時反序列化它,它都會創建該類的一個新實例。讓我們用一個簡單的程式來看看它。

package com.zwt.SerializableSingleton;

import java.io.*;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-09
 * Blog: //www.cnblogs.com/zwtblog/
 */
public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {

        SerializedSingleton instanceOne = SerializedSingleton.getInstance();

        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        //deserailize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode=" + instanceOne.hashCode());
        System.out.println("instanceTwo hashCode=" + instanceTwo.hashCode());

        System.out.println("===============");


    }

}

所以它破壞了單例模式,為了克服這種情況,我們需要做的就是提供readResolve()方法的實現。

protected Object readResolve() {
    return getInstance();
}

與其他模式關係

  • 外觀模式類通常可以轉換為單例模式類, 因為在大部分情況下一個外觀對象就足夠了。
  • 如果你能將對象的所有共享狀態簡化為一個享元對象, 那麼享元模式就和單例類似了。 但這兩個模式有兩個根本性的不同。
    1. 只會有一個單例實體, 但是享元類可以有多個實體, 各實體的內在狀態也可以不同。
    2. 單例對象可以是可變的。 享元對象是不可變的。
  • 抽象工廠模式生成器模式原型模式都可以用單例來實現。

Factory

背景

工廠方法模式是一種創建型設計模式, 其在父類中提供一個創建對象的方法, 允許子類決定實例化對象的類型。

假設你正在開發一款物流管理應用。 最初版本只能處理卡車運輸, 因此大部分程式碼都在位於名為 卡車的類中。

一段時間後, 這款應用變得極受歡迎。 你每天都能收到十幾次來自海運公司的請求, 希望應用能夠支援海上物流功能。

如果程式碼其餘部分與現有類已經存在耦合關係, 那麼向程式中添加新類其實並沒有那麼容易。

工廠方法模式建議使用特殊的工廠方法代替對於對象構造函數的直接調用 (即使用 new運算符)。 
不用擔心, 對象仍將通過 new運算符創建, 只是該運算符改在工廠方法中調用罷了。 工廠方法返回的對象通常被稱作 「產品」。

乍看之下, 這種更改可能毫無意義: 
我們只是改變了程式中調用構造函數的位置而已。 但是, 仔細想一下, 現在你可以在子類中重寫工廠方法, 從而改變其創建產品的類型。

但有一點需要注意:僅當這些產品具有共同的基類或者介面時, 子類才能返回不同類型的產品, 同時基類中的工廠方法還應將其返回類型聲明為這一共有介面。

調用工廠方法的程式碼 (通常被稱為客戶端程式碼) 無需了解不同子類返回實際對象之間的差別。 客戶端將所有產品視為抽象的 運輸 。 客戶端知道所有運輸對象都提供 交付方法, 但是並不關心其具體實現方式。

適用場景

  • 當你在編寫程式碼的過程中, 如果無法預知對象確切類別及其依賴關係時, 可使用工廠方法。
  • 如果你希望用戶能擴展你軟體庫或框架的內部組件, 可使用工廠方法。
  • 如果你希望復用現有對象來節省系統資源, 而不是每次都重新創建對象, 可使用工廠方法。

實例-實現

工廠方法模式在 Java 程式碼中得到了廣泛使用。 當你需要在程式碼中提供高層次的靈活性時, 該模式會非常實用。

  1. java.util.Calendar、ResourceBundle 和 NumberFormatgetInstance()方法使用工廠模式。
  2. valueOf() Boolean、Integer 等包裝類中的方法。

核心 Java 程式庫中有該模式的應用:

識別方法: 工廠方法可通過構建方法來識別, 它會創建具體類的對象, 但以抽象類型或介面的形式返回這些對象。

生成跨平台的 GUI 元素

buttons

buttons/Button.java: 通用產品介面

package com.zwt.buttons;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */
/**
 * Common interface for all buttons.
 */
public interface Button {
    void render();

    void onClick();
}

buttons/HtmlButton.java: 具體產品

package com.zwt.buttons;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */
/**
 * HTML button implementation.
 */
public class HtmlButton implements Button {

    public void render() {
        System.out.println("<button>Test Button</button>");
        onClick();
    }

    public void onClick() {
        System.out.println("Click! Button says - 'Hello World!'");
    }
}

buttons/WindowsButton.java: 另一個具體產品

package com.zwt.buttons;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */
/**
 * Windows button implementation.
 */
public class WindowsButton implements Button {
    JPanel panel = new JPanel();
    JFrame frame = new JFrame();
    JButton button;

    public void render() {
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JLabel label = new JLabel("Hello World!");
        label.setOpaque(true);
        label.setBackground(new Color(235, 233, 126));
        label.setFont(new Font("Dialog", Font.BOLD, 44));
        label.setHorizontalAlignment(SwingConstants.CENTER);
        panel.setLayout(new FlowLayout(FlowLayout.CENTER));
        frame.getContentPane().add(panel);
        panel.add(label);
        onClick();
        panel.add(button);

        frame.setSize(320, 200);
        frame.setVisible(true);
        onClick();
    }

    public void onClick() {
        button = new JButton("Exit");
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                frame.setVisible(false);
                System.exit(0);
            }
        });
    }
}

factory

factory/Dialog.java: 基礎創建者

package com.zwt.factory;

import com.zwt.buttons.Button;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

/**
 * Base factory class. Note that "factory" is merely a role for the class. It
 * should have some core business logic which needs different products to be
 * created.
 */
public abstract class Dialog {

    public void renderWindow() {
        // ... other code ...

        Button okButton = createButton();
        okButton.render();
    }

    /**
     * Subclasses will override this method in order to create specific button
     * objects.
     */
    public abstract Button createButton();
}

factory/HtmlDialog.java: 具體創建者

package com.zwt.factory;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

import com.zwt.buttons.Button;
import com.zwt.buttons.HtmlButton;

/**
 * HTML Dialog will produce HTML buttons.
 */
public class HtmlDialog extends Dialog {

    @Override
    public Button createButton() {
        return new HtmlButton();
    }
}

factory/WindowsDialog.java: 另一個具體創建者

package com.zwt.factory;

import com.zwt.buttons.Button;
import com.zwt.buttons.WindowsButton;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

/**
 * Windows Dialog will produce Windows buttons.
 */
public class WindowsDialog extends Dialog {

    @Override
    public Button createButton() {
        return new WindowsButton();
    }
}

Demo.java: 客戶端程式碼

package com.zwt;
/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

import com.zwt.factory.Dialog;
import com.zwt.factory.HtmlDialog;
import com.zwt.factory.WindowsDialog;

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    private static Dialog dialog;

    public static void main(String[] args) {
        configure();
        runBusinessLogic();
    }

    /**
     * The concrete factory is usually chosen depending on configuration or
     * environment options.
     */
    static void configure() {
        if (System.getProperty("os.name").equals("Windows 10")) {
            dialog = new WindowsDialog();
        } else {
            dialog = new HtmlDialog();
        }
    }

    /**
     * All of the client code should work with factories and products through
     * abstract interfaces. This way it does not care which factory it works
     * with and what kind of product it returns.
     */
    static void runBusinessLogic() {
        dialog.renderWindow();
    }
}

OutputDemo.txt: 執行結果 (Html­Dialog)

<button>Test Button</button>
Click! Button says - 'Hello World!'

OutputDemo.png: 執行結果 (Windows­Dialog)

與其他模式的關係

Abstract Factory

背景

抽象工廠模式是一種創建型設計模式, 它能創建一系列相關的對象, 而無需指定其具體類。

假設你正在開發一款傢具商店模擬器。 你的程式碼中包括一些類, 用於表示:

一系列相關產品, 例如 椅子Chair 、 ​ 沙發Sofa 和 咖啡桌Coffee ­Table 。

系列產品的不同變體。 例如, 你可以使用 現代Modern 、 ​ 維多利亞Victorian 、 ​ 裝飾風藝術Art­Deco等風格生成 椅子 、 ​ 沙發 和 咖啡桌 。

適用場景

  • 如果程式碼需要與多個不同系列的相關產品交互, 但是由於無法提前獲取相關資訊, 或者出於對未來擴展性的考慮, 你不希望程式碼基於產品的具體類進行構建, 在這種情況下, 你可以使用抽象工廠。
  • 如果你有一個基於一組抽象方法的類, 且其主要功能因此變得不明確, 那麼在這種情況下可以考慮使用抽象工廠模式。

實例-實現

使用示例: 抽象工廠模式在 Java 程式碼中很常見。 許多框架和程式庫會將它作為擴展和自定義其標準組件的一種方式。

以下是來自核心 Java 程式庫的一些示例:

識別方法: 我們可以通過方法來識別該模式——其會返回一個工廠對象。 接下來, 工廠將被用於創建特定的子組件。

跨平台 GUI 組件系列及其創建方式

在本例中, 按鈕和複選框將被作為產品。 它們有兩個變體: macOS 版和 Windows 版。

抽象工廠定義了用於創建按鈕和複選框的介面。 而兩個具體工廠都會返回同一變體的兩個產品。

客戶端程式碼使用抽象介面與工廠和產品進行交互。 同樣的程式碼能與依賴於不同工廠對象類型的多種產品變體進行交互。

buttons: 第一個產品層次結構

buttons/Button.java

package com.zwt.buttons;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

/**
 * Abstract Factory assumes that you have several families of products,
 * structured into separate class hierarchies (Button/Checkbox). All products of
 * the same family have the common interface.
 * <p>
 * This is the common interface for buttons family.
 */
public interface Button {
    void paint();
}

buttons/MacOSButton.java

package com.zwt.buttons;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

/**
 * All products families have the same varieties (MacOS/Windows).
 * <p>
 * This is a MacOS variant of a button.
 */
public class MacOSButton implements Button {

    @Override
    public void paint() {
        System.out.println("You have created MacOSButton.");
    }
}

buttons/WindowsButton.java

package refactoring_guru.abstract_factory.example.buttons;

package com.zwt.buttons;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

/**
 * All products families have the same varieties (MacOS/Windows).
 * <p>
 * This is another variant of a button.
 */
public class WindowsButton implements Button {

    @Override
    public void paint() {
        System.out.println("You have created WindowsButton.");
    }
}

checkboxes: 第二個產品層次結構

checkboxes/Checkbox.java

package com.zwt.checkboxes;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

/**
 * Checkboxes is the second product family. It has the same variants as buttons.
 */
public interface Checkbox {
    void paint();
}

checkboxes/MacOSCheckbox.java

/**
 * All products families have the same varieties (MacOS/Windows).
 *
 * This is a variant of a checkbox.
 */
public class MacOSCheckbox implements Checkbox {

    @Override
    public void paint() {
        System.out.println("You have created MacOSCheckbox.");
    }
}

checkboxes/WindowsCheckbox.java

/**
 * All products families have the same varieties (MacOS/Windows).
 *
 * This is another variant of a checkbox.
 */
public class WindowsCheckbox implements Checkbox {

    @Override
    public void paint() {
        System.out.println("You have created WindowsCheckbox.");
    }
}

factories

factories/GUIFactory.java: 抽象工廠

package com.zwt.factories;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

import com.zwt.buttons.Button;
import com.zwt.checkboxes.Checkbox;

/**
 * Abstract factory knows about all (abstract) product types.
 */
public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

factories/MacOSFactory.java: 具體工廠 ( mac­OS)

package com.zwt.factories;

/**
 * @author ML李嘉圖
 * @version createtime: 2021-11-10
 * Blog: //www.cnblogs.com/zwtblog/
 */

import com.zwt.buttons.Button;
import com.zwt.buttons.MacOSButton;
import com.zwt.checkboxes.Checkbox;
import com.zwt.checkboxes.MacOSCheckbox;

/**
 * Each concrete factory extends basic factory and responsible for creating
 * products of a single variety.
 */
public class MacOSFactory implements GUIFactory {

    @Override
    public Button createButton() {
        return new MacOSButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacOSCheckbox();
    }
}

factories/WindowsFactory.java: 具體工廠 (Windows)

package com.zwt.factories;

import com.zwt.buttons.Button;
import com.zwt.buttons.WindowsButton;
import com.zwt.checkboxes.Checkbox;
import com.zwt.checkboxes.WindowsCheckbox;

/**
 * Each concrete factory extends basic factory and responsible for creating
 * products of a single variety.
 */
public class WindowsFactory implements GUIFactory {

    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

app

app/Application.java: 客戶端程式碼

package com.zwt.app;

import com.zwt.buttons.Button;
import com.zwt.checkboxes.Checkbox;
import com.zwt.factories.GUIFactory;

/**
 * Factory users don't care which concrete factory they use since they work with
 * factories and products through abstract interfaces.
 */
public class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void paint() {
        button.paint();
        checkbox.paint();
    }
}

Demo.java: 程式配置

package com.zwt;

import com.zwt.app.Application;
import com.zwt.factories.GUIFactory;
import com.zwt.factories.MacOSFactory;
import com.zwt.factories.WindowsFactory;

/**
 * Demo class. Everything comes together here.
 */
public class Demo {

    /**
     * Application picks the factory type and creates it in run time (usually at
     * initialization stage), depending on the configuration or environment
     * variables.
     */
    private static Application configureApplication() {
        Application app;
        GUIFactory factory;
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("mac")) {
            factory = new MacOSFactory();
            app = new Application(factory);
        } else {
            factory = new WindowsFactory();
            app = new Application(factory);
        }
        return app;
    }

    public static void main(String[] args) {
        Application app = configureApplication();
        app.paint();
    }
}

與其他模式的關係

Builder

亦稱:建造者模式、Builder

背景

假設有這樣一個複雜對象, 在對其進行構造時需要對諸多成員變數和嵌套對象進行繁複的初始化工作。 
這些初始化程式碼通常深藏於一個包含眾多參數且讓人基本看不懂的構造函數中; 
甚至還有更糟糕的情況, 那就是這些程式碼散落在客戶端程式碼的多個位置。

生成器模式建議將對象構造程式碼從產品類中抽取出來, 並將其放在一個名為生成器的獨立對象中。

適用場景

  • 使用生成器模式可避免 「重疊構造函數 (telescopic constructor)」 的出現。
  • 當你希望使用程式碼創建不同形式的產品 (例如石頭或木頭房屋) 時, 可使用生成器模式。
  • 使用生成器構造組合樹或其他複雜對象。

實例-實現

生成器在 Java 核心程式庫中得到了廣泛的應用:

識別方法: 生成器模式可以通過類來識別, 它擁有一個構建方法和多個配置結果對象的方法。 生成器方法通常支援鏈式編程 (例如 someBuilder->setValueA(1)->setValueB(2)->create() )。

分步製造汽車

在本例中, 生成器模式允許你分步驟地製造不同型號的汽車。

示例還展示了生成器如何使用相同的生產過程製造不同類型的產品 (汽車手冊)。

主管控制著構造順序。 它知道製造各種汽車型號需要調用的生產步驟。 它僅與汽車的通用介面進行交互。 這樣就能將不同類型的生成器傳遞給主管了。

最終結果將從生成器對象中獲得, 因為主管不知道最終產品的類型。 只有生成器對象知道自己生成的產品是什麼。

builders

builders/Builder.java: 通用生成器介面

/**
 * Builder interface defines all possible ways to configure a product.
 */
public interface Builder {
    void setCarType(CarType type);
    void setSeats(int seats);
    void setEngine(Engine engine);
    void setTransmission(Transmission transmission);
    void setTripComputer(TripComputer tripComputer);
    void setGPSNavigator(GPSNavigator gpsNavigator);
}

builders/CarBuilder.java: 汽車生成器

/**
 * Concrete builders implement steps defined in the common interface.
 */
public class CarBuilder implements Builder {
    private CarType type;
    private int seats;
    private Engine engine;
    private Transmission transmission;
    private TripComputer tripComputer;
    private GPSNavigator gpsNavigator;

    public void setCarType(CarType type) {
        this.type = type;
    }

    @Override
    public void setSeats(int seats) {
        this.seats = seats;
    }

    @Override
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    @Override
    public void setTransmission(Transmission transmission) {
        this.transmission = transmission;
    }

    @Override
    public void setTripComputer(TripComputer tripComputer) {
        this.tripComputer = tripComputer;
    }

    @Override
    public void setGPSNavigator(GPSNavigator gpsNavigator) {
        this.gpsNavigator = gpsNavigator;
    }

    public Car getResult() {
        return new Car(type, seats, engine, transmission, tripComputer, gpsNavigator);
    }
}

builders/CarManualBuilder.java: 汽車手冊生成器

/**
 * Unlike other creational patterns, Builder can construct unrelated products,
 * which don't have the common interface.
 *
 * In this case we build a user manual for a car, using the same steps as we
 * built a car. This allows to produce manuals for specific car models,
 * configured with different features.
 */
public class CarManualBuilder implements Builder{
    private CarType type;
    private int seats;
    private Engine engine;
    private Transmission transmission;
    private TripComputer tripComputer;
    private GPSNavigator gpsNavigator;

    @Override
    public void setCarType(CarType type) {
        this.type = type;
    }

    @Override
    public void setSeats(int seats) {
        this.seats = seats;
    }

    @Override
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    @Override
    public void setTransmission(Transmission transmission) {
        this.transmission = transmission;
    }

    @Override
    public void setTripComputer(TripComputer tripComputer) {
        this.tripComputer = tripComputer;
    }

    @Override
    public void setGPSNavigator(GPSNavigator gpsNavigator) {
        this.gpsNavigator = gpsNavigator;
    }

    public Manual getResult() {
        return new Manual(type, seats, engine, transmission, tripComputer, gpsNavigator);
    }
}

cars

cars/Car.java: 汽車產品

/**
 * Car is a product class.
 */
public class Car {
    private final CarType carType;
    private final int seats;
    private final Engine engine;
    private final Transmission transmission;
    private final TripComputer tripComputer;
    private final GPSNavigator gpsNavigator;
    private double fuel = 0;

    public Car(CarType carType, int seats, Engine engine, Transmission transmission,
               TripComputer tripComputer, GPSNavigator gpsNavigator) {
        this.carType = carType;
        this.seats = seats;
        this.engine = engine;
        this.transmission = transmission;
        this.tripComputer = tripComputer;
        if (this.tripComputer != null) {
            this.tripComputer.setCar(this);
        }
        this.gpsNavigator = gpsNavigator;
    }

    public CarType getCarType() {
        return carType;
    }

    public double getFuel() {
        return fuel;
    }

    public void setFuel(double fuel) {
        this.fuel = fuel;
    }

    public int getSeats() {
        return seats;
    }

    public Engine getEngine() {
        return engine;
    }

    public Transmission getTransmission() {
        return transmission;
    }

    public TripComputer getTripComputer() {
        return tripComputer;
    }

    public GPSNavigator getGpsNavigator() {
        return gpsNavigator;
    }
}

cars/Manual.java: 手冊產品

/**
 * Car manual is another product. Note that it does not have the same ancestor
 * as a Car. They are not related.
 */
public class Manual {
    private final CarType carType;
    private final int seats;
    private final Engine engine;
    private final Transmission transmission;
    private final TripComputer tripComputer;
    private final GPSNavigator gpsNavigator;

    public Manual(CarType carType, int seats, Engine engine, Transmission transmission,
                  TripComputer tripComputer, GPSNavigator gpsNavigator) {
        this.carType = carType;
        this.seats = seats;
        this.engine = engine;
        this.transmission = transmission;
        this.tripComputer = tripComputer;
        this.gpsNavigator = gpsNavigator;
    }

    public String print() {
        String info = "";
        info += "Type of car: " + carType + "\n";
        info += "Count of seats: " + seats + "\n";
        info += "Engine: volume - " + engine.getVolume() + "; mileage - " + engine.getMileage() + "\n";
        info += "Transmission: " + transmission + "\n";
        if (this.tripComputer != null) {
            info += "Trip Computer: Functional" + "\n";
        } else {
            info += "Trip Computer: N/A" + "\n";
        }
        if (this.gpsNavigator != null) {
            info += "GPS Navigator: Functional" + "\n";
        } else {
            info += "GPS Navigator: N/A" + "\n";
        }
        return info;
    }
}

cars/CarType.java

public enum CarType {
    CITY_CAR, SPORTS_CAR, SUV
}

components

components/Engine.java: 產品特徵 1

/**
 * Just another feature of a car.
 */
public class Engine {
    private final double volume;
    private double mileage;
    private boolean started;

    public Engine(double volume, double mileage) {
        this.volume = volume;
        this.mileage = mileage;
    }

    public void on() {
        started = true;
    }

    public void off() {
        started = false;
    }

    public boolean isStarted() {
        return started;
    }

    public void go(double mileage) {
        if (started) {
            this.mileage += mileage;
        } else {
            System.err.println("Cannot go(), you must start engine first!");
        }
    }

    public double getVolume() {
        return volume;
    }

    public double getMileage() {
        return mileage;
    }
}

components/GPSNavigator.java: 產品特徵 2

/**
 * Just another feature of a car.
 */
public class GPSNavigator {
    private String route;

    public GPSNavigator() {
        this.route = "221b, Baker Street, London  to Scotland Yard, 8-10 Broadway, London";
    }

    public GPSNavigator(String manualRoute) {
        this.route = manualRoute;
    }

    public String getRoute() {
        return route;
    }
}

components/Transmission.java: 產品特徵 3

/**
 * Just another feature of a car.
 */
public enum Transmission {
    SINGLE_SPEED, MANUAL, AUTOMATIC, SEMI_AUTOMATIC
}

components/TripComputer.java: 產品特徵 4

/**
 * Just another feature of a car.
 */
public class TripComputer {

    private Car car;

    public void setCar(Car car) {
        this.car = car;
    }

    public void showFuelLevel() {
        System.out.println("Fuel level: " + car.getFuel());
    }

    public void showStatus() {
        if (this.car.getEngine().isStarted()) {
            System.out.println("Car is started");
        } else {
            System.out.println("Car isn't started");
        }
    }
}

director

director/Director.java: 主管控制生成器

/**
 * Director defines the order of building steps. It works with a builder object
 * through common Builder interface. Therefore it may not know what product is
 * being built.
 */
public class Director {

    public void constructSportsCar(Builder builder) {
        builder.setCarType(CarType.SPORTS_CAR);
        builder.setSeats(2);
        builder.setEngine(new Engine(3.0, 0));
        builder.setTransmission(Transmission.SEMI_AUTOMATIC);
        builder.setTripComputer(new TripComputer());
        builder.setGPSNavigator(new GPSNavigator());
    }

    public void constructCityCar(Builder builder) {
        builder.setCarType(CarType.CITY_CAR);
        builder.setSeats(2);
        builder.setEngine(new Engine(1.2, 0));
        builder.setTransmission(Transmission.AUTOMATIC);
        builder.setTripComputer(new TripComputer());
        builder.setGPSNavigator(new GPSNavigator());
    }

    public void constructSUV(Builder builder) {
        builder.setCarType(CarType.SUV);
        builder.setSeats(4);
        builder.setEngine(new Engine(2.5, 0));
        builder.setTransmission(Transmission.MANUAL);
        builder.setGPSNavigator(new GPSNavigator());
    }
}

Demo.java: 客戶端程式碼

/**
 * Demo class. Everything comes together here.
 */
public class Demo {

    public static void main(String[] args) {
        Director director = new Director();

        // Director gets the concrete builder object from the client
        // (application code). That's because application knows better which
        // builder to use to get a specific product.
        CarBuilder builder = new CarBuilder();
        director.constructSportsCar(builder);

        // The final product is often retrieved from a builder object, since
        // Director is not aware and not dependent on concrete builders and
        // products.
        Car car = builder.getResult();
        System.out.println("Car built:\n" + car.getCarType());


        CarManualBuilder manualBuilder = new CarManualBuilder();

        // Director may know several building recipes.
        director.constructSportsCar(manualBuilder);
        Manual carManual = manualBuilder.getResult();
        System.out.println("\nCar manual built:\n" + carManual.print());
    }

}

OutputDemo.txt: 執行結果

Car built:
SPORTS_CAR

Car manual built:
Type of car: SPORTS_CAR
Count of seats: 2
Engine: volume - 3.0; mileage - 0.0
Transmission: SEMI_AUTOMATIC
Trip Computer: Functional
GPS Navigator: Functional

與其他模式的關係

Prototype

原型模式是一種創建型設計模式, 使你能夠複製已有對象, 而又無需使程式碼依賴它們所屬的類。

背景

如果你有一個對象, 並希望生成與其完全相同的一個複製品, 你該如何實現呢? 
首先, 你必須新建一個屬於相同類的對象。 
然後, 你必須遍歷原始對象的所有成員變數, 並將成員變數值複製到新對象中。

不錯! 但有個小問題。 
並非所有對象都能通過這種方式進行複製, 因為有些對象可能擁有私有成員變數, 它們在對象本身以外是不可見的。

直接複製還有另外一個問題。 
因為你必須知道對象所屬的類才能創建複製品, 所以程式碼必須依賴該類。 
即使你可以接受額外的依賴性, 
那還有另外一個問題: 有時你只知道對象所實現的介面, 而不知道其所屬的具體類, 
比如可向方法的某個參數傳入實現了某個介面的任何對象。

原型模式將克隆過程委派給被克隆的實際對象。 模式為所有支援克隆的對象聲明了一個通用介面, 該介面讓你能夠克隆對象, 同時又無需將程式碼和對象所屬類耦合。 通常情況下, 這樣的介面中僅包含一個 克隆方法。

所有的類對 克隆方法的實現都非常相似。 該方法會創建一個當前類的對象, 然後將原始對象所有的成員變數值複製到新建的類中。 你甚至可以複製私有成員變數, 因為絕大部分程式語言都允許對象訪問其同類對象的私有成員變數。

支援克隆的對象即為原型。 當你的對象有幾十個成員變數和幾百種類型時, 對其進行克隆甚至可以代替子類的構造。

其運作方式如下: 創建一系列不同類型的對象並不同的方式對其進行配置。 如果所需對象與預先配置的對象相同, 那麼你只需克隆原型即可, 無需新建一個對象。

適用場景

  • 如果你需要複製一些對象, 同時又希望程式碼獨立於這些對象所屬的具體類, 可以使用原型模式。
  • 如果子類的區別僅在於其對象的初始化方式, 那麼你可以使用該模式來減少子類的數量。 別人創建這些子類的目的可能是為了創建特定類型的對象。

實例-實現

使用示例: Java 的 Cloneable (可克隆) 介面就是立即可用的原型模式。

任何類都可通過實現該介面來實現可被克隆的性質。

識別方法: 原型可以簡單地通過 clonecopy等方法來識別。

複製圖形

讓我們來看看在不使用標準 Cloneable介面的情況下如何實現原型模式。

shapes: 形狀列表

shapes/Shape.java: 通用形狀介面

import java.util.Objects;

public abstract class Shape {
    public int x;
    public int y;
    public String color;

    public Shape() {
    }

    public Shape(Shape target) {
        if (target != null) {
            this.x = target.x;
            this.y = target.y;
            this.color = target.color;
        }
    }

    public abstract Shape clone();

    @Override
    public boolean equals(Object object2) {
        if (!(object2 instanceof Shape)) return false;
        Shape shape2 = (Shape) object2;
        return shape2.x == x && shape2.y == y && Objects.equals(shape2.color, color);
    }
}

shapes/Circle.java: 簡單形狀

public class Circle extends Shape {
    public int radius;

    public Circle() {
    }

    public Circle(Circle target) {
        super(target);
        if (target != null) {
            this.radius = target.radius;
        }
    }

    @Override
    public Shape clone() {
        return new Circle(this);
    }

    @Override
    public boolean equals(Object object2) {
        if (!(object2 instanceof Circle) || !super.equals(object2)) return false;
        Circle shape2 = (Circle) object2;
        return shape2.radius == radius;
    }
}

shapes/Rectangle.java: 另一個形狀

public class Rectangle extends Shape {
    public int width;
    public int height;

    public Rectangle() {
    }

    public Rectangle(Rectangle target) {
        super(target);
        if (target != null) {
            this.width = target.width;
            this.height = target.height;
        }
    }

    @Override
    public Shape clone() {
        return new Rectangle(this);
    }

    @Override
    public boolean equals(Object object2) {
        if (!(object2 instanceof Rectangle) || !super.equals(object2)) return false;
        Rectangle shape2 = (Rectangle) object2;
        return shape2.width == width && shape2.height == height;
    }
}

Demo.java: 克隆示例

import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<>();
        List<Shape> shapesCopy = new ArrayList<>();

        Circle circle = new Circle();
        circle.x = 10;
        circle.y = 20;
        circle.radius = 15;
        circle.color = "red";
        shapes.add(circle);

        Circle anotherCircle = (Circle) circle.clone();
        shapes.add(anotherCircle);

        Rectangle rectangle = new Rectangle();
        rectangle.width = 10;
        rectangle.height = 20;
        rectangle.color = "blue";
        shapes.add(rectangle);

        cloneAndCompare(shapes, shapesCopy);
    }

    private static void cloneAndCompare(List<Shape> shapes, List<Shape> shapesCopy) {
        for (Shape shape : shapes) {
            shapesCopy.add(shape.clone());
        }

        for (int i = 0; i < shapes.size(); i++) {
            if (shapes.get(i) != shapesCopy.get(i)) {
                System.out.println(i + ": Shapes are different objects (yay!)");
                if (shapes.get(i).equals(shapesCopy.get(i))) {
                    System.out.println(i + ": And they are identical (yay!)");
                } else {
                    System.out.println(i + ": But they are not identical (booo!)");
                }
            } else {
                System.out.println(i + ": Shape objects are the same (booo!)");
            }
        }
    }
}

OutputDemo.txt: 執行結果

0: Shapes are different objects (yay!)
0: And they are identical (yay!)
1: Shapes are different objects (yay!)
1: And they are identical (yay!)
2: Shapes are different objects (yay!)
2: And they are identical (yay!)

與其他模式的關係

StructuralPatterns

Adapter

背景

適配器模式是一種結構型設計模式, 它能使介面不兼容的對象能夠相互合作。

假如你正在開發一款股票市場監測程式, 
它會從不同來源下載 XML 格式的股票數據, 然後向用戶呈現出美觀的圖表。

在開發過程中, 你決定在程式中整合一個第三方智慧分析函數庫。 
但是遇到了一個問題, 那就是分析函數庫只兼容 JSON 格式的數據。

你可以修改程式庫來支援 XML。 
但是, 這可能需要修改部分依賴該程式庫的現有程式碼。 
甚至還有更糟糕的情況, 你可能根本沒有程式庫的源程式碼, 從而無法對其進行修改。
你可以創建一個適配器。 這是一個特殊的對象, 能夠轉換對象介面, 使其能與其他對象進行交互。

適配器模式通過封裝對象將複雜的轉換過程隱藏於幕後。 被封裝的對象甚至察覺不到適配器的存在。 

對象適配器:實現時使用了構成原則: 適配器實現了其中一個對象的介面, 並對另一個對象進行封裝。

類適配器: 這一實現使用了繼承機制: 適配器同時繼承兩個對象的介面。

適用場景

  • 當你希望使用某個類, 但是其介面與其他程式碼不兼容時, 可以使用適配器類。
  • 如果您需要復用這樣一些類, 他們處於同一個繼承體系, 並且他們又有了額外的一些共同的方法, 但是這些共同的方法不是所有在這一繼承體系中的子類所具有的共性。

實例-實現

使用示例: 適配器模式在 Java 程式碼中很常見。 基於一些遺留程式碼的系統常常會使用該模式。 在這種情況下, 適配器讓遺留程式碼與現代的類得以相互合作。

Java 核心程式庫中有一些標準的適配器:

識別方法: 適配器可以通過以不同抽象或介面類型實例為參數的構造函數來識別。 當適配器的任何方法被調用時, 它會將參數轉換為合適的格式, 然後將調用定向到其封裝對象中的一個或多個方法。

讓方釘適配圓孔

這個簡單的例子展示了適配器如何讓不兼容的對象相互合作。

round

round/RoundHole.java: 圓孔

/**
 * RoundHoles are compatible with RoundPegs.
 */
public class RoundHole {
    private double radius;

    public RoundHole(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public boolean fits(RoundPeg peg) {
        boolean result;
        result = (this.getRadius() >= peg.getRadius());
        return result;
    }
}

round/RoundPeg.java: 圓釘

/**
 * RoundPegs are compatible with RoundHoles.
 */
public class RoundPeg {
    private double radius;

    public RoundPeg() {}

    public RoundPeg(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

square

square/SquarePeg.java: 方釘

/**
 * SquarePegs are not compatible with RoundHoles (they were implemented by
 * previous development team). But we have to integrate them into our program.
 */
public class SquarePeg {
    private double width;

    public SquarePeg(double width) {
        this.width = width;
    }

    public double getWidth() {
        return width;
    }

    public double getSquare() {
        double result;
        result = Math.pow(this.width, 2);
        return result;
    }
}

adapters

adapters/SquarePegAdapter.java: 方釘到圓孔的適配器

/**
 * Adapter allows fitting square pegs into round holes.
 */
public class SquarePegAdapter extends RoundPeg {
    private SquarePeg peg;

    public SquarePegAdapter(SquarePeg peg) {
        this.peg = peg;
    }

    @Override
    public double getRadius() {
        double result;
        // Calculate a minimum circle radius, which can fit this peg.
        result = (Math.sqrt(Math.pow((peg.getWidth() / 2), 2) * 2));
        return result;
    }
}

Demo.java: 客戶端程式碼

/**
 * Somewhere in client code...
 */
public class Demo {
    public static void main(String[] args) {
        // Round fits round, no surprise.
        RoundHole hole = new RoundHole(5);
        RoundPeg rpeg = new RoundPeg(5);
        if (hole.fits(rpeg)) {
            System.out.println("Round peg r5 fits round hole r5.");
        }

        SquarePeg smallSqPeg = new SquarePeg(2);
        SquarePeg largeSqPeg = new SquarePeg(20);
        // hole.fits(smallSqPeg); // Won't compile.

        // Adapter solves the problem.
        SquarePegAdapter smallSqPegAdapter = new SquarePegAdapter(smallSqPeg);
        SquarePegAdapter largeSqPegAdapter = new SquarePegAdapter(largeSqPeg);
        if (hole.fits(smallSqPegAdapter)) {
            System.out.println("Square peg w2 fits round hole r5.");
        }
        if (!hole.fits(largeSqPegAdapter)) {
            System.out.println("Square peg w20 does not fit into round hole r5.");
        }
    }
}

OutputDemo.txt: 執行結果

Round peg r5 fits round hole r5.
Square peg w2 fits round hole r5.
Square peg w20 does not fit into round hole r5.

與其他模式的關係

  • 橋接模式通常會於開發前期進行設計, 使你能夠將程式的各個部分獨立開來以便開發。 另一方面, 適配器模式通常在已有程式中使用, 讓相互不兼容的類能很好地合作。
  • 適配器可以對已有對象的介面進行修改, 裝飾模式則能在不改變對象介面的前提下強化對象功能。 此外, 裝飾還支援遞歸組合, 適配器則無法實現。
  • 適配器能為被封裝對象提供不同的介面, 代理模式能為對象提供相同的介面, 裝飾則能為對象提供加強的介面。
  • 外觀模式為現有對象定義了一個新介面, 適配器則會試圖運用已有的介面。 適配器通常只封裝一個對象, 外觀通常會作用於整個對象子系統上。
  • 橋接狀態模式策略模式 (在某種程度上包括適配器) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。

Bridge

背景

橋接模式是一種結構型設計模式, 可將一個大類或一系列緊密相關的類拆分為抽象和實現兩個獨立的層次結構, 從而能在開發時分別使用。

適用場景

  • 如果你想要拆分或重組一個具有多重功能的龐雜類 (例如能與多個資料庫伺服器進行交互的類), 可以使用橋接模式。
  • 如果你希望在幾個獨立維度上擴展一個類, 可使用該模式。
  • 如果你需要在運行時切換不同實現方法, 可使用橋接模式。

實例-實現

使用示例: 橋接模式在處理跨平台應用、 支援多種類型的資料庫伺服器或與多個特定種類 (例如雲平台和社交網路等) 的 API 供應商協作時會特別有用。

識別方法: 橋接可以通過一些控制實體及其所依賴的多個不同平台之間的明確區別來進行識別。

設備和遠程控制之間的橋接

本例展示了遠程控制器及其所控制的設備的類之間的分離。

遠程控制器是抽象部分, 設備則是其實現部分。 由於有通用的介面, 同一遠程控制器可與不同的設備合作, 反過來也一樣。

橋接模式允許在不改動另一層次程式碼的前提下修改已有類, 甚至創建新類。

devices

devices/Device.java: 所有設備的通用介面

public interface Device {
    boolean isEnabled();

    void enable();

    void disable();

    int getVolume();

    void setVolume(int percent);

    int getChannel();

    void setChannel(int channel);

    void printStatus();
}

devices/Radio.java: 收音機

public class Radio implements Device {
    private boolean on = false;
    private int volume = 30;
    private int channel = 1;

    @Override
    public boolean isEnabled() {
        return on;
    }

    @Override
    public void enable() {
        on = true;
    }

    @Override
    public void disable() {
        on = false;
    }

    @Override
    public int getVolume() {
        return volume;
    }

    @Override
    public void setVolume(int volume) {
        if (volume > 100) {
            this.volume = 100;
        } else if (volume < 0) {
            this.volume = 0;
        } else {
            this.volume = volume;
        }
    }

    @Override
    public int getChannel() {
        return channel;
    }

    @Override
    public void setChannel(int channel) {
        this.channel = channel;
    }

    @Override
    public void printStatus() {
        System.out.println("------------------------------------");
        System.out.println("| I'm radio.");
        System.out.println("| I'm " + (on ? "enabled" : "disabled"));
        System.out.println("| Current volume is " + volume + "%");
        System.out.println("| Current channel is " + channel);
        System.out.println("------------------------------------\n");
    }
}

devices/Tv.java: 電視機

public class Tv implements Device {
    private boolean on = false;
    private int volume = 30;
    private int channel = 1;

    @Override
    public boolean isEnabled() {
        return on;
    }

    @Override
    public void enable() {
        on = true;
    }

    @Override
    public void disable() {
        on = false;
    }

    @Override
    public int getVolume() {
        return volume;
    }

    @Override
    public void setVolume(int volume) {
        if (volume > 100) {
            this.volume = 100;
        } else if (volume < 0) {
            this.volume = 0;
        } else {
            this.volume = volume;
        }
    }

    @Override
    public int getChannel() {
        return channel;
    }

    @Override
    public void setChannel(int channel) {
        this.channel = channel;
    }

    @Override
    public void printStatus() {
        System.out.println("------------------------------------");
        System.out.println("| I'm TV set.");
        System.out.println("| I'm " + (on ? "enabled" : "disabled"));
        System.out.println("| Current volume is " + volume + "%");
        System.out.println("| Current channel is " + channel);
        System.out.println("------------------------------------\n");
    }
}

remotes

remotes/Remote.java: 所有遠程控制器的通用介面

public interface Remote {
    void power();

    void volumeDown();

    void volumeUp();

    void channelDown();

    void channelUp();
}

remotes/BasicRemote.java: 基礎遠程控制器

public class BasicRemote implements Remote {
    protected Device device;

    public BasicRemote() {}

    public BasicRemote(Device device) {
        this.device = device;
    }

    @Override
    public void power() {
        System.out.println("Remote: power toggle");
        if (device.isEnabled()) {
            device.disable();
        } else {
            device.enable();
        }
    }

    @Override
    public void volumeDown() {
        System.out.println("Remote: volume down");
        device.setVolume(device.getVolume() - 10);
    }

    @Override
    public void volumeUp() {
        System.out.println("Remote: volume up");
        device.setVolume(device.getVolume() + 10);
    }

    @Override
    public void channelDown() {
        System.out.println("Remote: channel down");
        device.setChannel(device.getChannel() - 1);
    }

    @Override
    public void channelUp() {
        System.out.println("Remote: channel up");
        device.setChannel(device.getChannel() + 1);
    }
}

remotes/AdvancedRemote.java: 高級遠程控制器

public class AdvancedRemote extends BasicRemote {

    public AdvancedRemote(Device device) {
        super.device = device;
    }

    public void mute() {
        System.out.println("Remote: mute");
        device.setVolume(0);
    }
}

Demo.java: 客戶端程式碼

public class Demo {
    public static void main(String[] args) {
        testDevice(new Tv());
        testDevice(new Radio());
    }

    public static void testDevice(Device device) {
        System.out.println("Tests with basic remote.");
        BasicRemote basicRemote = new BasicRemote(device);
        basicRemote.power();
        device.printStatus();

        System.out.println("Tests with advanced remote.");
        AdvancedRemote advancedRemote = new AdvancedRemote(device);
        advancedRemote.power();
        advancedRemote.mute();
        device.printStatus();
    }
}

OutputDemo.txt: 執行結果

Tests with basic remote.
Remote: power toggle
------------------------------------
| I'm TV set.
| I'm enabled
| Current volume is 30%
| Current channel is 1
------------------------------------

Tests with advanced remote.
Remote: power toggle
Remote: mute
------------------------------------
| I'm TV set.
| I'm disabled
| Current volume is 0%
| Current channel is 1
------------------------------------

Tests with basic remote.
Remote: power toggle
------------------------------------
| I'm radio.
| I'm enabled
| Current volume is 30%
| Current channel is 1
------------------------------------

Tests with advanced remote.
Remote: power toggle
Remote: mute
------------------------------------
| I'm radio.
| I'm disabled
| Current volume is 0%
| Current channel is 1
------------------------------------

與其他模式的關係

  • 橋接模式通常會於開發前期進行設計, 使你能夠將程式的各個部分獨立開來以便開發。 另一方面, 適配器模式通常在已有程式中使用, 讓相互不兼容的類能很好地合作。
  • 橋接狀態模式策略模式 (在某種程度上包括適配器) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
  • 你可以將抽象工廠模式橋接搭配使用。 如果由橋接定義的抽象只能與特定實現合作, 這一模式搭配就非常有用。 在這種情況下, 抽象工廠可以對這些關係進行封裝, 並且對客戶端程式碼隱藏其複雜性。
  • 你可以結合使用生成器模式橋接模式主管類負責抽象工作, 各種不同的生成器負責實現工作。

Composite

組合模式是一種結構型設計模式, 你可以使用它將對象組合成樹狀結構, 並且能像使用獨立對象一樣使用它們。

背景

該方式的最大優點在於你無需了解構成樹狀結構的對象的具體類。 你也無需了解對象是簡單的產品還是複雜的盒子。 你只需調用通用介面以相同的方式對其進行處理即可。 當你調用該方法後, 對象會將請求沿著樹結構傳遞下去。

適用場景

  • 如果你需要實現樹狀對象結構, 可以使用組合模式。
  • 如果你希望客戶端程式碼以相同方式處理簡單和複雜元素, 可以使用該模式 。

實例-實現

使用實例: 組合模式在 Java 程式碼中很常見,常用於表示與圖形打交道的用戶介面組件或程式碼的層次結構。

下面是一些來自 Java 標準程式庫中的組合示例:

識別方法: 組合可以通過將同一抽象或介面類型的實例放入樹狀結構的行為方法來輕鬆識別。

簡單和複合圖形

本例展示了如何利用較為簡單的形狀來組成複雜圖形, 以及如何統一處理簡單和複雜圖形。

shapes

shapes/Shape.java: 通用形狀介面

import java.awt.*;

public interface Shape {
    int getX();
    int getY();
    int getWidth();
    int getHeight();
    void move(int x, int y);
    boolean isInsideBounds(int x, int y);
    void select();
    void unSelect();
    boolean isSelected();
    void paint(Graphics graphics);
}

shapes/BaseShape.java: 提供基本功能的抽象形狀

import java.awt.*;

abstract class BaseShape implements Shape {
    public int x;
    public int y;
    public Color color;
    private boolean selected = false;

    BaseShape(int x, int y, Color color) {
        this.x = x;
        this.y = y;
        this.color = color;
    }

    @Override
    public int getX() {
        return x;
    }

    @Override
    public int getY() {
        return y;
    }

    @Override
    public int getWidth() {
        return 0;
    }

    @Override
    public int getHeight() {
        return 0;
    }

    @Override
    public void move(int x, int y) {
        this.x += x;
        this.y += y;
    }

    @Override
    public boolean isInsideBounds(int x, int y) {
        return x > getX() && x < (getX() + getWidth()) &&
                y > getY() && y < (getY() + getHeight());
    }

    @Override
    public void select() {
        selected = true;
    }

    @Override
    public void unSelect() {
        selected = false;
    }

    @Override
    public boolean isSelected() {
        return selected;
    }

    void enableSelectionStyle(Graphics graphics) {
        graphics.setColor(Color.LIGHT_GRAY);

        Graphics2D g2 = (Graphics2D) graphics;
        float dash1[] = {2.0f};
        g2.setStroke(new BasicStroke(1.0f,
                BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_MITER,
                2.0f, dash1, 0.0f));
    }

    void disableSelectionStyle(Graphics graphics) {
        graphics.setColor(color);
        Graphics2D g2 = (Graphics2D) graphics;
        g2.setStroke(new BasicStroke());
    }


    @Override
    public void paint(Graphics graphics) {
        if (isSelected()) {
            enableSelectionStyle(graphics);
        }
        else {
            disableSelectionStyle(graphics);
        }

        // ...
    }
}

shapes/Dot.java:

import java.awt.*;

public class Dot extends BaseShape {
    private final int DOT_SIZE = 3;

    public Dot(int x, int y, Color color) {
        super(x, y, color);
    }

    @Override
    public int getWidth() {
        return DOT_SIZE;
    }

    @Override
    public int getHeight() {
        return DOT_SIZE;
    }

    @Override
    public void paint(Graphics graphics) {
        super.paint(graphics);
        graphics.fillRect(x - 1, y - 1, getWidth(), getHeight());
    }
}

shapes/Circle.java: 圓形

import java.awt.*;

public class Circle extends BaseShape {
    public int radius;

    public Circle(int x, int y, int radius, Color color) {
        super(x, y, color);
        this.radius = radius;
    }

    @Override
    public int getWidth() {
        return radius * 2;
    }

    @Override
    public int getHeight() {
        return radius * 2;
    }

    public void paint(Graphics graphics) {
        super.paint(graphics);
        graphics.drawOval(x, y, getWidth() - 1, getHeight() - 1);
    }
}

shapes/Rectangle.java: 三角形

import java.awt.*;

public class Rectangle extends BaseShape {
    public int width;
    public int height;

    public Rectangle(int x, int y, int width, int height, Color color) {
        super(x, y, color);
        this.width = width;
        this.height = height;
    }

    @Override
    public int getWidth() {
        return width;
    }

    @Override
    public int getHeight() {
        return height;
    }

    @Override
    public void paint(Graphics graphics) {
        super.paint(graphics);
        graphics.drawRect(x, y, getWidth() - 1, getHeight() - 1);
    }
}

shapes/CompoundShape.java: 由其他形狀對象組成的複合形狀

import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CompoundShape extends BaseShape {
    protected List<Shape> children = new ArrayList<>();

    public CompoundShape(Shape... components) {
        super(0, 0, Color.BLACK);
        add(components);
    }

    public void add(Shape component) {
        children.add(component);
    }

    public void add(Shape... components) {
        children.addAll(Arrays.asList(components));
    }

    public void remove(Shape child) {
        children.remove(child);
    }

    public void remove(Shape... components) {
        children.removeAll(Arrays.asList(components));
    }

    public void clear() {
        children.clear();
    }

    @Override
    public int getX() {
        if (children.size() == 0) {
            return 0;
        }
        int x = children.get(0).getX();
        for (Shape child : children) {
            if (child.getX() < x) {
                x = child.getX();
            }
        }
        return x;
    }

    @Override
    public int getY() {
        if (children.size() == 0) {
            return 0;
        }
        int y = children.get(0).getY();
        for (Shape child : children) {
            if (child.getY() < y) {
                y = child.getY();
            }
        }
        return y;
    }

    @Override
    public int getWidth() {
        int maxWidth = 0;
        int x = getX();
        for (Shape child : children) {
            int childsRelativeX = child.getX() - x;
            int childWidth = childsRelativeX + child.getWidth();
            if (childWidth > maxWidth) {
                maxWidth = childWidth;
            }
        }
        return maxWidth;
    }

    @Override
    public int getHeight() {
        int maxHeight = 0;
        int y = getY();
        for (Shape child : children) {
            int childsRelativeY = child.getY() - y;
            int childHeight = childsRelativeY + child.getHeight();
            if (childHeight > maxHeight) {
                maxHeight = childHeight;
            }
        }
        return maxHeight;
    }

    @Override
    public void move(int x, int y) {
        for (Shape child : children) {
            child.move(x, y);
        }
    }

    @Override
    public boolean isInsideBounds(int x, int y) {
        for (Shape child : children) {
            if (child.isInsideBounds(x, y)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void unSelect() {
        super.unSelect();
        for (Shape child : children) {
            child.unSelect();
        }
    }

    public boolean selectChildAt(int x, int y) {
        for (Shape child : children) {
            if (child.isInsideBounds(x, y)) {
                child.select();
                return true;
            }
        }
        return false;
    }

    @Override
    public void paint(Graphics graphics) {
        if (isSelected()) {
            enableSelectionStyle(graphics);
            graphics.drawRect(getX() - 1, getY() - 1, getWidth() + 1, getHeight() + 1);
            disableSelectionStyle(graphics);
        }

        for (refactoring_guru.composite.example.shapes.Shape child : children) {
            child.paint(graphics);
        }
    }
}

editor

editor/ImageEditor.java: 形狀編輯器

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ImageEditor {
    private EditorCanvas canvas;
    private CompoundShape allShapes = new CompoundShape();

    public ImageEditor() {
        canvas = new EditorCanvas();
    }

    public void loadShapes(Shape... shapes) {
        allShapes.clear();
        allShapes.add(shapes);
        canvas.refresh();
    }

    private class EditorCanvas extends Canvas {
        JFrame frame;

        private static final int PADDING = 10;

        EditorCanvas() {
            createFrame();
            refresh();
            addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    allShapes.unSelect();
                    allShapes.selectChildAt(e.getX(), e.getY());
                    e.getComponent().repaint();
                }
            });
        }

        void createFrame() {
            frame = new JFrame();
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.setLocationRelativeTo(null);

            JPanel contentPanel = new JPanel();
            Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);
            contentPanel.setBorder(padding);
            frame.setContentPane(contentPanel);

            frame.add(this);
            frame.setVisible(true);
            frame.getContentPane().setBackground(Color.LIGHT_GRAY);
        }

        public int getWidth() {
            return allShapes.getX() + allShapes.getWidth() + PADDING;
        }

        public int getHeight() {
            return allShapes.getY() + allShapes.getHeight() + PADDING;
        }

        void refresh() {
            this.setSize(getWidth(), getHeight());
            frame.pack();
        }

        public void paint(Graphics graphics) {
            allShapes.paint(graphics);
        }
    }
}

Demo.java: 客戶端程式碼

import java.awt.*;

public class Demo {
    public static void main(String[] args) {
        ImageEditor editor = new ImageEditor();

        editor.loadShapes(
                new Circle(10, 10, 10, Color.BLUE),

                new CompoundShape(
                    new Circle(110, 110, 50, Color.RED),
                    new Dot(160, 160, Color.RED)
                ),

                new CompoundShape(
                        new Rectangle(250, 250, 100, 100, Color.GREEN),
                        new Dot(240, 240, Color.GREEN),
                        new Dot(240, 360, Color.GREEN),
                        new Dot(360, 360, Color.GREEN),
                        new Dot(360, 240, Color.GREEN)
                )
        );
    }
}

與其他模式的關係

  • 橋接模式狀態模式策略模式 (在某種程度上包括適配器模式) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。

  • 你可以在創建複雜組合樹時使用生成器模式, 因為這可使其構造步驟以遞歸的方式運行。

  • 責任鏈模式通常和組合模式結合使用。 在這種情況下, 葉組件接收到請求後, 可以將請求沿包含全體父組件的鏈一直傳遞至對象樹的底部。

  • 你可以使用迭代器模式來遍歷組合樹。

  • 你可以使用訪問者模式對整個組合樹執行操作。

  • 你可以使用享元模式實現組合樹的共享葉節點以節省記憶體。

  • 組合裝飾模式的結構圖很相似, 因為兩者都依賴遞歸組合來組織無限數量的對象。

    裝飾類似於組合, 但其只有一個子組件。 此外還有一個明顯不同: 裝飾為被封裝對象添加了額外的職責, 組合僅對其子節點的結果進行了 「求和」。

    但是, 模式也可以相互合作: 你可以使用裝飾來擴展組合樹中特定對象的行為。

  • 大量使用組合裝飾的設計通常可從對於原型模式的使用中獲益。 你可以通過該模式來複制複雜結構, 而非從零開始重新構造。

Decorator

裝飾模式是一種結構型設計模式, 允許你通過將對象放入包含行為的特殊封裝對象中來為原對象綁定新的行為。

背景

適用場景

如果你希望在無需修改程式碼的情況下即可使用對象, 且希望在運行時為對象新增額外的行為, 可以使用裝飾模式。

  • 裝飾能將業務邏輯組織為層次結構, 你可為各層創建一個裝飾, 在運行時將各種不同邏輯組合成對象。 由於這些對象都遵循通用介面, 客戶端程式碼能以相同的方式使用這些對象。

如果用繼承來擴展對象行為的方案難以實現或者根本不可行, 你可以使用該模式。

  • 許多程式語言使用 final最終關鍵字來限制對某個類的進一步擴展。 復用最終類已有行為的唯一方法是使用裝飾模式: 用封裝器對其進行封裝。

實例-實現

使用示例: 裝飾在 Java 程式碼中可謂是標準配置, 尤其是在與流式載入相關的程式碼中。

Java 核心程式庫中有一些關於裝飾的示例:

識別方法: 裝飾可通過以當前類或對象為參數的創建方法或構造函數來識別。

編碼和壓縮裝飾

本例展示了如何在不更改對象程式碼的情況下調整其行為。

最初的業務邏輯類僅能讀取和寫入純文本的數據。 此後, 我們創建了幾個小的封裝器類, 以便在執行標準操作後添加新的行為。

第一個封裝器負責加密和解密數據, 而第二個則負責壓縮和解壓數據。

你甚至可以讓這些封裝器嵌套封裝以將它們組合起來。

decorators

decorators/DataSource.java: 定義了讀取和寫入操作的通用數據介面

public interface DataSource {
    void writeData(String data);

    String readData();
}

decorators/FileDataSource.java: 簡單數據讀寫器

import java.io.*;

public class FileDataSource implements DataSource {
    private String name;

    public FileDataSource(String name) {
        this.name = name;
    }

    @Override
    public void writeData(String data) {
        File file = new File(name);
        try (OutputStream fos = new FileOutputStream(file)) {
            fos.write(data.getBytes(), 0, data.length());
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        }
    }

    @Override
    public String readData() {
        char[] buffer = null;
        File file = new File(name);
        try (FileReader reader = new FileReader(file)) {
            buffer = new char[(int) file.length()];
            reader.read(buffer);
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        }
        return new String(buffer);
    }
}

decorators/DataSourceDecorator.java: 抽象基礎裝飾

public class DataSourceDecorator implements DataSource {
    private DataSource wrappee;

    DataSourceDecorator(DataSource source) {
        this.wrappee = source;
    }

    @Override
    public void writeData(String data) {
        wrappee.writeData(data);
    }

    @Override
    public String readData() {
        return wrappee.readData();
    }
}

decorators/EncryptionDecorator.java: 加密裝飾

import java.util.Base64;

public class EncryptionDecorator extends DataSourceDecorator {

    public EncryptionDecorator(DataSource source) {
        super(source);
    }

    @Override
    public void writeData(String data) {
        super.writeData(encode(data));
    }

    @Override
    public String readData() {
        return decode(super.readData());
    }

    private String encode(String data) {
        byte[] result = data.getBytes();
        for (int i = 0; i < result.length; i++) {
            result[i] += (byte) 1;
        }
        return Base64.getEncoder().encodeToString(result);
    }

    private String decode(String data) {
        byte[] result = Base64.getDecoder().decode(data);
        for (int i = 0; i < result.length; i++) {
            result[i] -= (byte) 1;
        }
        return new String(result);
    }
}

decorators/CompressionDecorator.java: 壓縮裝飾

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

public class CompressionDecorator extends DataSourceDecorator {
    private int compLevel = 6;

    public CompressionDecorator(DataSource source) {
        super(source);
    }

    public int getCompressionLevel() {
        return compLevel;
    }

    public void setCompressionLevel(int value) {
        compLevel = value;
    }

    @Override
    public void writeData(String data) {
        super.writeData(compress(data));
    }

    @Override
    public String readData() {
        return decompress(super.readData());
    }

    private String compress(String stringData) {
        byte[] data = stringData.getBytes();
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
            DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater(compLevel));
            dos.write(data);
            dos.close();
            bout.close();
            return Base64.getEncoder().encodeToString(bout.toByteArray());
        } catch (IOException ex) {
            return null;
        }
    }

    private String decompress(String stringData) {
        byte[] data = Base64.getDecoder().decode(stringData);
        try {
            InputStream in = new ByteArrayInputStream(data);
            InflaterInputStream iin = new InflaterInputStream(in);
            ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
            int b;
            while ((b = iin.read()) != -1) {
                bout.write(b);
            }
            in.close();
            iin.close();
            bout.close();
            return new String(bout.toByteArray());
        } catch (IOException ex) {
            return null;
        }
    }
}

Demo.java: 客戶端程式碼

package refactoring_guru.decorator.example;

import refactoring_guru.decorator.example.decorators.*;

public class Demo {
    public static void main(String[] args) {
        String salaryRecords = "Name,Salary\nJohn Smith,100000\nSteven Jobs,912000";
        DataSourceDecorator encoded = new CompressionDecorator(
                                         new EncryptionDecorator(
                                             new FileDataSource("out/OutputDemo.txt")));
        encoded.writeData(salaryRecords);
        DataSource plain = new FileDataSource("out/OutputDemo.txt");

        System.out.println("- Input ----------------");
        System.out.println(salaryRecords);
        System.out.println("- Encoded --------------");
        System.out.println(plain.readData());
        System.out.println("- Decoded --------------");
        System.out.println(encoded.readData());
    }
}

與其他模式的關係

  • 適配器模式可以對已有對象的介面進行修改, 裝飾模式則能在不改變對象介面的前提下強化對象功能。 此外, 裝飾 還支援遞歸組合, 適配器 則無法實現。

  • 適配器能為被封裝對象提供不同的介面, 代理模式能為對象提供相同的介面, 裝飾則能為對象提供加強的介面。

  • 責任鏈模式裝飾模式的類結構非常相似。 兩者都依賴遞歸組合將需要執行的操作傳遞給一系列對象。 但是, 兩者有幾點重要的不同之處。

    責任鏈的管理者可以相互獨立地執行一切操作, 還可以隨時停止傳遞請求。 另一方面, 各種裝飾 可以在遵循基本介面的情況下擴展對象的行為。 此外, 裝飾無法中斷請求的傳遞。

  • 組合模式裝飾的結構圖很相似, 因為兩者都依賴遞歸組合來組織無限數量的對象。

    裝飾類似於組合, 但其只有一個子組件。 此外還有一個明顯不同: 裝飾為被封裝對象添加了額外的職責, 組合僅對其子節點的結果進行了 「求和」。

    但是, 模式也可以相互合作: 你可以使用裝飾來擴展組合樹中特定對象的行為。

  • 大量使用組合裝飾的設計通常可從對於原型模式的使用中獲益。 你可以通過該模式來複制複雜結構, 而非從零開始重新構造。

  • 裝飾可讓你更改對象的外表, 策略模式則讓你能夠改變其本質。

  • 裝飾代理有著相似的結構, 但是其意圖卻非常不同。 這兩個模式的構建都基於組合原則, 也就是說一個對象應該將部分工作委派給另一個對象。 兩者之間的不同之處在於代理 通常自行管理其服務對象的生命周期, 而裝飾 的生成則總是由客戶端進行控制。

Facade

背景

外觀模式是一種結構型設計模式, 能為程式庫、 框架或其他複雜類提供一個簡單的介面。

問題:

假設你必須在程式碼中使用某個複雜的庫或框架中的眾多對象。 正常情況下, 你需要負責所有對象的初始化工作、 管理其依賴關係並按正確的順序執行方法等。

最終, 程式中類的業務邏輯將與第三方類的實現細節緊密耦合, 使得理解和維護程式碼的工作很難進行。

解決:

外觀類為包含許多活動部件的複雜子系統提供一個簡單的介面。 與直接調用子系統相比, 外觀提供的功能可能比較有限, 但它卻包含了客戶端真正關心的功能。

如果你的程式需要與包含幾十種功能的複雜庫整合, 但只需使用其中非常少的功能, 那麼使用外觀模式會非常方便,

例如, 上傳貓咪搞笑短影片到社交媒體網站的應用可能會用到專業的影片轉換庫, 但它只需使用一個包含 encode­(filename, format)方法 (以文件名與文件格式為參數進行編碼的方法) 的類即可。 在創建這個類並將其連接到影片轉換庫後, 你就擁有了自己的第一個外觀。

與真實世界的類比:

你通過手機網購時, 該商店的所有服務和部門的外觀。 程式設計師為你提供了一個同購物系統、 支付網關和各種送貨服務進行互動的簡單語音介面。

使用場景

如果你需要一個指向複雜子系統的直接介面, 且該介面的功能有限, 則可以使用外觀模式。

  • 子系統通常會隨著時間的推進變得越來越複雜。 即便是應用了設計模式, 通常你也會創建更多的類。 儘管在多種情形中子系統可能是更靈活或易於復用的, 但其所需的配置和樣板程式碼數量將會增長得更快。 為了解決這個問題, 外觀將會提供指向子系統中最常用功能的快捷方式, 能夠滿足客戶端的大部分需求。

如果需要將子系統組織為多層結構, 可以使用外觀。

  • 創建外觀來定義子系統中各層次的入口。 你可以要求子系統僅使用外觀來進行交互, 以減少子系統之間的耦合。

讓我們回到影片轉換框架的例子。 該框架可以拆分為兩個層次: 音頻相關和影片相關。 你可以為每個層次創建一個外觀, 然後要求各層的類必須通過這些外觀進行交互。 這種方式看上去與中介者模式非常相似。

實例-實現

下面是一些核心 Java 程式庫中的外觀示例:

識別方法: 外觀可以通過使用簡單介面, 但將絕大部分工作委派給其他類的類來識別。 通常情況下, 外觀管理著其所使用的對象的完整生命周期。

複雜影片轉換庫的簡單介面

some_complex_media_library/VideoFile.java

public class VideoFile {
    private String name;
    private String codecType;

    public VideoFile(String name) {
        this.name = name;
        this.codecType = name.substring(name.indexOf(".") + 1);
    }

    public String getCodecType() {
        return codecType;
    }

    public String getName() {
        return name;
    }
}

some_complex_media_library/Codec.java

public interface Codec {
}

some_complex_media_library/MPEG4CompressionCodec.java

public class MPEG4CompressionCodec implements Codec {
    public String type = "mp4";

}

some_complex_media_library/OggCompressionCodec.java

public class OggCompressionCodec implements Codec {
    public String type = "ogg";
}

some_complex_media_library/CodecFactory.java

public class CodecFactory {
    public static Codec extract(VideoFile file) {
        String type = file.getCodecType();
        if (type.equals("mp4")) {
            System.out.println("CodecFactory: extracting mpeg audio...");
            return new MPEG4CompressionCodec();
        }
        else {
            System.out.println("CodecFactory: extracting ogg audio...");
            return new OggCompressionCodec();
        }
    }
}

some_complex_media_library/BitrateReader.java

public class BitrateReader {
    public static VideoFile read(VideoFile file, Codec codec) {
        System.out.println("BitrateReader: reading file...");
        return file;
    }

    public static VideoFile convert(VideoFile buffer, Codec codec) {
        System.out.println("BitrateReader: writing file...");
        return buffer;
    }
}

some_complex_media_library/AudioMixer.java

import java.io.File;

public class AudioMixer {
    public File fix(VideoFile result){
        System.out.println("AudioMixer: fixing audio...");
        return new File("tmp");
    }
}

facade

facade/VideoConversionFacade.java: 外觀提供了進行影片轉換的簡單介面

import java.io.File;

public class VideoConversionFacade {
    public File convertVideo(String fileName, String format) {
        System.out.println("VideoConversionFacade: conversion started.");
        VideoFile file = new VideoFile(fileName);
        Codec sourceCodec = CodecFactory.extract(file);
        Codec destinationCodec;
        if (format.equals("mp4")) {
            destinationCodec = new OggCompressionCodec();
        } else {
            destinationCodec = new MPEG4CompressionCodec();
        }
        VideoFile buffer = BitrateReader.read(file, sourceCodec);
        VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);
        File result = (new AudioMixer()).fix(intermediateResult);
        System.out.println("VideoConversionFacade: conversion completed.");
        return result;
    }
}

Demo.java: 客戶端程式碼

import java.io.File;

public class Demo {
    public static void main(String[] args) {
        VideoConversionFacade converter = new VideoConversionFacade();
        File mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4");
        // ...
    }
}

與其他模式的關係

  • 外觀模式為現有對象定義了一個新介面, 適配器模式則會試圖運用已有的介面。 適配器 通常只封裝一個對象, 外觀 通常會作用於整個對象子系統上。
  • 當只需對客戶端程式碼隱藏子系統創建對象的方式時, 你可以使用抽象工廠模式來代替外觀
  • 享元模式展示了如何生成大量的小型對象, 外觀則展示了如何用一個對象來代表整個子系統。
  • 外觀中介者模式的職責類似: 它們都嘗試在大量緊密耦合的類中組織起合作。
    • 外觀 為子系統中的所有對象定義了一個簡單介面, 但是它不提供任何新功能。 子系統本身不會意識到外觀的存在。 子系統中的對象可以直接進行交流。
    • 中介者 將系統中組件的溝通行為中心化。 各組件只知道中介者對象, 無法直接相互交流。
  • 外觀類通常可以轉換為單例模式類, 因為在大部分情況下一個外觀對象就足夠了。
  • 外觀代理模式的相似之處在於它們都快取了一個複雜實體並自行對其進行初始化。 代理 與其服務對象遵循同一介面, 使得自己和服務對象可以互換, 在這一點上它與外觀 不同。

Flyweight

背景

享元模式是一種結構型設計模式, 它摒棄了在每個對象中保存所有數據的方式, 通過共享多個對象所共有的相同狀態, 讓你能在有限的記憶體容量中載入更多對象。

享元模式通過共享多個對象的部分狀態來實現上述功能。 換句話來說, 享元會將不同對象的相同數據進行快取以節省記憶體。

問題

開發了一款簡單的遊戲: 玩家們在地圖上移動並相互射擊。 你決定實現一個真實的粒子系統, 並將其作為遊戲的特色。 大量的子彈、 導彈和爆炸彈片會在整個地圖上穿行, 為玩家提供緊張刺激的遊戲體驗。

遊戲總是會在他的電腦上運行幾分鐘後崩潰。 在研究了幾個小時的調試消息記錄後, 你發現導致遊戲崩潰的原因是記憶體容量不足。 真正的問題與粒子系統有關。 每個粒子 (一顆子彈、 一枚導彈或一塊彈片) 都由包含完整數據的獨立對象來表示。 當玩家在遊戲中鏖戰進入高潮後的某一時刻, 遊戲將無法在剩餘記憶體中載入新建粒子, 於是程式就崩潰了。

解決方案

每個粒子的只有一些狀態,例如: (坐標、 移動矢量和速度) 是不同的。

對象的常量數據通常被稱為內在狀態, 其位於對象中, 其他對象只能讀取但不能修改其數值。 而對象的其他狀態常常能被其他對象 「從外部」 改變, 因此被稱為外在狀態

享元模式建議不在對象中存儲外在狀態, 而是將其傳遞給依賴於它的一個特殊方法。 程式只在對象中保存內在狀態, 以方便在不同情景下重用。 這些對象的區別僅在於其內在狀態 (與外在狀態相比, 內在狀態的變體要少很多), 因此你所需的對象數量會大大削減。

外在狀態存儲

為了能將外在狀態移動到這個類中, 你需要創建多個數組成員變數來存儲每個粒子的坐標、 方向矢量和速度。 除此之外, 你還需要另一個數組來存儲指向代表粒子的特定享元的引用。 這些數組必須保持同步, 這樣你才能夠使用同一索引來獲取關於某個粒子的所有數據。

更優雅的解決方案是創建獨立的情景類來存儲外在狀態和對享元對象的引用。 在該方法中, 容器類只需包含一個數組。

這樣的話情景對象數量不是會和不採用該模式時的對象數量一樣多嗎? 的確如此, 但這些對象要比之前小很多。 消耗記憶體最多的成員變數已經被移動到很少的幾個享元對象中了。 現在, 一個享元大對象會被上千個情境小對象復用, 因此無需再重複存儲數千個大對象的數據。

享元與不可變性

由於享元對象可在不同的情景中使用, 你必須確保其狀態不能被修改。 享元類的狀態只能由構造函數的參數進行一次性初始化, 它不能對其他對象公開其設置器或公有成員變數。

享元工廠

為了能更方便地訪問各種享元, 你可以創建一個工廠方法來管理已有享元對象的快取池。 工廠方法從客戶端處接收目標享元對象的內在狀態作為參數, 如果它能在快取池中找到所需享元, 則將其返回給客戶端; 如果沒有找到, 它就會新建一個享元, 並將其添加到快取池中。

你可以選擇在程式的不同地方放入該函數。 最簡單的選擇就是將其放置在享元容器中。 除此之外, 你還可以新建一個工廠類, 或者創建一個靜態的工廠方法並將其放入實際的享元類中。

使用場景

僅在程式必須支援大量對象且沒有足夠的記憶體容量時使用享元模式。

應用該模式所獲的收益大小取決於使用它的方式和情景。 它在下列情況中最有效:

  • 程式需要生成數量巨大的相似對象
  • 這將耗盡目標設備的所有記憶體
  • 對象中包含可抽取且能在多個對象間共享的重複狀態。

實例-實現

享元模式在核心 Java 程式庫中的示例:

識別方法: 享元可以通過構建方法來識別, 它會返回快取對象而不是創建新的對象。

渲染一片森林

本例中, 我們將渲染一片森林 (1,000,000 棵樹)! 每棵樹都由包含一些狀態的對象來表示 (坐標和紋理等)。 儘管程式能夠完成其主要工作, 但很顯然它需要消耗大量記憶體。

原因很簡單: 太多樹對象包含重複數據 (名稱、 紋理和顏色)。 因此我們可用享元模式來將這些數值存儲在單獨的享元對象中 ( Tree­Type類)。 現在我們不再將相同數據存儲在數千個 Tree對象中, 而是使用一組特殊的數值來引用其中一個享元對象。

客戶端程式碼不會知道任何事情, 因為重用享元對象的複雜機制隱藏在了享元工廠中。

trees

trees/Tree.java: 包含每棵樹的獨特狀態

import java.awt.*;

public class Tree {
    private int x;
    private int y;
    private TreeType type;

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void draw(Graphics g) {
        type.draw(g, x, y);
    }
}

trees/TreeType.java: 包含多棵樹共享的狀態

import java.awt.*;

public class TreeType {
    private String name;
    private Color color;
    private String otherTreeData;

    public TreeType(String name, Color color, String otherTreeData) {
        this.name = name;
        this.color = color;
        this.otherTreeData = otherTreeData;
    }

    public void draw(Graphics g, int x, int y) {
        g.setColor(Color.BLACK);
        g.fillRect(x - 1, y, 3, 5);
        g.setColor(color);
        g.fillOval(x - 5, y - 10, 10, 10);
    }
}

trees/TreeFactory.java: 封裝創建享元的複雜機制

import java.awt.*;
import java.util.HashMap;
import java.util.Map;

public class TreeFactory {
    static Map<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, Color color, String otherTreeData) {
        TreeType result = treeTypes.get(name);
        if (result == null) {
            result = new TreeType(name, color, otherTreeData);
            treeTypes.put(name, result);
        }
        return result;
    }
}

forest

forest/Forest.java: 我們繪製的森林

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class Forest extends JFrame {
    private List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, Color color, String otherTreeData) {
        TreeType type = TreeFactory.getTreeType(name, color, otherTreeData);
        Tree tree = new Tree(x, y, type);
        trees.add(tree);
    }

    @Override
    public void paint(Graphics graphics) {
        for (Tree tree : trees) {
            tree.draw(graphics);
        }
    }
}

Demo.java: 客戶端程式碼

import refactoring_guru.flyweight.example.forest.Forest;

import java.awt.*;

public class Demo {
    static int CANVAS_SIZE = 500;
    static int TREES_TO_DRAW = 1000000;
    static int TREE_TYPES = 2;

    public static void main(String[] args) {
        Forest forest = new Forest();
        for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) {
            forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
                    "Summer Oak", Color.GREEN, "Oak texture stub");
            forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
                    "Autumn Oak", Color.ORANGE, "Autumn Oak texture stub");
        }
        forest.setSize(CANVAS_SIZE, CANVAS_SIZE);
        forest.setVisible(true);

        System.out.println(TREES_TO_DRAW + " trees drawn");
        System.out.println("---------------------");
        System.out.println("Memory usage:");
        System.out.println("Tree size (8 bytes) * " + TREES_TO_DRAW);
        System.out.println("+ TreeTypes size (~30 bytes) * " + TREE_TYPES + "");
        System.out.println("---------------------");
        System.out.println("Total: " + ((TREES_TO_DRAW * 8 + TREE_TYPES * 30) / 1024 / 1024) +
                "MB (instead of " + ((TREES_TO_DRAW * 38) / 1024 / 1024) + "MB)");
    }

    private static int random(int min, int max) {
        return min + (int) (Math.random() * ((max - min) + 1));
    }
}

與其他模式的關係

  • 你可以使用享元模式實現組合模式樹的共享葉節點以節省記憶體。
  • 享元展示了如何生成大量的小型對象, 外觀模式則展示了如何用一個對象來代表整個子系統。
  • 如果你能將對象的所有共享狀態簡化為一個享元對象, 那麼享元就和單例模式類似了。 但這兩個模式有兩個根本性的不同。
    1. 只會有一個單例實體, 但是享元類可以有多個實體, 各實體的內在狀態也可以不同。
    2. 單例 對象可以是可變的。 享元對象是不可變的。

Proxy

背景

代理模式是一種結構型設計模式, 讓你能夠提供對象的替代品或其佔位符。 代理控制著對於原對象的訪問, 並允許在將請求提交給對象前後進行一些處理。

代理是一種結構型設計模式, 讓你能提供真實服務對象的替代品給客戶端使用。 代理接收客戶端的請求並進行一些處理 (訪問控制和快取等), 然後再將請求傳遞給服務對象。

代理對象擁有和服務對象相同的介面, 這使得當其被傳遞給客戶端時可與真實對象互換。

於真實世界類比

信用卡是銀行賬戶的代理, 銀行賬戶則是一大捆現金的代理。

它們都實現了同樣的介面, 均可用於進行支付。 消費者會非常滿意, 因為不必隨身攜帶大量現金; 商店老闆同樣會十分高興, 因為交易收入能以電子化的方式進入商店的銀行賬戶中, 無需擔心存款時出現現金丟失或被搶劫的情況。

使用場景

延遲初始化 (虛擬代理)。 如果你有一個偶爾使用的重量級服務對象, 一直保持該對象運行會消耗系統資源時, 可使用代理模式。

  • 你無需在程式啟動時就創建該對象, 可將對象的初始化延遲到真正有需要的時候。

訪問控制 (保護代理)。 如果你只希望特定客戶端使用服務對象, 這裡的對象可以是作業系統中非常重要的部分, 而客戶端則是各種已啟動的程式 (包括惡意程式), 此時可使用代理模式。

  • 代理可僅在客戶端憑據滿足要求時將請求傳遞給服務對象。

本地執行遠程服務 (遠程代理)。 適用於服務對象位於遠程伺服器上的情形。

  • 這種情形中, 代理通過網路傳遞客戶端請求, 負責處理所有與網路相關的複雜細節。

記錄日誌請求 (日誌記錄代理)。 適用於當你需要保存對於服務對象的請求歷史記錄時。 代理可以在向服務傳遞請求前進行記錄。

快取請求結果 (快取代理)。 適用於需要快取客戶請求結果並對快取生命周期進行管理時, 特別是當返回結果的體積非常大時。

  • 代理可對重複請求所需的相同結果進行快取, 還可使用請求參數作為索引快取的鍵值。

實例-實現

使用示例: 儘管代理模式在絕大多數 Java 程式中並不常見, 但它在一些特殊情況下仍然非常方便。 當你希望在無需修改客戶程式碼的前提下於已有類的對象上增加額外行為時, 該模式是無可替代的。

Java 標準程式庫中的一些代理模式的示例:

識別方法: 代理模式會將所有實際工作委派給一些其他對象。 除非代理是某個服務的子類, 否則每個代理方法最後都應該引用一個服務對象。

快取代理

在本例中, 代理模式有助於實現延遲初始化, 並對低效的第三方 YouTube 集成程式庫進行快取。

當你需要在無法修改程式碼的類上新增一些額外行為時, 代理模式的價值無可估量。

some_cool_media_library

some_cool_media_library/ThirdPartyYouTubeLib.java: 遠程服務介面

import java.util.HashMap;

public interface ThirdPartyYouTubeLib {
    HashMap<String, Video> popularVideos();

    Video getVideo(String videoId);
}

some_cool_media_library/ThirdPartyYouTubeClass.java: 遠程服務實現

import java.util.HashMap;

public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {

    @Override
    public HashMap<String, Video> popularVideos() {
        connectToServer("//www.youtube.com");
        return getRandomVideos();
    }

    @Override
    public Video getVideo(String videoId) {
        connectToServer("//www.youtube.com/" + videoId);
        return getSomeVideo(videoId);
    }

    // -----------------------------------------------------------------------
    // Fake methods to simulate network activity. They as slow as a real life.

    private int random(int min, int max) {
        return min + (int) (Math.random() * ((max - min) + 1));
    }

    private void experienceNetworkLatency() {
        int randomLatency = random(5, 10);
        for (int i = 0; i < randomLatency; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    private void connectToServer(String server) {
        System.out.print("Connecting to " + server + "... ");
        experienceNetworkLatency();
        System.out.print("Connected!" + "\n");
    }

    private HashMap<String, Video> getRandomVideos() {
        System.out.print("Downloading populars... ");

        experienceNetworkLatency();
        HashMap<String, Video> hmap = new HashMap<String, Video>();
        hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
        hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
        hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
        hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
        hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));

        System.out.print("Done!" + "\n");
        return hmap;
    }

    private Video getSomeVideo(String videoId) {
        System.out.print("Downloading video... ");

        experienceNetworkLatency();
        Video video = new Video(videoId, "Some video title");

        System.out.print("Done!" + "\n");
        return video;
    }

}

some_cool_media_library/Video.java: 影片文件

public class Video {
    public String id;
    public String title;
    public String data;

    Video(String id, String title) {
        this.id = id;
        this.title = title;
        this.data = "Random video.";
    }
}

proxy

proxy/YouTubeCacheProxy.java: 快取代理

import java.util.HashMap;

public class YouTubeCacheProxy implements ThirdPartyYouTubeLib {
    private ThirdPartyYouTubeLib youtubeService;
    private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
    private HashMap<String, Video> cacheAll = new HashMap<String, Video>();

    public YouTubeCacheProxy() {
        this.youtubeService = new ThirdPartyYouTubeClass();
    }

    @Override
    public HashMap<String, Video> popularVideos() {
        if (cachePopular.isEmpty()) {
            cachePopular = youtubeService.popularVideos();
        } else {
            System.out.println("Retrieved list from cache.");
        }
        return cachePopular;
    }

    @Override
    public Video getVideo(String videoId) {
        Video video = cacheAll.get(videoId);
        if (video == null) {
            video = youtubeService.getVideo(videoId);
            cacheAll.put(videoId, video);
        } else {
            System.out.println("Retrieved video '" + videoId + "' from cache.");
        }
        return video;
    }

    public void reset() {
        cachePopular.clear();
        cacheAll.clear();
    }
}

downloader

downloader/YouTubeDownloader.java: 媒體下載應用

import java.util.HashMap;

public class YouTubeDownloader {
    private ThirdPartyYouTubeLib api;

    public YouTubeDownloader(ThirdPartyYouTubeLib api) {
        this.api = api;
    }

    public void renderVideoPage(String videoId) {
        Video video = api.getVideo(videoId);
        System.out.println("\n-------------------------------");
        System.out.println("Video page (imagine fancy HTML)");
        System.out.println("ID: " + video.id);
        System.out.println("Title: " + video.title);
        System.out.println("Video: " + video.data);
        System.out.println("-------------------------------\n");
    }

    public void renderPopularVideos() {
        HashMap<String, Video> list = api.popularVideos();
        System.out.println("\n-------------------------------");
        System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
        for (Video video : list.values()) {
            System.out.println("ID: " + video.id + " / Title: " + video.title);
        }
        System.out.println("-------------------------------\n");
    }
}

Demo.java: 初始化程式碼

public class Demo {

    public static void main(String[] args) {
        YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
        YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());

        long naive = test(naiveDownloader);
        long smart = test(smartDownloader);
        System.out.print("Time saved by caching proxy: " + (naive - smart) + "ms");

    }

    private static long test(YouTubeDownloader downloader) {
        long startTime = System.currentTimeMillis();

        // User behavior in our app:
        downloader.renderPopularVideos();
        downloader.renderVideoPage("catzzzzzzzzz");
        downloader.renderPopularVideos();
        downloader.renderVideoPage("dancesvideoo");
        // Users might visit the same page quite often.
        downloader.renderVideoPage("catzzzzzzzzz");
        downloader.renderVideoPage("someothervid");

        long estimatedTime = System.currentTimeMillis() - startTime;
        System.out.print("Time elapsed: " + estimatedTime + "ms\n");
        return estimatedTime;
    }
}

與其他模式的關係

  • 適配器模式能為被封裝對象提供不同的介面, 代理模式能為對象提供相同的介面, 裝飾模式則能為對象提供加強的介面。
  • 外觀模式代理的相似之處在於它們都快取了一個複雜實體並自行對其進行初始化。 代理 與其服務對象遵循同一介面, 使得自己和服務對象可以互換, 在這一點上它與外觀 不同。
  • 裝飾代理有著相似的結構, 但是其意圖卻非常不同。 這兩個模式的構建都基於組合原則, 也就是說一個對象應該將部分工作委派給另一個對象。 兩者之間的不同之處在於代理通常自行管理其服務對象的生命周期, 而裝飾的生成則總是由客戶端進行控制。

BehavioralPattern

Chain of Responsibility

背景

責任鏈模式是一種行為設計模式, 允許你將請求沿著處理者鏈進行發送。 收到請求後, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。

該模式允許多個對象來對請求進行處理, 而無需讓發送者類與具體接收者類相耦合。 鏈可在運行時由遵循標準處理者介面的任意處理者動態生成。

使用場景

當程式需要使用不同方式處理不同種類請求, 而且請求類型和順序預先未知時, 可以使用責任鏈模式。

  • 該模式能將多個處理者連接成一條鏈。 接收到請求後, 它會 「詢問」 每個處理者是否能夠對其進行處理。 這樣所有處理者都有機會來處理請求。

當必須按順序執行多個處理者時, 可以使用該模式。

  • 論你以何種順序將處理者連接成一條鏈, 所有請求都會嚴格按照順序通過鏈上的處理者。

如果所需處理者及其順序必須在運行時進行改變, 可以使用責任鏈模式。

  • 如果在處理者類中有對引用成員變數的設定方法, 你將能動態地插入和移除處理者, 或者改變其順序。

實例-實現

使用示例: 責任鏈模式在 Java 程式中並不常見, 因為它僅在程式碼與對象鏈打交道時才能發揮作用。

該模式最流行的使用案例之一是在 GUI 類中將事件向上傳遞給父組件。 另一個值得注意的使用案例是依次訪問過濾器。

下面是該模式在核心 Java 程式庫中的一些示例:

識別方法: 該模式可通過一組對象的行為方法間接調用其他對象的相同方法來識別, 而且所有對象都會遵循相同的介面。

過濾訪問

middleware

middleware/Middleware.java: 基礎驗證介面

/**
 * Base middleware class.
 */
public abstract class Middleware {
    private Middleware next;

    /**
     * Builds chains of middleware objects.
     */
    public Middleware linkWith(Middleware next) {
        this.next = next;
        return next;
    }

    /**
     * Subclasses will implement this method with concrete checks.
     */
    public abstract boolean check(String email, String password);

    /**
     * Runs check on the next object in chain or ends traversing if we're in
     * last object in chain.
     */
    protected boolean checkNext(String email, String password) {
        if (next == null) {
            return true;
        }
        return next.check(email, password);
    }
}

middleware/ThrottlingMiddleware.java: 檢查請求數量限制

/**
 * ConcreteHandler. Checks whether there are too many failed login requests.
 */
public class ThrottlingMiddleware extends Middleware {
    private int requestPerMinute;
    private int request;
    private long currentTime;

    public ThrottlingMiddleware(int requestPerMinute) {
        this.requestPerMinute = requestPerMinute;
        this.currentTime = System.currentTimeMillis();
    }

    /**
     * Please, not that checkNext() call can be inserted both in the beginning
     * of this method and in the end.
     *
     * This gives much more flexibility than a simple loop over all middleware
     * objects. For instance, an element of a chain can change the order of
     * checks by running its check after all other checks.
     */
    public boolean check(String email, String password) {
        if (System.currentTimeMillis() > currentTime + 60_000) {
            request = 0;
            currentTime = System.currentTimeMillis();
        }

        request++;
        
        if (request > requestPerMinute) {
            System.out.println("Request limit exceeded!");
            Thread.currentThread().stop();
        }
        return checkNext(email, password);
    }
}

middleware/UserExistsMiddleware.java: 檢查用戶登錄資訊

/**
 * ConcreteHandler. Checks whether a user with the given credentials exists.
 */
public class UserExistsMiddleware extends Middleware {
    private Server server;

    public UserExistsMiddleware(Server server) {
        this.server = server;
    }

    public boolean check(String email, String password) {
        if (!server.hasEmail(email)) {
            System.out.println("This email is not registered!");
            return false;
        }
        if (!server.isValidPassword(email, password)) {
            System.out.println("Wrong password!");
            return false;
        }
        return checkNext(email, password);
    }
}

middleware/RoleCheckMiddleware.java: 檢查用戶角色

/**
 * ConcreteHandler. Checks a user's role.
 */
public class RoleCheckMiddleware extends Middleware {
    public boolean check(String email, String password) {
        if (email.equals("[email protected]")) {
            System.out.println("Hello, admin!");
            return true;
        }
        System.out.println("Hello, user!");
        return checkNext(email, password);
    }
}

server

server/Server.java: 授權目標

import java.util.HashMap;
import java.util.Map;

/**
 * Server class.
 */
public class Server {
    private Map<String, String> users = new HashMap<>();
    private Middleware middleware;

    /**
     * Client passes a chain of object to server. This improves flexibility and
     * makes testing the server class easier.
     */
    public void setMiddleware(Middleware middleware) {
        this.middleware = middleware;
    }

    /**
     * Server gets email and password from client and sends the authorization
     * request to the chain.
     */
    public boolean logIn(String email, String password) {
        if (middleware.check(email, password)) {
            System.out.println("Authorization have been successful!");

            // Do something useful here for authorized users.

            return true;
        }
        return false;
    }

    public void register(String email, String password) {
        users.put(email, password);
    }

    public boolean hasEmail(String email) {
        return users.containsKey(email);
    }

    public boolean isValidPassword(String email, String password) {
        return users.get(email).equals(password);
    }
}

Demo.java: 客戶端程式碼

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * Demo class. Everything comes together here.
 */
public class Demo {
    private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    private static Server server;

    private static void init() {
        server = new Server();
        server.register("[email protected]", "admin_pass");
        server.register("[email protected]", "user_pass");

        // All checks are linked. Client can build various chains using the same
        // components.
        Middleware middleware = new ThrottlingMiddleware(2);
        middleware.linkWith(new UserExistsMiddleware(server))
                .linkWith(new RoleCheckMiddleware());

        // Server gets a chain from client code.
        server.setMiddleware(middleware);
    }

    public static void main(String[] args) throws IOException {
        init();

        boolean success;
        do {
            System.out.print("Enter email: ");
            String email = reader.readLine();
            System.out.print("Input password: ");
            String password = reader.readLine();
            success = server.logIn(email, password);
        } while (!success);
    }
}

與其他模式的關係

  • 責任鏈模式命令模式中介者模式觀察者模式用於處理請求發送者和接收者之間的不同連接方式:

    • 責任鏈 按照順序將請求動態傳遞給一系列的潛在接收者, 直至其中一名接收者對請求進行處理。
    • 命令 在發送者和請求者之間建立單向連接。
    • 中介者 清除了發送者和請求者之間的直接連接, 強制它們通過一個中介對象進行間接溝通。
    • 觀察者 允許接收者動態地訂閱或取消接收請求。
  • 責任鏈通常和組合模式結合使用。 在這種情況下, 葉組件接收到請求後, 可以將請求沿包含全體父組件的鏈一直傳遞至對象樹的底部。

  • 責任鏈的管理者可使用命令模式實現。 在這種情況下, 你可以對由請求代表的同一個上下文對象執行許多不同的操作。

    還有另外一種實現方式, 那就是請求自身就是一個命令 對象。 在這種情況下, 你可以對由一系列不同上下文連接而成的鏈執行相同的操作。

  • 責任鏈裝飾模式的類結構非常相似。 兩者都依賴遞歸組合將需要執行的操作傳遞給一系列對象。 但是, 兩者有幾點重要的不同之處。

    責任鏈的管理者可以相互獨立地執行一切操作, 還可以隨時停止傳遞請求。 另一方面, 各種裝飾 可以在遵循基本介面的情況下擴展對象的行為。 此外, 裝飾無法中斷請求的傳遞。

Command

背景

命令模式是一種行為設計模式, 它可將請求轉換為一個包含與請求相關的所有資訊的獨立對象。 該轉換讓你能根據不同的請求將方法參數化、 延遲請求執行或將其放入隊列中, 且能實現可撤銷操作。

使用場景

在市中心逛了很久的街後, 你找到了一家不錯的餐廳, 坐在了臨窗的座位上。 一名友善的服務員走近你, 迅速記下你點的食物, 寫在一張紙上。 服務員來到廚房, 把訂單貼在牆上。 過了一段時間, 廚師拿到了訂單, 他根據訂單來準備食物。 廚師將做好的食物和訂單一起放在托盤上。 服務員看到托盤後對訂單進行檢查, 確保所有食物都是你要的, 然後將食物放到了你的桌上。

那張紙就是一個命令, 它在廚師開始烹飪前一直位於隊列中。 命令中包含與烹飪這些食物相關的所有資訊。 廚師能夠根據它馬上開始烹飪, 而無需跑來直接和你確認訂單詳情。

如果你需要通過操作來參數化對象, 可使用命令模式。

  • 命令模式可將特定的方法調用轉化為獨立對象。 這一改變也帶來了許多有趣的應用: 你可以將命令作為方法的參數進行傳遞、 將命令保存在其他對象中, 或者在運行時切換已連接的命令等。
  • 舉個例子: 你正在開發一個 GUI 組件 (例如上下文菜單), 你希望用戶能夠配置菜單項, 並在點擊菜單項時觸發操作。

如果你想要將操作放入隊列中、 操作的執行或者遠程執行操作, 可使用命令模式。

  • 同其他對象一樣, 命令也可以實現序列化 (序列化的意思是轉化為字元串), 從而能方便地寫入文件或資料庫中。 一段時間後, 該字元串可被恢復成為最初的命令對象。 因此, 你可以延遲或計劃命令的執行。 但其功能遠不止如此! 使用同樣的方式, 你還可以將命令放入隊列、 記錄命令或者通過網路發送命令。

如果你想要實現操作回滾功能, 可使用命令模式。

  • 儘管有很多方法可以實現撤銷和恢復功能, 但命令模式可能是其中最常用的一種。
  • 為了能夠回滾操作, 你需要實現已執行操作的歷史記錄功能。 命令歷史記錄是一種包含所有已執行命令對象及其相關程式狀態備份的棧結構。
  • 這種方法有兩個缺點。 首先, 程式狀態的保存功能並不容易實現, 因為部分狀態可能是私有的。 你可以使用備忘錄模式來在一定程度上解決這個問題。
  • 其次, 備份狀態可能會佔用大量記憶體。 因此, 有時你需要藉助另一種實現方式: 命令無需恢復原始狀態, 而是執行反向操作。 反向操作也有代價: 它可能會很難甚至是無法實現。

實例-實現

使用示例: 命令模式在 Java 程式碼中很常見。 大部分情況下, 它被用於代替包含行為的參數化 UI 元素的回調函數, 此外還被用於對任務進行排序和記錄操作歷史記錄等。

以下是在核心 Java 程式庫中的一些示例:

識別方法: 命令模式可以通過抽象或介面類型 (發送者) 中的行為方法來識別, 該類型調用另一個不同的抽象或介面類型 (接收者) 實現中的方法, 該實現則是在創建時由命令模式的實現封裝。 命令類通常僅限於一些特殊行為。

文字編輯器和撤銷

本例中的文字編輯器在每次用戶與其互動時, 都會創建一個新的命令對象。 命令執行其行為後會被壓入歷史堆棧。

現在, 當程式執行撤銷操作時, 它就需要從歷史記錄中取出最近執行的命令, 然後執行反向操作或者恢復由該命令保存的編輯器歷史狀態。

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 責任鏈模式命令模式中介者模式觀察者模式用於處理請求發送者和接收者之間的不同連接方式:

    • 責任鏈按照順序將請求動態傳遞給一系列的潛在接收者, 直至其中一名接收者對請求進行處理。
    • 命令在發送者和請求者之間建立單向連接。
    • 中介者清除了發送者和請求者之間的直接連接, 強制它們通過一個中介對象進行間接溝通。
    • 觀察者允許接收者動態地訂閱或取消接收請求。
  • 責任鏈的管理者可使用命令模式實現。 在這種情況下, 你可以對由請求代表的同一個上下文對象執行許多不同的操作。

    還有另外一種實現方式, 那就是請求自身就是一個命令對象。 在這種情況下, 你可以對由一系列不同上下文連接而成的鏈執行相同的操作。

  • 你可以同時使用命令備忘錄模式來實現 「撤銷」。 在這種情況下, 命令用於對目標對象執行各種不同的操作, 備忘錄用來保存一條命令執行前該對象的狀態。

  • 命令策略模式看上去很像, 因為兩者都能通過某些行為來參數化對象。 但是, 它們的意圖有非常大的不同。

    • 你可以使用命令來將任何操作轉換為對象。 操作的參數將成為對象的成員變數。 你可以通過轉換來延遲操作的執行、 將操作放入隊列、 保存歷史命令或者向遠程服務發送命令等。
    • 另一方面, 策略通常可用於描述完成某件事的不同方式, 讓你能夠在同一個上下文類中切換演算法。
  • 原型模式可用於保存命令的歷史記錄。

  • 你可以將訪問者模式視為命令模式的加強版本, 其對象可對不同類的多種對象執行操作。

Iterator

背景

迭代器模式是一種行為設計模式, 讓你能在不暴露集合底層表現形式 (列表、 棧和樹等) 的情況下遍歷集合中所有的元素。 迭代器模式的主要思想是將集合的遍歷行為抽取為單獨的迭代器 對象。

迭代器可實現多種遍歷演算法。 多個迭代器對象可同時遍歷同一個集合。

除實現自身演算法外, 迭代器還封裝了遍歷操作的所有細節, 例如當前位置和末尾剩餘元素的數量。 因此, 多個迭代器可以在相互獨立的情況下同時訪問集合。

迭代器通常會提供一個獲取集合元素的基本方法。 客戶端可不斷調用該方法直至它不返回任何內容, 這意味著迭代器已經遍歷了所有元素。

所有迭代器必須實現相同的介面。 這樣一來, 只要有合適的迭代器, 客戶端程式碼就能兼容任何類型的集合或遍歷演算法。 如果你需要採用特殊方式來遍歷集合, 只需創建一個新的迭代器類即可, 無需對集合或客戶端進行修改。

使用場景

當集合背後為複雜的數據結構, 且你希望對客戶端隱藏其複雜性時 (出於使用便利性或安全性的考慮), 可以使用迭代器模式。

  • 迭代器封裝了與複雜數據結構進行交互的細節, 為客戶端提供多個訪問集合元素的簡單方法。 這種方式不僅對客戶端來說非常方便, 而且能避免客戶端在直接與集合交互時執行錯誤或有害的操作, 從而起到保護集合的作用。

使用該模式可以減少程式中重複的遍歷程式碼。

  • 重要迭代演算法的程式碼往往體積非常龐大。 當這些程式碼被放置在程式業務邏輯中時, 它會讓原始程式碼的職責模糊不清, 降低其可維護性。 因此, 將遍歷程式碼移到特定的迭代器中可使程式程式碼更加精鍊和簡潔。

如果你希望程式碼能夠遍歷不同的甚至是無法預知的數據結構, 可以使用迭代器模式。

  • 該模式為集合和迭代器提供了一些通用介面。 如果你在程式碼中使用了這些介面, 那麼將其他實現了這些介面的集合和迭代器傳遞給它時, 它仍將可以正常運行。

實例-實現

使用示例: 該模式在 Java 程式碼中很常見。 許多框架和程式庫都會使用它來提供遍歷其集合的標準方式。

下面是該模式在核心 Java 程式庫中的一些示例:

識別方法: 迭代器可以通過導航方法 (例如 nextprevious等) 來輕鬆識別。 使用迭代器的客戶端程式碼可能沒有其所遍歷的集合的直接訪問許可權。

迭代訪問社交網路檔案

在本例中, 迭代器模式被用於在不向客戶端程式碼暴露通訊細節的情況下訪問遠程社交網路集合中的社交媒體檔案。

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

Mediator

背景

中介者模式是一種行為設計模式, 能讓你減少對象之間混亂無序的依賴關係。 該模式會限制對象之間的直接交互, 迫使它們通過一個中介者對象進行合作。

使用場景

當一些對象和其他對象緊密耦合以致難以對其進行修改時, 可使用中介者模式。

  • 該模式讓你將對象間的所有關係抽取成為一個單獨的類, 以使對於特定組件的修改工作獨立於其他組件。

當組件因過於依賴其他組件而無法在不同應用中復用時, 可使用中介者模式。

  • 應用中介者模式後, 每個組件不再知曉其他組件的情況。 儘管這些組件無法直接交流, 但它們仍可通過中介者對象進行間接交流。 如果你希望在不同應用中復用一個組件, 則需要為其提供一個新的中介者類。

如果為了能在不同情景下復用一些基本行為, 導致你需要被迫創建大量組件子類時, 可使用中介者模式。

  • 由於所有組件間關係都被包含在中介者中, 因此你無需修改組件就能方便地新建中介者類以定義新的組件合作方式。

實例-實現

使用示例: 中介者模式在 Java 程式碼中最常用於幫助程式 GUI 組件之間的通訊。 在 MVC 模式中, 控制器是中介者的同義詞。

下面是核心 Java 程式庫中該模式的一些示例:

筆記程式

本例展示了如何將許多 GUI 元素組織起來, 使其在中介者的幫助下無需相互依賴就能合作。2

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 責任鏈模式命令模式中介者模式觀察者模式用於處理請求發送者和接收者之間的不同連接方式:

    • 責任鏈按照順序將請求動態傳遞給一系列的潛在接收者, 直至其中一名接收者對請求進行處理。
    • 命令在發送者和請求者之間建立單向連接。
    • 中介者清除了發送者和請求者之間的直接連接, 強制它們通過一個中介對象進行間接溝通。
    • 觀察者允許接收者動態地訂閱或取消接收請求。
  • 外觀模式中介者的職責類似: 它們都嘗試在大量緊密耦合的類中組織起合作。

    • 外觀為子系統中的所有對象定義了一個簡單介面, 但是它不提供任何新功能。 子系統本身不會意識到外觀的存在。 子系統中的對象可以直接進行交流。
    • 中介者將系統中組件的溝通行為中心化。 各組件只知道中介者對象, 無法直接相互交流。
  • 中介者觀察者之間的區別往往很難記住。 在大部分情況下, 你可以使用其中一種模式, 而有時可以同時使用。 讓我們來看看如何做到這一點。

    中介者的主要目標是消除一系列系統組件之間的相互依賴。 這些組件將依賴於同一個中介者對象。 觀察者的目標是在對象之間建立動態的單向連接, 使得部分對象可作為其他對象的附屬發揮作用。

    有一種流行的中介者模式實現方式依賴於觀察者。 中介者對象擔當發布者的角色, 其他組件則作為訂閱者, 可以訂閱中介者的事件或取消訂閱。 當中介者以這種方式實現時, 它可能看上去與觀察者非常相似。

    當你感到疑惑時, 記住可以採用其他方式來實現中介者。 例如, 你可永久性地將所有組件鏈接到同一個中介者對象。 這種實現方式和觀察者並不相同, 但這仍是一種中介者模式。

    假設有一個程式, 其所有的組件都變成了發布者, 它們之間可以相互建立動態連接。 這樣程式中就沒有中心化的中介者對象, 而只有一些分散式的觀察者。

Memento

背景

備忘錄模式是一種行為設計模式, 允許在不暴露對象實現細節的情況下保存和恢復對象之前的狀態。

使用場景

當你需要創建對象狀態快照來恢復其之前的狀態時, 可以使用備忘錄模式。

  • 備忘錄模式允許你複製對象中的全部狀態 (包括私有成員變數), 並將其獨立於對象進行保存。 儘管大部分人因為 「撤銷」 這個用例才記得該模式, 但其實它在處理事務 (比如需要在出現錯誤時回滾一個操作) 的過程中也必不可少。

當直接訪問對象的成員變數、 獲取器或設置器將導致封裝被突破時, 可以使用該模式。

  • 備忘錄讓對象自行負責創建其狀態的快照。 任何其他對象都不能讀取快照, 這有效地保障了數據的安全性。

實例-實現

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 你可以同時使用命令模式備忘錄模式來實現 「撤銷」。 在這種情況下, 命令用於對目標對象執行各種不同的操作, 備忘錄用來保存一條命令執行前該對象的狀態。
  • 你可以同時使用備忘錄迭代器模式來獲取當前迭代器的狀態, 並且在需要的時候進行回滾。
  • 有時候原型模式可以作為備忘錄的一個簡化版本, 其條件是你需要在歷史記錄中存儲的對象的狀態比較簡單, 不需要鏈接其他外部資源, 或者鏈接可以方便地重建。

Observer

背景

觀察者模式是一種行為設計模式, 允許你定義一種訂閱機制, 可在對象事件發生時通知多個 「觀察」 該對象的其他對象。

使用場景

當一個對象狀態的改變需要改變其他對象, 或實際對象是事先未知的或動態變化的時, 可使用觀察者模式。

  • 當你使用圖形用戶介面類時通常會遇到一個問題。 比如, 你創建了自定義按鈕類並允許客戶端在按鈕中注入自定義程式碼, 這樣當用戶按下按鈕時就會觸發這些程式碼。

  • 觀察者模式允許任何實現了訂閱者介面的對象訂閱發布者對象的事件通知。 你可在按鈕中添加訂閱機制, 允許客戶端通過自定義訂閱類注入自定義程式碼。

當應用中的一些對象必須觀察其他對象時, 可使用該模式。 但僅能在有限時間內或特定情況下使用。

  • 訂閱列表是動態的, 因此訂閱者可隨時加入或離開該列表。

實例-實現

使用示例: 觀察者模式在 Java 程式碼中很常見, 特別是在 GUI 組件中。 它提供了在不與其他對象所屬類耦合的情況下對其事件做出反應的方式。

這裡是核心 Java 程式庫中該模式的一些示例:

識別方法: 該模式可以通過將對象存儲在列表中的訂閱方法, 和對於面向該列表中對象的更新方法的調用來識別。

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 責任鏈模式命令模式中介者模式觀察者模式用於處理請求發送者和接收者之間的不同連接方式:

    • 責任鏈按照順序將請求動態傳遞給一系列的潛在接收者, 直至其中一名接收者對請求進行處理。
    • 命令在發送者和請求者之間建立單向連接。
    • 中介者清除了發送者和請求者之間的直接連接, 強制它們通過一個中介對象進行間接溝通。
    • 觀察者允許接收者動態地訂閱或取消接收請求。
  • 中介者觀察者之間的區別往往很難記住。 在大部分情況下, 你可以使用其中一種模式, 而有時可以同時使用。 讓我們來看看如何做到這一點。

    中介者的主要目標是消除一系列系統組件之間的相互依賴。 這些組件將依賴於同一個中介者對象。 觀察者的目標是在對象之間建立動態的單向連接, 使得部分對象可作為其他對象的附屬發揮作用。

    有一種流行的中介者模式實現方式依賴於觀察者。 中介者對象擔當發布者的角色, 其他組件則作為訂閱者, 可以訂閱中介者的事件或取消訂閱。 當中介者以這種方式實現時, 它可能看上去與觀察者非常相似。

    當你感到疑惑時, 記住可以採用其他方式來實現中介者。 例如, 你可永久性地將所有組件鏈接到同一個中介者對象。 這種實現方式和觀察者並不相同, 但這仍是一種中介者模式。

    假設有一個程式, 其所有的組件都變成了發布者, 它們之間可以相互建立動態連接。 這樣程式中就沒有中心化的中介者對象, 而只有一些分散式的觀察者。

State

背景

狀態模式是一種行為設計模式, 讓你能在一個對象的內部狀態變化時改變其行為, 使其看上去就像改變了自身所屬的類一樣。 狀態模式與有限狀態機的概念緊密相關。 其主要思想是程式在任意時刻僅可處於幾種有限狀態中。 在任何一個特定狀態中, 程式的行為都不相同, 且可瞬間從一個狀態切換到另一個狀態。 不過, 根據當前狀態, 程式可能會切換到另外一種狀態, 也可能會保持當前狀態不變。 這些數量有限且預先定義的狀態切換規則被稱為轉移

例如:

使用場景

如果對象需要根據自身當前狀態進行不同行為, 同時狀態的數量非常多且與狀態相關的程式碼會頻繁變更的話, 可使用狀態模式。

  • 模式建議你將所有特定於狀態的程式碼抽取到一組獨立的類中。 這樣一來, 你可以在獨立於其他狀態的情況下添加新狀態或修改已有狀態, 從而減少維護成本。

如果某個類需要根據成員變數的當前值改變自身行為, 從而需要使用大量的條件語句時, 可使用該模式。

  • 狀態模式會將這些條件語句的分支抽取到相應狀態類的方法中。 同時, 你還可以清除主要類中與特定狀態相關的臨時成員變數和幫手方法程式碼。

當相似狀態和基於條件的狀態機轉換中存在許多重複程式碼時, 可使用狀態模式。

  • 狀態模式讓你能夠生成狀態類層次結構, 通過將公用程式碼抽取到抽象基類中來減少重複。

實例-實現

使用示例: 在 Java 語言中, 狀態模式通常被用於將基於 switch語句的大型狀態機轉換為對象。

這裡是核心 Java 程式庫中一些狀態模式的示例:

識別方法: 狀態模式可通過受外部控制且能根據對象狀態改變行為的方法來識別。

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 橋接模式狀態模式策略模式 (在某種程度上包括適配器模式) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
  • 狀態可被視為策略的擴展。 兩者都基於組合機制: 它們都通過將部分工作委派給 「幫手」 對象來改變其在不同情景下的行為。 策略使得這些對象相互之間完全獨立, 它們不知道其他對象的存在。 但狀態模式沒有限制具體狀態之間的依賴, 且允許它們自行改變在不同情景下的狀態。

Strategy

背景

策略模式是一種行為設計模式, 它能讓你定義一系列演算法, 並將每種演算法分別放入獨立的類中, 以使演算法的對象能夠相互替換。

策略是一種行為設計模式, 它將一組行為轉換為對象, 並使其在原始上下文對象內部能夠相互替換。

原始對象被稱為上下文, 它包含指向策略對象的引用並將執行行為的任務分派給策略對象。

為了改變上下文完成其工作的方式, 其他對象可以使用另一個對象來替換當前鏈接的策略對象。

使用場景

當你想使用對象中各種不同的演算法變體, 並希望能在運行時切換演算法時, 可使用策略模式。

  • 策略模式讓你能夠將對象關聯至可以不同方式執行特定子任務的不同子對象, 從而以間接方式在運行時更改對象行為。

當你有許多僅在執行某些行為時略有不同的相似類時, 可使用策略模式。

  • 策略模式讓你能將不同行為抽取到一個獨立類層次結構中, 並將原始類組合成同一個, 從而減少重複程式碼。

如果演算法在上下文的邏輯中不是特別重要, 使用該模式能將類的業務邏輯與其演算法實現細節隔離開來。

  • 策略模式讓你能將各種演算法的程式碼、 內部數據和依賴關係與其他程式碼隔離開來。 不同客戶端可通過一個簡單介面執行演算法, 並能在運行時進行切換。

當類中使用了複雜條件運算符以在同一演算法的不同變體中切換時, 可使用該模式。

  • 策略模式將所有繼承自同樣介面的演算法抽取到獨立類中, 因此不再需要條件語句。 原始對象並不實現所有演算法的變體, 而是將執行工作委派給其中的一個獨立演算法對象。

實例-實現

使用示例: 策略模式在 Java 程式碼中很常見。 它經常在各種框架中使用, 能在不擴展類的情況下向用戶提供改變其行為的方式。

Java 8 開始支援 lambda 方法, 它可作為一種替代策略模式的簡單方式。

這裡有一些核心 Java 程式庫中策略模式的示例:

識別方法: 策略模式可以通過允許嵌套對象完成實際工作的方法以及允許將該對象替換為不同對象的設置器來識別。

例子:電子商務應用中的支付方法

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 橋接模式狀態模式策略模式 (在某種程度上包括適配器模式) 模式的介面非常相似。 實際上, 它們都基於組合模式——即將工作委派給其他對象, 不過也各自解決了不同的問題。 模式並不只是以特定方式組織程式碼的配方, 你還可以使用它們來和其他開發者討論模式所解決的問題。
  • 命令模式策略看上去很像, 因為兩者都能通過某些行為來參數化對象。 但是, 它們的意圖有非常大的不同。
    • 你可以使用命令來將任何操作轉換為對象。 操作的參數將成為對象的成員變數。 你可以通過轉換來延遲操作的執行、 將操作放入隊列、 保存歷史命令或者向遠程服務發送命令等。
    • 另一方面, 策略通常可用於描述完成某件事的不同方式, 讓你能夠在同一個上下文類中切換演算法。
  • 裝飾模式可讓你更改對象的外表, 策略則讓你能夠改變其本質。
  • 模板方法模式基於繼承機制: 它允許你通過擴展子類中的部分內容來改變部分演算法。 策略基於組合機制: 你可以通過對相應行為提供不同的策略來改變對象的部分行為。 模板方法在類層次上運作, 因此它是靜態的。 策略在對象層次上運作, 因此允許在運行時切換行為。
  • 狀態可被視為策略的擴展。 兩者都基於組合機制: 它們都通過將部分工作委派給 「幫手」 對象來改變其在不同情景下的行為。 策略使得這些對象相互之間完全獨立, 它們不知道其他對象的存在。 但狀態模式沒有限制具體狀態之間的依賴, 且允許它們自行改變在不同情景下的狀態。

Template Method

背景

模板方法模式是一種行為設計模式, 它在超類中定義了一個演算法的框架, 允許子類在不修改結構的情況下重寫演算法的特定步驟。

使用場景

當你只希望客戶端擴展某個特定演算法步驟, 而不是整個演算法或其結構時, 可使用模板方法模式。

  • 模板方法將整個演算法轉換為一系列獨立的步驟, 以便子類能對其進行擴展, 同時還可讓超類中所定義的結構保持完整。

當多個類的演算法除一些細微不同之外幾乎完全一樣時, 你可使用該模式。 但其後果就是, 只要演算法發生變化, 你就可能需要修改所有的類。

  • 在將演算法轉換為模板方法時, 你可將相似的實現步驟提取到超類中以去除重複程式碼。 子類間各不同的程式碼可繼續保留在子類中。

實例-實現

使用示例: 模版方法模式在 Java 框架中很常見。 開發者通常使用它來向框架用戶提供通過繼承實現的、 對標準功能進行擴展的簡單方式。

這裡是一些核心 Java 程式庫中模版方法的示例:

識別方法: 模版方法可以通過行為方法來識別, 該方法已有一個在基類中定義的 「默認」 行為。

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係

  • 工廠方法模式模板方法模式的一種特殊形式。 同時, 工廠方法可以作為一個大型模板方法中的一個步驟。
  • 模板方法基於繼承機制: 它允許你通過擴展子類中的部分內容來改變部分演算法。 策略模式基於組合機制: 你可以通過對相應行為提供不同的策略來改變對象的部分行為。 模板方法在類層次上運作, 因此它是靜態的。 策略在對象層次上運作, 因此允許在運行時切換行為。

Visitor

背景

訪問者模式是一種行為設計模式, 它能將演算法與其所作用的對象隔離開來。

訪問者是一種行為設計模式, 允許你在不修改已有程式碼的情況下向已有類層次結構中增加新的行為。

使用場景

如果你需要對一個複雜對象結構 (例如對象樹) 中的所有元素執行某些操作, 可使用訪問者模式。

  • 訪問者模式通過在訪問者對象中為多個目標類提供相同操作的變體, 讓你能在屬於不同類的一組對象上執行同一操作。

可使用訪問者模式來清理輔助行為的業務邏輯。

  • 該模式會將所有非主要的行為抽取到一組訪問者類中, 使得程式的主要類能更專註於主要的工作。

當某個行為僅在類層次結構中的一些類中有意義, 而在其他類中沒有意義時, 可使用該模式。

  • 你可將該行為抽取到單獨的訪問者類中, 只需實現接收相關類的對象作為參數的訪問者方法並將其他方法留空即可。

實例-實現

使用示例: 訪問者不是常用的設計模式, 因為它不僅複雜, 應用範圍也比較狹窄。

這裡是 Java 程式庫程式碼中該模式的一些示例:

具體實現查看項目地址://gitee.com/zwtgit/gof23

與其他模式的關係