設計模式概覽

xfcoding發表於2024-04-22

23種設計模式,背後是7大設計原則。即每個設計模式都歸屬於一個或多個設計原則。

7大設計原則也是物件導向程式設計應遵循的七大原則,這些原則的思想就是,一個字:分(低耦合 )。

七大設計原則

  • 單一職責原則
  • 里氏替換原則
  • 依賴倒置原則
  • 開閉原則
  • 迪米特法則(最少知識原則)
  • 介面隔離原則
  • 組合優於繼承原則

單一職責原則:

每個方法、每個類、每個框架只負責一件事。

開閉原則

對擴充套件開放,對修改關閉。

介面隔離原則

客戶端不應該依賴它不需要的介面。

一個類對另一個類的依賴應該建立在最小的介面上。

使用多個專門的介面比使用單一的總介面要好。一個介面代表一個角色,不應當將不同的角色都交給一個介面。沒有關係的介面合併在一起,形成一個臃腫的大介面,這是對角色和介面的汙染。

說白了,不要強迫客戶使用它們不用的方法,否則,這些客戶就會面臨由於這些不使用的方法的改變所帶來的改變。

跟單一職責原則的思想一致。

依賴倒置原則

依賴倒置原則是程式要依賴於抽象介面,不要依賴於具體實現。簡單的說就是要求對抽象進行程式設計,不要對實現進行程式設計,這樣就降低了客戶與實現模組間的耦合

上層不能依賴於下層,他們都應該依賴於抽象。

什麼是上層,什麼是下層?呼叫別的方法的就是上層,被其他方法呼叫的,就是下層。

迪米特法則

思想就是封裝。

迪米特法則又叫作最少知識原則(The Least Knowledge Principle),一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少的瞭解,只和朋友通訊,不和陌生人說話。設計模式的門面模式和中介模式,都是迪米特法則應用的例子

簡單說就是:talk only to your immediate friends。什麼是朋友?

“朋友”條件:

1)該物件本身(this)

2)被當做方法的引數而傳遞進來的物件

3)當前方法建立的例項化的物件

4)當前物件的例項變數如果是一個聚集,那麼聚集中的元素也都是朋友

5)當前物件的任何元件

任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”;否則就是“陌生人”。

迪米特法則也有缺點,需權衡。

裡式替換原則

任何能使用父類物件的地方,都應該能透明的替換為子類物件。

也就說,子類物件可以隨時替換父類物件,且替換後不改變業務邏輯。

典型案例:正方形不是長方形;鴕鳥非鳥。

引申:什麼情況下可以繼承?

兩個原則:是否有“is a”的關係;子類替換父類後是否影響業務邏輯。

組合優於繼承

類和類之間基本有3中關係:

  • 繼承

  • 依賴(一個類的物件作為另一個類的區域性變數)

  • 關聯(一個類的物件作為另一個類的成員變數)

    • 組合:鳥與翅膀。就是關係強
    • 聚合:大雁與雁群。就是關係弱

繼承的缺點:

打破了封裝,導致基類向子類暴露了實現細節;父類的改變通常是不可控的。

繼承意味著可以重用父類的所有可用的方法,即使這個行為當前類的設計原則違背。典型例子:JDK的Stack類,它繼承了Vector,但是Vector有在指定位置增加、刪除的方法,這些方法是違背Stack的設計原則的。

學習設計模式,要注意亮點:

1.開發程式碼的程式設計師,被劃分為兩種角色:

a.作者(服務端程式設計師)

​ b. 使用者(客戶端程式設計師)

​ 比如,我們使用c3p0連線池時,c3p0的開發者是作者,而類庫的呼叫者就是使用者。

2.我們調類庫時,一定要符合開閉原則。

設計模式分類

  • 建立型模式
    • 單例模式
    • 抽象工廠模式
    • 工廠方法模式
    • 建造者模式
    • 原型模式
  • 結構型模式
    • 介面卡模式
    • 橋接模式
    • 裝飾者模式
    • 組合模式
    • 外觀模式
    • 享元模式
    • 代理模式
  • 行為型模式
    • 模板方法模式
    • 命令模式
    • 迭代器模式
    • 觀察者模式
    • 中介者模式
    • 備忘錄模式
    • 直譯器模式
    • 狀態模式
    • 策略模式
    • 責任鏈模式
    • 訪問者模式

建立型模式

工廠方法(Factory)

工廠模式分為三種:簡單工廠(不屬於23種 設計模式,是工廠方法的特例),工廠方法,抽象工廠。

工廠模式相關概念:

  1. 產品:類的例項
  2. 抽象產品:抽象類或介面
  3. 產品簇:多個有內在聯絡或者有邏輯關係的產品。比如肯德基套餐:薯條+漢堡+可樂
  4. 產品等級:相當於產品型別

工廠模式的核心思想:面向介面程式設計

簡單工廠是工廠方法的一個特例,先來看看簡單工廠。

工廠是一個類,工廠生產產品。

每 new 一個物件,相當於呼叫者多知道了一個類,增加了類與類之間的聯絡,不利於程式的松耦合。其實構建過程可以被封裝起來,工廠模式便是用於封裝物件的設計模式。

水果工廠:

public class FruitFactory {
     public Fruit create(String type) {
         switch (type){
         	case "蘋果": return new Apple();
            case "梨子": return new Pear();
            default: throw new IllegalArgumentException("暫時沒有這種水果");   
     }
}

事實上,將構建過程封裝的好處不僅可以降低耦合,如果某個產品構造方法相當複雜,使用工廠模式可以大大減少程式碼重複,這對呼叫者是很方便的,因為它不用關心複雜的物件構建過程。

優點:封裝了物件構建過程,讓客戶端呼叫變得方便,讓程式解耦。

缺點:

  1. 客戶端需要記住產品的對映關係
  2. 如果具體產品多,則簡單工廠變得十分臃腫
  3. 客戶端需要擴充套件具體產品的時候,勢必要修改簡單工廠中的程式碼,這樣違反了開閉原則

工廠方法模式來解決簡單工單的弊端,它規定每個產品都有一個專屬工廠。

工廠方法模式定義了一個建立物件的介面,但由子類決定要例項化的類是哪一個。工廠方法讓類把例項化推遲到子類。

工廠方法模式(Factory Method pattern)是最典型的模板方法模式(Template Method pattern)應用

工廠方法有四個角色:抽象工廠,具體工廠;抽象產品,具體產品

// 工廠介面
public interface IFruitFactory {
    Fruit create();
}

// 蘋果工廠
public class AppleFactory implements IFruitFactory {
	public Fruit create() {
		return new Apple();
    }
}

// 梨子工廠
public class PearFactory implements IFruitFactory {
    public Fruit create() {
        return new Pear();
	}
}

不要覺得這例子很簡單,就認為這模式是脫褲子放屁,在物件構建很複雜的情況下,這種模式還是保持了它的優點的,就是:工廠將構建過程封裝起來,呼叫者可以很方便的直接使用。

同時他解決了簡單工廠的兩個弊端:

  1. 產品種類多時,工廠類不會臃腫。工廠方法符合單一職責原則。
  2. 需要新的產品時,不用改原來的工廠類,只需要新增新的工廠類。符合開閉原則。

與此同時,還是那個的缺點:產品等級增多時,工廠類的數量就會增多。

工廠方法UML:

抽象工廠(Factory)

在工廠裡生產多個產品等級。

一個工廠介面,生產不同的產品。

抽象工廠模式提供一個介面,用於建立相關或依賴物件的家族,而不需要明確指定具體類。

加入現在有個商鋪只賣兩種水果,蘋果和梨子,生產蘋果和梨子的工廠也有很多。它選擇的是工廠A。

public interface IFruitFactory {
    Fruit createApple();
    Fruit createPear();
}

public class AFactory implements IFruitFactory {
    @Override
    public Fruit createApple(){
        return new Apple("A");
    }
    
    @Override
    public Fruit createPear(){
        return new Pear("A");
    }
}

那麼對商鋪來說,賣來自A工廠的水果就是:

public class Client {
    public void main(String[] args) {
        IFruitFactory fruitFactory = new AFactory();
        Fruit apple = fruitFactory.createApple();
        Fruit pear = fruitFactory.createPear();
        
        sell(apple, pear);
	}
}

如果哪天,A工廠的水果質量不好了,商鋪想換成B工廠的水果,就是這樣:

public class BFactory implements IFruitFactory {
    @Override
    public Fruit createApple(){
        return new Apple("B");
    }
    
    @Override
    public Fruit createPear(){
        return new Pear("B");
    }
}

那麼對於客戶端而言,改動很小,只用換一個具體的工廠就可以了:

public class Client {
    public void main(String[] args) {
        IFruitFactory fruitFactory = new BFactory();
        Fruit apple = fruitFactory.createApple();
        Fruit pear = fruitFactory.createPear();
        
        sell(apple, pear);
	}
}

由於客戶端只和 IFactory 打交道了,呼叫的是介面中的方法,使用時根本不需要知道是在哪個具體工廠中實現的這些方法,這就使得替換工廠變得非常容易。

抽象工廠模式主要用於替換一系列方法。也就是說,客戶端會使用這個介面工廠中的一系列介面,當服務端換掉這些介面的具體實現時,客戶端毫不知情。

優點:1.仍然有簡單工廠和工廠方法的優點;2.減少了工廠類的數量。

缺點:當產品等級(產品的種類)增多時,就要修改抽象工廠的程式碼,這會違反開閉原則;

因此,當產品等級比較固定時,可以考慮抽象工廠,否則不建議使用。

原型模式(Prototype)

原型模式是一種建立型模式,它允許一個物件再建立另外一個可定製的物件,根本無需知道建立的細節。當直接建立物件的代價比較大時,則採用這種模式。

在Java中Prototype模式變成clone()方法的使用,由於Java的物件導向特性,使得在Java中使用原型模式變得很自然,兩者已經幾乎是渾然一體了。這反映在很多模式上,如Iterator遍歷模式。

實現方法:

  1. 必須讓布標類實現Cloneable介面
  2. 必須重寫Object的clone方法,並且要把重寫後的方法的訪問控制修飾符改為public
public class MilkTea implements Cloneable {
    public String type;
    public boolean ice;
    
    // 這種方式是淺複製
    public MilkTea clone() throws CloneNotSupportedException {
        return (MilkTea) super.clone();
    }
}

這種模式要分清物件的淺複製與深複製。

建造者模式(Builder)

建造者模式是將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。說白了就是,將一個相似的構建物件的過程抽象出來,達到程式碼複用和可擴充套件的功能。

在這種設計模式中,有以下幾個角色:

  1. builder,為建立一個產品物件的各個部件指定介面(指定建造標準,穩定建造流程)
  2. ConcreteBuilder:實現builder介面的具體建造者
  3. Director:構造一個使用builder介面的物件。它就是指導者,由他封裝建造的過程。
  4. Product:具體的產品,被構建的物件。

擴充套件:建造者模式在使用過程中可以演化出多種形式

省略抽象建造者角色:如果系統中只需要一個具體的建造者的話,可以省略掉抽象建造者

省略指導者角色:在具體建造者只有一個的情況下,如果抽象建造者角色已經被省略掉,那麼還可以省略掉指導者角色,讓Builder自己扮演指導者和建造者雙重角色

建造者與工廠模式的區別在於,工廠模式只需要新建出 一個產品即可,即new出一個物件;而建造者模式更注重產品新建出來後,為產品屬性賦值的過程,它強調的是構建過程。

單例模式(Singleton)

單例模式確保一個類只有一個例項,並提供一個全域性訪問點。

餓漢式

變數在申明時即被初始化。

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

優點:執行緒安全;直觀

缺點:增加類初始化時間,類即使不使用,也會被例項化

懶漢式

先宣告一個空變數,需要用時才初始化

方式一:雙檢鎖方式實現的執行緒安全的單例模式,別漏掉volatile

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

方式二:靜態內部類方式保證懶漢式單例的執行緒安全

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

為什麼這種方式能保證執行緒安全?來分析兩個問題:

  • 靜態內部類方式是怎麼實現懶載入的
  • 靜態內部類方式是怎麼保證執行緒安全的

類在初始化的時候,會立即載入內部類,內部類會在使用時才載入。所以當此 Singleton 類載入時,SingletonHolder 並不會被立即載入,所以不會像餓漢式那樣佔用記憶體。另外,Java 虛擬機器規定,當訪問一個類的靜態欄位時,如果該類尚未初始化,則立即初始化此類。當呼叫Singleton 的 getInstance 方法時,由於其使用了 SingletonHolder 的靜態變數instance,所以這時才會去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 物件。這就實現了懶載入。

其次,虛擬機器在載入類的 clinit 方法時,會保證 clinit 在多執行緒中被正確的加鎖、同步,即使有多個執行緒同時去初始化一個類,一次也只有一個執行緒可以執行 clinit 方法,其他執行緒都需要阻塞等待,從而保證了執行緒安全。

如何權衡兩種方式?一般的建議是:對於構建不復雜,載入完成後會立即使用的單例物件,推薦使用餓漢式。對於構建過程耗時較長,並不是所有使用此類都會用到的單例物件,推薦使用懶漢式。

結構型模式

裝飾者模式(Decorator)

裝飾模式是在不必改變原類檔案和使用繼承的情況下,動態地擴充套件一個物件的功能。它是透過建立一個包裝物件,也就是裝飾來包裹真實的物件。

裝飾模式:動態地給一個物件增加一些額外的職責,就增加物件功能來說,裝飾模式比生成子類實現更為靈活。

提到裝飾,我們先來想一下生活中有哪些裝飾:

  • 女生的首飾:戒指、耳環、項鍊等裝飾品
  • 家居裝飾品:粘鉤、鏡子等

可以看到,一種裝飾是為了讓原來的更美觀,一種不僅是美觀,還能有新的功能(粘鉤可掛東西,鏡子是用來照的)。因此可以總結出裝飾模式的兩種功能:

  • 增強原有的特性
  • 新增新的特性

並且,裝飾者並不會改變原有物品本身,只是起到一個錦上添花的作用。裝飾模式也一樣:

  • 增強一個類的功能
  • 為一個類新增新的功能

且裝飾類不會改變原有的類。

例子:

現在有一個類要求輸出一個人的顏值。

新建一個顏值介面:

public interface IBeauty {
    int getBeautyValue();
}

新建一個具體實現:

public class me implements IBeauty {
    @Override
    public int getBeautyValue() {
        return 100;
    }
}

但是一個人顏值在不同的情況下是不同的,當我們帶上裝飾品後,顏值會提高,且裝飾品可以不斷組合。例如:

我帶上了戒指,很帥氣。新建一個戒指裝飾類

public class RingDecorator implements IBeauty {
	private final IBeauty me;

       public RingDecorator(IBeauty me) {
          this.me = me;
       }

       @Override
       public int getBeautyValue() {
          return me.getBeautyValue() + 20;
       }
}

測試一把:

public class Client {
       @Test
       public void show() {
           IBeauty me = new Me();
           System.out.println("我原本的顏值:" + me.getBeautyValue());
       
           IBeauty meWithRing = new RingDecorator(me);
           System.out.println("我帶上戒指後的顏值:" + meWithRing.getBeautyValue());
       }
}

輸出結果:

我原本的顏值:100
我帶上戒指後的顏值:120

這就是最簡單的增強功能的裝飾模式。以後我們可以新增更多的裝飾類,比如:

耳環裝飾類:

public class EarringDecorator implements IBeauty { 
	private final IBeauty me;
    
    public EarringDecorator(IBeauty me) {
        this.me = me;
    }
    
    @Override
    public int getBeautyValue() {
        return me.getBeautyValue() + 50;
    }
}

還可以有更多的裝飾類......

測試:

public class Client {
    @Test
    public void show() {
        IBeauty me = new Me();
        System.out.println("我原本的顏值:" + me.getBeautyValue());
        
        // 多次裝飾、隨意組合
        IBeauty meWithEarring = new EarringDecorator(new RingDecorator(me));
		System.out.println("我帶上戒指、耳環後的顏值:" + meWithEarring.getBeautyValue());
    }
}

輸出:

我原本的顏值:100
我帶上戒指、耳環後的顏值:170

這裡的裝飾器也實現了IBeaty介面,並且沒有新增新的方法,也就是或裝飾器僅用於增強功能,沒有改變Me原有的功能,這種裝飾模式叫透明裝飾模式,所以這種透明裝飾模式可以無限裝飾。

裝飾模式是繼承的一種替代方案。本例中如果不適用裝飾模式,那麼每一種裝飾品都要新增一個子類,每一個裝飾品之間組合又得新增一個子類,當組合增多時就會造成類爆炸。所以說這種場景下裝飾模式更靈活。

下面看看新增功能的的場景:

我們用程式來模擬一下房屋裝飾粘鉤後,新增了掛東西功能的過程:

新建房屋介面和房屋類:

public interface IHouse {
    void live();
}

public class House implements IHouse {
    @Override
    public void live() {
         System.out.println("房屋原有的功能:居住功能");
    }
}

新建粘鉤裝飾器介面,繼承自房屋介面:

public interface IStickyHookHouse extends IHouse {
    void hangThings();
}

粘鉤裝飾類:

public class StickyHookDecorator implements IStickyHookHouse {
    private final IHouse house;
    
    public StickyHookDecorator(IHouse house) {
        this.house = house;
    }
    
    @Override
    public void live() {
        house.live();
    }
    
    @Override
    public void hangThings() {
    	System.out.println("有了粘鉤後,新增了掛東西功能");
    }
}

這裡為什麼要搞一個裝飾器介面,原因是基礎元件是IHouse,是一個介面,基礎實現是House。裝飾器是對基礎元件的繼承或實現,如果基礎元件是一個抽象類,可以不要這個介面,直接繼承這個類,本例中因為裝飾器類不能直接繼承IHouse,所以需要一個介面,從抽象的角度來說,是一樣的,我們只關注抽象。

注意:繼承抽象是為了有正確的型別,而不是繼承它的行為,行為來自裝飾者和基礎元件,或與其他裝飾者之間的組合關係。

客戶端測試:

public class Client {
    @Test
    public void show() {
        IHouse house = new House();
        house.live();
        
        IHouse house = new stickyHookHouse();
        stickyHookHouse.live();
        stickyHookHouse.hangThings();
	}
}

結果:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘鉤後,新增了掛東西功能

這就是用於 新增功能 的裝飾模式。我們並沒有修改原有的功能,只是擴充套件了新的功能,這種模式在裝飾模式中稱之為 半透明裝飾模式。半透明裝飾模式具有不能多次裝飾的特點。因為新的功能來自裝飾器本身,而裝飾器之間是不能有繼承等依賴關係的,那樣就不是裝飾模式了。

UML

裝飾模式中,面對抽象元件,有一個基本具體實現,當需要組合其他功能時,可以構建一個裝飾器,且裝飾器是要繼承和關聯抽象元件的。繼承是為了跟抽象元件型別一致,這是裝飾的核心,關聯是為了使用基礎元件的功能。

I/O流中的裝飾模式

其中,InputStream 是一個抽象類,對應上文例子中的 IHouse,其中最重要的方法是 read 方法,這是一個抽象方法。

左邊的三個類 FileInputStream、ByteArrayInputStream、ServletInputStreamInputStream 的子類,對應上文例子中實現了 IHouse 介面的 House。右下角的三個類 BufferedInputStream、DataInputStream、CheckedInputStream是三個具體的裝飾者類,他們都為 InputStream 增強了原有功能或新增了新功能。

FilterInputStream 是所有裝飾類的父類,它沒有實現具體的功能,僅用來包裝了一下InputStream

在原始碼中我們發現,BufferedInputStream 沒有新增 InputStream 中沒有的方法,所以BufferedInputStream 使用的是 透明的裝飾模式DataInputStream 用於更加方便地讀取 int、double 等內容,觀察 DataInputStream 的原始碼可以發現,DataInputStream 中新增了 readIntreadLong 等方法,所以 DataInputStream 使用的是 半透明裝飾模式

這就是裝飾模式,注意不要和介面卡模式混淆了。兩者在使用時都是包裝一個類,但兩者的區別其實也很明顯:

  • 純粹的介面卡模式 僅用於改變介面,不改變其功能,部分情況下我們需要改變一點功能以適配新介面。但使用介面卡模式時,介面一定會有一個 回爐重造 的過程
  • 裝飾模式 不改變原有的介面,僅用於增強原有功能或新增新功能,強調的是 錦上添花

裝飾模式的優缺點:

優點: 1、裝飾者是繼承的有力補充,比繼承靈活,不改變原有物件的情況下動態地給一個物件 擴充套件功能,即插即用。 2、透過使用不同裝飾類以及這些裝飾類的排列組合,可以實現不同效果。 3、裝飾者完全遵守開閉原則。

缺點: 1、會出現更多的程式碼,更多的類,增加程式複雜性。 2、動態裝飾時,多層裝飾時會更復雜。

介面卡模式(Adapter)

將一個原有的類的介面轉換成期望的另一個介面,使原本的介面不相容的類可以一起工作,這個轉換過程就是適配,這個中介軟體就稱之為介面卡,屬於結構型設計模式。介面卡模式是作為兩個不相容的介面之間的橋樑,它結合了兩個獨立介面的功能。

那什麼情況下可以使用介面卡模式呢?

比如,現在系統已經有一個處理介面Processor,對一個字串進行處理,Processor類有兩個方法,預設方法name()返回當前類名,process()方法接受引數,返回處理後的物件。

interface Processor {
    default String name() {
        return getClass().getSimpleName();
    }

    Object process(Object input);
}

針對不同的處理情況,process方法有不同的實現,那麼伴隨Processor多個實現類。

class Upcase implements Processor {
    @Override 
    public String process(Object input) {
        return ((String) input).toUpperCase();
    }
}

class Downcase implements Processor {
    @Override
    public String process(Object input) {
        return ((String) input).toLowerCase();
    }
}

這裡有三個處理類,分別是:字串轉大寫、字串轉小寫。

那麼實際應用的時候,就是這樣:

public class Applicator {
    public static void apply(Processor p, Object s) {
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }

    public static void main(String[] args) {
        String s = "We are such stuff as dreams are made on";
        apply(new Upcase(), s);
        apply(new Downcase(), s);
    }
}

輸出:

Using Processor Upcase
WE ARE SUCH STUFF AS DREAMS ARE MADE ON
Using Processor Downcase
we are such stuff as dreams are made on

現在,假如我們偶發現,系統中別人寫的類(或者要用的類庫)有跟Applicator類的apply方法相似的套路,比如現在有一個波段的類Waveform,以及過濾波段的過濾器類Filter

public class Waveform {
    private static long counter;
    private final long id = counter++;

    @Override
    public String toString() {
        return "Waveform " + id;
    }
}

public class Filter {
    public String name() {
        return getClass().getSimpleName();
    }

    public Waveform process(Waveform input) {
        return input;
    }
}

過濾器也是為了處理波段,它有兩個方法:返回類名name()跟處理波段process()。針對不同的過濾波段的方式,伴隨著不同的Filter的子類:

public class LowPass extends Filter {
    double cutoff;

    public LowPass(double cutoff) {
        this.cutoff = cutoff;
    }

    @Override
    public Waveform process(Waveform input) {
        return input; // Dummy processing 啞處理
    }
}

public class HighPass extends Filter {
    double cutoff;

    public HighPass(double cutoff) {
        this.cutoff = cutoff;
    }

    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

為了簡寫,子類過濾器的process()方法都做了虛假實現,實際情況是會有不同的實現的。

可以看到這個處理波段的過濾器類跟前面的Processor類很相似,處理套路幾乎一樣。我們想要複用Applicator的apply方法!

這個時候,我們要做的就是適配apply方法的Processor型別的引數。那如何讓Filter變成Processor型別的呢?實現Processor介面時肯定不行的,這違背開閉原則,而且如果是第三方庫的話也不可能修改。

這個時候,我們申明一個介面卡來適配Processor介面

class FilterAdapter implements Processor {
    Filter filter;

    public FilterAdapter(Filter filter) {
        this.filter = filter;
    }

    @Override
    public String name() {
        return filter.name();
    }

    @Override
    public Waveform process(Object input) {
        return filter.process((Waveform) input);
    }
}

public class FilterProcessor {
    public static void main(String[] args) {
        Waveform w = new Waveform();
        Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);
        Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);
    }
}

這就是介面卡模式的應用。再摘一個簡單例子:

鴨子與野雞的故事

鴨子可以飛的近,可以“呱呱叫”;野雞飛的遠,可以“咯咯叫”;

public interface Duck {
    quack();
    fly(); 
}

public class FamilyDuck implements Duck {
    @override
    public void quack() {
        System.out.Println("家鴨呱呱叫");
    }
    
    @override
    public void fly() {
        System.out.Println("家鴨飛了一小段");
    }
}
public interface WildChicken {
    gobble();
    fly(); 
}

public class WildChicken implements WildChicken {
    @override
    public void gobble() {
        System.out.Println("野雞咯咯叫");
    }
    
    @override
    public void fly() {
        System.out.Println("野雞飛了好遠");
    }
}

假設你現在缺少鴨子物件,需要野雞冒充,那就需要野雞適配鴨子。

public class WildChickenAdapter implements Duck {
    private WildChicken wildChicken;
    
    public WildChickenAdapter(WildChicken wildChicken) {
        this.wildChicken = wildChicken;
    }
    
    @override
    public void quack() {
        System.out.Println(wildChicken.gobble());
    }
    
    @override
    public void fly() {
        System.out.Println(wildChicken.fly());
    }
}

這樣,野雞就適配鴨子了,需要鴨子的時候,用野雞介面卡就可以了。

總結就是:當你需要把一個物件轉成另一種介面需要的型別的時候,你可以做一個物件介面卡實現這個介面,這時候型別就匹配了,這個介面卡有一個被適配者的引用,介面卡裡的行為,也就是介面的行為都委託給被適配者。

使用場景:有動機地修改一個正常執行的系統的介面,這時應該考慮使用介面卡模式。

注意事項:介面卡模式不是軟體設計階段考慮的設計模式,是隨著軟體維護,由於不同產品、不同廠家造成功能類似而介面不相同情況下的解決方案。

優點: 1、可以讓任何兩個沒有關聯的類一起執行。 2、提高了類的複用。 3、增加了類的透明度。 4、靈活性好。

缺點: 1、過多地使用介面卡,會讓系統非常零亂,不易整體進行把握。比如,明明看到呼叫的是 A 介面,其實內部被適配成了 B 介面的實現。

外觀模式(Facade)

外觀模式提供了一個統一的介面,用來訪問子系統中的一群介面。外觀定義了一個高層介面,讓子系統更容易使用。外觀模式又稱為門面模式。

示意圖:

外觀模式非常簡單,體現的就是 Java 中封裝的思想。將多個子系統封裝起來,提供一個更簡潔的介面供外部呼叫。

舉個簡單例子:

每天上班都要做的事情有:

  1. 開啟瀏覽器
  2. 開啟IDE
  3. 開啟微信

下班要做的額事情有:

  1. 關瀏覽器
  2. 關IDE
  3. 關微信

用程式模擬就是:

public class Browser {
    public static void open() {
        System.out.println("開啟瀏覽器");
    }
    
    public static void close() {
        System.out.println("關閉瀏覽器");
    }
}

public class Ide {
    public static void open() {
        System.out.println("開啟IDEA");
    }
    
    public static void close() {
        System.out.println("關閉IDEA");
    }
}

public class Wechat {
    public static void open() {
        System.out.println("開啟微信");
    }
    
    public staticvoid close() {
        System.out.println("關閉微信");
    }
}

那咱們上下班就是這樣

public class Client {
    @Test
	public void test() {
        System.out.println("上班:");
        Browser.open();
        Ide.open();
        Wechat.open();
        
        System.out.println("下班:");
        Browser.close();
        Ide.close();
        Wechat.close();
    }
}
上班:
開啟瀏覽器
開啟 IDEA
開啟微信
下班:
關閉瀏覽器
關閉 IDEA
關閉微信

外觀模式給我們提供了一個更合理的介面,使得一個複雜的子系統的使用變得簡單。

public class WorkingFacade {
    public void startWork() {
        Browser.open();
        Ide.open();
        Wechat.open();
    }
    
    public void getOffWork() {
        Browser.close();
        Ide.close();
        Wechat.close();
    }
}

注意點:

  1. 雖說外觀模式“封裝”了子系統了的類,但也不是真正的封裝,外觀只是提供更直接的操作,並未將原來的子系統阻隔起來。如果需要子系統的更高層功能,還是可以使用原來的子系統的功能。這是外觀模式的一個很好的特徵:提供簡化的介面的同時,依然將系統的完整功能暴露出來,以供需要的人使用。
  2. 子系統可以有多個外觀。
  3. 設計模式之間的區別在於他們的設計意圖,不要在形式上與其他模式混淆,例如介面卡模式。

總結一下外觀模式、介面卡模式、裝飾者模式之間的特點:

  • 當需要使用一個現有的類而其介面不符合你的需要時,就是用介面卡;當需要簡化並統一一個很大的介面或者一群複雜的介面時,使用外觀
  • 介面卡改變介面以符合客戶的期望;外觀將客戶從一個複雜的子系統中解耦
  • 介面卡將一個物件包裝起來以改變其介面;裝飾者將一個物件包裝起來以增加新的行為和責任;而外觀將一群物件“包裝”起來以簡化其介面

組合模式(Composite)

組合模式允許你將物件組合成樹形結構來表現“整體/部分”的結構層次。組合可以讓客戶以一致的方式處理單個物件和組合物件。它建立了物件組的樹形結構。

只有當應用的核心模型可以表示為樹時,使用組合模式才有意義。

由組合模式定義的所有元素共享一個公共介面。使用此介面,客戶端不必擔心其使用的物件的具體類。

例子:

// TODO

UML:

優點: 1、高層模組呼叫簡單。 2、節點自由增加。

缺點:在使用組合模式時,其葉子和樹枝的宣告都是實現類,而不是介面,違反了依賴倒置原則。

使用場景:部分、整體場景,如樹形選單,檔案、資料夾的管理。

注意事項:定義時為具體類。

橋接模式(Bridge)

橋接模式:將抽象部分與它的實現部分分離,使它們都可以獨立地變化

實用場景:如果一個物件有兩種或者多種分類方式,並且兩種分類方式都容易變化。

比如說:現在有一個需求,繪製矩形、圓形、三角形這三種圖案;每種顏色都有四種不同的顏色:紅黃藍綠。顯然如果用排列組合來構建不同顏色的形狀的話,那將產生類爆炸。

橋接模式的做法是:

public interface IShape {
    void draw();
}
public class Rectangle implements IShape {
    @override
    public void draw() {
        System.out.println("繪製矩形");
    }
}
public class Round implements IShape {
    @override
    public void draw() {
        System.out.println("繪製圓形");
    }
}
public class Triangle  implements IShape {
    @override
    public void draw() {
        System.out.println("繪製三角形");
    }
}
public interface IColor {
    void color();
}

public class Red implements IColor {
    @Override
    public String getColor() {
        return "紅";
    }
}

public class Green implements IColor {
    @Override
    public String getColor() {
        return "綠";
    }
}

public class Blue implements IColor {
    @Override
    public String getColor() {
        return "藍";
    }
}

在每個形狀中橋接顏色引用:

public class Rectangle implements IShape {
    private IColor color;
    
    public void setColor(IColor color) {
        this.color = color;
    }
    
    @override
    public void draw() {
        System.out.println("繪製" + color.color() +"矩形");
    }
}
// ......

橋接模式的思想也是基於一個原則:組合優於繼承原則。

設計模式不是程式碼的結構規範,而是設計思想。

享元模式(Cache、Flyweight)

享元模式體現的是程式可複用的特點,為了節約寶貴的記憶體,程式應該儘可能地複用。

適用場景:程式需要生成數量巨大的相似物件;這將耗盡目標裝置的所有記憶體;物件間包含可共享的重複狀態。

代理模式(Proxy)

給某一個物件提供一個代理,並由代理物件控制對原物件的引用。

1.靜態代理

靜態代理很簡單,跟目標物件實現同一個介面就可以了。

比如現有個人類:

public interface IPerson {
    void eat();
}

public class Person implements IPersion {
    @override
    public void eat() {
        System.out.println("我在吃飯");
    }
}

代理類就是:

public class PersonProxy implements IPerson {
	private Person person;
    
    public PersonProxy(Person person) {
        this.person = person;
    }
    
    @override
    public void eat() {
        System.out.println("在吃飯前要先洗手");
        person.eat();
		System.out.println("吃完飯後要收拾");
    }
}

看到這個程式碼,我不禁想到他和裝飾模式的程式碼結構一模一樣。不過設計模式不能拘泥於程式碼的形式,要關注他的目的。裝飾是為了增強或者增加目標功能,代理更多的是新增控制,當然也可以說是一種增強,;兩者增強的目的不一樣。

2.動態代理(jdk內建)

這是老生常談的,實現InvocationHandler,使用JDK的Proxy類。

動態代理的優勢就是比靜態代理節省程式碼。

行為型模式

模板方法模式(Template)

模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟

例子:

public abstract class AbstractClass {
    public final void templateMethod(){
        /*
         * 呼叫基本方法,完成相關的邏輯
         */
        this.doAnything();
        this.doSomething();
    }
        protected abstract void doAnything();
    
    protected abstract void doSomething();
}

一般情況下,模板方法被申明為final。子類去實現模板方法中某個具體步驟。特點是封裝不變部分,擴充套件可變部分。

優點:1、提高程式碼的複用性。2、提高程式碼的擴充套件性。3、符合開閉原則。

缺點:1、導致類的數目增加。2、間接地增加了系統實現的複雜度。3、繼承關係存在自身缺點,如果父類新增了新的抽象方法,所有子類都需要重新改一遍。

策略模式(Strategy)

策略模式指物件有某個行為,但是在不同的場景中,該行為有不同的實現演算法,這組演算法可以在執行時互相代替。

主要解決:在有多種演算法相似的情況下,使用 if... else 所帶來的複雜和難以維護。

使用場景: 1、如果在一個系統裡面有許多類,它們之間的區別僅在於它們的行為,那麼使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。2、一個系統需要動態地在幾種演算法中選擇一種。 3、如果一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。

注意事項:如果一個系統的策略多於四個,就需要考慮使用混合模式,解決策略類膨脹的問題。

UML:

舉例:

加入現在模擬一下游戲角色撿到不同武器就有不同的攻擊方式的場景:

// 角色有名稱和打鬥行為
// 打鬥行為根據武器不同而改變
public class Role {
    private String name;
    private WeaponStrategy weapon;
    
    public Role(String name) {
        this.name = name;
    }
    
    public void fight() {
        weapon.attack();
    }
    
    public void setWeapon(WeaponStrategy weapon) {
        this.weapon = weapon;
    }
}

public interface WeaponStrategy {
    void attack();
}

public class Sword implements WeaponStrategy {
    @override
    public void attack() {
        System.out.println("用利劍刺");
    }
}

public class Axe implements WeaponStrategy {
    @override
    public void attack() {
        System.out.println("用斧頭砍");
    }
}

public class Gun implements WeaponStrategy {
    @override
    public void attack() {
        System.out.println("用機槍射");
    }
}

測試一下:

public class Client {
    public static void main(String[] args) {
        Role role = new Role("銳雯");
        
        // 撿到劍的事件
        role.setWeapon(new Sword());
        role.attack();
        
        // 撿到斧子的事件
        role.setWeapon(new Axe());
        role.attack();
       
        // 撿到機槍的事件
        role.setWeapon(new Gun());
        role.attack();
	}
}

這就是在執行時改變策略。雖然策略模式為了解決多重if else的難以維護,但是何時選擇哪種策略模式依然需要判斷,這是目前比較困惑的一點,先留著,後續解決吧......

觀察者模式(Observer)

觀察者模式定義了物件之間的一對多依賴,當一個物件改變狀態時,它的所有依賴者都會受到通知並自動更新。

舉一個氣象站與展示牌之間的例子:氣象站已有資料更新,不同的展示牌就要實時更新資料。展示牌需要訂閱氣象站才可以受到通知,當然,也可以取消訂閱。他們之間有四個角色:Subject、Concrete Subject、Observer、Concrete Observer。

package com.learning.designpattern;

import java.util.*;

interface Subject {
    void register(Observer o);
    void remove(Observer o);
    void notifyObservers();
}

interface Observer {
    void update(DisplayData displayData);
}

interface DisplayElements {
    void display();
}

// 展示牌首先是觀察者,其次他們有共同的功能:就是展示資訊
class CurrentConditionDisplay implements Observer, DisplayElements  {
    private DisplayData displayData;
    private WeatherData weatherData;

    public CurrentConditionDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
    }

    @Override
    public void update(DisplayData displayData) {
        this.displayData = displayData;
        display();
    }

    @Override
    public void display() {
        System.out.println("CurrentConditionDisplay:" + displayData.toString());
    }
}

class StatisticDisplay implements DisplayElements, Observer {
    private DisplayData displayData;
    private WeatherData weatherData;

    public StatisticDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
    }

    @Override
    public void update(DisplayData displayData) {
        this.displayData = displayData;
        display();
    }

    @Override
    public void display() {
        System.out.println("StatisticDisplay:" + displayData.toString());
    }
}

class DisplayData {
    private float temp;
    private float humidity;
    private float pressure;
    
    // constructor and setters and getters...
}

class WeatherData implements Subject {
    Set<Observer> observers = new HashSet<>();
    private DisplayData displayData;

    @Override
    public void register(Observer o) {
        observers.add(o);
    }

    @Override
    public void remove(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        observers.forEach(o -> o.update(displayData));
    }

    public void dataChanged() {
        notifyObservers();
    }

    public void setDisplayData(DisplayData displayData) {
        this.displayData = displayData;
    }

    public DisplayData getDisplayData() {
        return displayData;
    }
}

public class ObserverClientTest {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        weatherData.register(new CurrentConditionDisplay(weatherData));
        weatherData.register(new StatisticDisplay(weatherData));
        weatherData.setDisplayData(new DisplayData(21, 30, 40));
        
        weatherData.notifyObservers();
    }
}

UML:

JDK有內建的觀察者模式,在java.util包下,有一個Observer介面,和一個Observable類,Observable是一個類,他就是主題,Observer就是介面。如果我們要複用JDK的觀察者介面的話,我們具體主題類繼承Observable就可以了。

java.util.Observable既然是一個類,那麼也很明顯,就是擴充套件性不夠好。

可觀察者(Observable)和觀察者(Observer)之間用松耦合的方式結合,可觀察者不知道觀察者的細節,只知道觀察者實現觀察者介面。觀察者應用廣泛,例如監聽器,MVC都是觀察者模式的思想。

狀態模式(State)

允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類。

UML:

優點: 1、封裝了轉換規則。2、將所有與某個狀態有關的行為放到一個類中,這讓新增一個狀態變得方便,只需要改變物件狀態即可改變物件的行為,讓維護變得方便。3、他是if else語句塊的替代。

缺點: 1、必然會增加系統類和物件的個數

使用場景: 1、行為隨狀態改變而改變的場景。 2、條件、分支語句的代替者。

直譯器模式(Interpreter)

給定一門語言,定義它的文法的一種表示,並定義一個直譯器,該直譯器使用該表示來解釋語言中的句子

這個就不看了,設計語言用的,瞭解下概念,用不到。

迭代器模式(Iterator)

提供一種方法順序的訪問一個聚合物件中的各個元素,而不暴露其內部的表示。

簡單點就是,不管啥集合,定義了一種遍歷的標準,而不管集合內部如何實現這個標準的。

JDK已經幫我們實現了,不用自己寫了,用就行了。

中介者模式(Mediator)

定義一箇中介物件來封裝一系列物件之間的互動,使原有物件之間的耦合鬆散,且可以獨立地改變它們之間的互動

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

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

使用場景:

  • 當一些物件和其他物件緊密耦合以致難以對其進行修改時
  • 當元件因過於依賴其他元件而無法在不同應用中複用時
  • 如果為了能在不同情景下複用一些基本行為,導致你需要被迫建立大量元件子類時

優點:單一職責;開閉原則,無需修改其他元件就就能加入新的元件;降低這元件之間的耦合;方便複用。

缺點:中介者類由於職責眾多,可能變成一個超級類。

例子,模擬一下群聊中聊天室與使用者,使用者將要傳送的訊息交給聊天室顯示出來。

public class ChatRoom {
    
   public void showMessage(User user, String message){
      System.out.println(new Date().toString()
         + " [" + user.getName() +"] : " + message);
   }
}

public class User {
   private String name;
   private ChatRoom chatRoom;
 
   public User(String name){
      this.name  = name;
   }
 
   public void setChatRoom(ChatRoom chatRoom) {
       this.chatRoom = chatRoom;
   }
    
   public void sendMessage(String message){
      chatRoom.showMessage(this, message);
   }
}

元件儲存對於中介者物件的引用。

備忘錄模式(Snapshot、Memento)

在不破壞封裝的條件下(不暴露物件實現細節),透過備忘錄物件儲存另外一個物件內部狀態的快照,在將來合適的時候把這個物件還原到儲存起來的狀態。

備忘錄模式(Memento Pattern)又叫做快照模式(Snapshot Pattern)或Token模式,

優點:提供了一種可恢復的機制;實現了封裝,

缺點:消耗資源,如果類的成員變數過多,儲存勢必消耗一定的記憶體。

總體而言,備忘錄模式是利大於弊的。

備忘錄模式使用三個類Originator (源物件)、Memento(備忘錄物件)、和 CareTaker(管理者)。

Originator 提供一個備忘的方法,決定哪些屬性是可以備忘的;CareTaker負責管理備忘錄物件與源物件;

public class Originator {
   private String state;
 
   public void setState(String state){
      this.state = state;
   }
 
   public String getState(){
      return state;
   }
 
   public Memento saveStateToMemento(){
      return new Memento(state);
   }
 
   public void getStateFromMemento(Memento Memento){
      state = Memento.getState();
   }
}

public class Memento {
   private String state;
 
   public Memento(String state){
      this.state = state;
   }
 
   public String getState(){
      return state;
   }  
}

public class CareTaker {
   private List<Memento> mementoList = new ArrayList<Memento>();
 
   public void add(Memento state){
      mementoList.add(state);
   }
 
   public Memento get(int index){
      return mementoList.get(index);
   }
}

命令模式(Command)

命令模式將“請求”封裝成物件,以便使用不同的請求、佇列或者日誌來引數化其他物件。命令模式也支援撤銷的操作。

命令模式將發出請求的物件與接收請求的物件解耦。在被解耦的兩者之間是透過命令物件進行溝通的。命令物件封裝了接受者的一個或多個物件。

宏命令是以命令的一種簡單延伸,允許呼叫多個命令。宏方法也支援撤銷。實際操作時,也有可能命令物件直接實現了請求,而不是將工作委託給接受者。

角色:

  • ICommand:命令介面,一般會定義一個execute方法,如果需要也會有undo(撤銷)方法。
  • ConcreteCommand:具體命令
  • Receiver:命令的接受者,就是執行者
  • Invoker:命令的呼叫者,命令入口

使用場景:認為是命令的地方都可以使用命令模式,比如: 1、GUI 中每一個按鈕都是一條命令。 2、模擬 CMD。

模擬一下,遙控器按下開關按鈕控制燈的開關。

燈(Receiver):

public class Light {
    
    public void on() {
        System.out.println("燈亮了");
    }
    
    public void off() {
        System.out.println("燈關了");
    }
}

開關燈命令(Command):

public interface ICommand {
	void execute();
}

public class LightOnCmd implements ICommand {
    private Light light;
    
	public LightOnCmd(Light light) {
        this.light = light;
    }
    
    @override
    public void execute() {
        light.on();
	}
}

public class LightOffCmd implements ICommand {
    private Light light;
    
	public LightOnCmd(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        light.off();
	}
}

遙控器(Invoker):

public class RemoteControl {
    public ICommand cmd;
    
    public void setCmd(ICommand cmd) {
        this.cmd = cmd;
    }
    
    public void lightOnPressed() {
        System.out.println("按下了開燈按鈕");
        cmd.execute();
	}
    
    public void lightOffPressed() {
        System.out.println("按下了關燈按鈕");
        cmd.execute();
	}
}

客戶端測試(Client):

public class Client {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        Light light = new Light();

        remoteControl.setCmd(new LightOnCmd(light));
        remoteControl.lightOnPressed();

        remoteControl.setCmd(new LightOffCmd(light));
        remoteControl.lightOffPressed();
    }
}

輸出:

按下了開燈按鈕
燈亮了
按下了關燈按鈕
燈關了

這一塊,還是看下Head First那裡的例子,講的很好。

優點:解耦。符合開閉原則,很方便增加新的的命令。

缺點:可能會產生很多命令類。

責任鏈模式(Chain of Responsibility)

責任鏈模式(Chain of Responsibility)使多個物件都有機會處理請求,從而避免請求的傳送者和接受者之間的耦合關係。將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有物件能夠處理它。

你要去給某公司借款 1 萬元,當你來到櫃檯的時候向櫃員發起 "借款 1 萬元" 的請求時,櫃員認為金額太多,處理不了這樣的請求,他轉交這個請求給他的組長,組長也處理不了這樣的請求,那麼他接著向經理轉交這樣的請求。

簡單java程式碼:

public void test(Request request) {
    int money = request.getRequestMoney();
    if(money <= 1000) {
        Clerk.response(request);	
    } else if(money <= 5000) {
        Leader.response(request);
    } else if(money <= 10000) {
        Manager.response(request);
    }
}

程式碼的業務邏輯就是這樣:根據的借款金額來判定誰來處理這個借款請求 (request)

  • 如果請求借款金額小於 1000 元,那麼櫃檯職員就可以直接處理這個請求(比如簽字)
  • 如果請求借款金額小於 5000 元但大於 1000 元,那麼職員處理不了,該請求轉交給組長,組長能夠處理這樣的請求(比如簽字)
  • 如果請求借款金額大於 5000 元但小於 10000 元,那麼職員和組長都處理不了(沒有許可權),那麼這個請求就會轉交給經理,經理能夠處理這樣的請求(比如簽字)

但是這樣的程式碼缺點很明顯:

  1. 程式碼臃腫,不優雅
  2. 耦合度高,新的請求類必然伴隨新的if else,違反開閉原則

責任鏈模式:

UML:

定義一個請求的等級:

public class Level {
    private int level = 0;
    
    public Level(int level) {
        this.level = level;
    }
    
    public int getLevel() {
        return level;
    }
    
    // 判斷請求等級,等級高的可以處理請求,反之不行
    public boolean above(Level level) {
   		return this.level >= level.getLevel();
    }
}

請求與相應:

//請求
class Request {
    Level level;
    public Request(Level level) {
        System.out.println("開始請求...");
        this.level = level;
    }
    public Level getLevel() {
        return level;
    }
}

//響應
class Response {
    private String message;
    public Response(String message) {
        System.out.println("處理完請求");
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

抽象處理類和具體處理類:

//抽象處理器
abstract class Handler {
    private Handler nextHandler;
    
    public void setNextHandler(Handler handler) {
        nextHandler = handler;
    }
    
    public final Response handlerRequest(Request request) {
        Response response = null;
        if (this.getHandlerLevel().above(request.getLevel())) {
            response = this.response(request);
        } else {
            if (nextHandler != null) {
                response = this.nextHandler.handlerRequest(request);
            } else {
                System.out.println("沒有合適的處理器處理該請求...");
            }
        }
        
        return response;
    }
    
    protected abstract Level getHandlerLevel();
    
    public abstract Response response(Request request); 
}
//具體的處理器 1
class ConcreteHandler1 extends Handler {
    protected Level getHandlerLevel() {
        return new Level(1);
    }
    public Response response(Request request) {
        System.out.println("該請求由 ConcreteHandler1 處理");
        return new Response("響應結果 1");
    }
}

//具體的處理器 2
class ConcreteHandler2 extends Handler {
    protected Level getHandlerLevel() {
        return new Level(2);
    }
    public Response response(Request request) {
        System.out.println("該請求由 ConcreteHandler2 處理");
        return new Response("響應結果 2");
    }
}

//具體的處理器 3
class ConcreteHandler3 extends Handler {
    protected Level getHandlerLevel() {
        return new Level(3);
    }
    public Response response(Request request) {
        System.out.println("該請求由 ConcreteHandler3 處理");
        return new Response("響應結果 3");
    }
}

客戶端測試:

public class Client {
    public static void main(String[] args) {
        Handler ch1 = new ConcreteHandler1();
        Handler ch2 = new ConcreteHandler2();
        Handler ch3 = new ConcreteHandler3();

        ch1.setNextHandler(ch2);
        ch2.setNextHandler(ch3);

        Response res1 = ch1.handlerRequest(new Request(new Level(2)));
        if (res1 != null) {
            System.out.println(res1.getMessage());
		}
        Response res2 = ch1.handlerRequest(new Request(new Level(4)));
        if (res2 != null) {
            System.out.println(res2.getMessage());
        }
    } 
}

優點:與 if...else 相比,他的耦合性要低一些,因為它將條件判定分散到各個處理類中,並且這些處理類的優先處理順序可以隨意的設定,並且如果想要新增新的 handler 類也是十分簡單的,這符合開放閉合原則

缺點:不能保證請求一定被接受;可能不容易觀察執行時的特徵,對調錯不友好

注意事項:責任鏈模式帶來了靈活性,但是在設定處理類前後關係時,一定要避免在鏈中出現迴圈引用的問題。

訪問者模式(Visitor)

據說是最複雜的設計模式,先不管了吧。

相關文章