【深入設計模式】裝飾模式—什麼是裝飾模式?裝飾模式在原始碼中的應用

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

我們使用的手機最開始只能打電話、發簡訊,後來又來彩屏之後手機開始能夠拍照、錄影,再後來發展到智慧機時代,不僅能夠通話、發簡訊、拍照錄像,還能夠安裝 APP、影片通話、玩遊戲等等新功能,手機的迭代更新總是在原有功能的基礎上進行擴充套件豐富。在開發中我們也經常會遇到這樣的需求,保留現有功能,並在其基礎上進行擴充套件,同時還不能影響到已有功能,這個時候就需要使用到這裡所講的裝飾模式。

1333514.gif

1. 裝飾模式

1.1 裝飾模式簡介

當我們需要在現有功能之上對其進行擴充套件時,可能首先想到的是使用子類的方式完成,如果我們使用子類進行擴充套件會出現很多的子類,並且在呼叫方使用這些子類時也會構建很多次,同時這個呼叫的過程也開放給了呼叫者,那麼如果這些擴充套件類的呼叫在業務上存在順序,就可能造成業務執行混亂的情況。這個時候我們可以考慮使用裝飾模式來處理這樣的問題。

裝飾模式能夠在不改變原來物件結構和功能的前提下,動態的給物件增加新的功能,相比於使用子類的擴充套件的方式,裝飾模式更加的靈活。

1.2 裝飾模式結構

裝飾模式需要涉及到以下幾個角色:

  • 抽象元件(Component):定義一個系列物件應該有的功能,今後的擴充套件呼叫均基於該介面實現
  • 具體元件(SpecificComponent):抽象元件的具體實現,裝飾類將對該具體實現進行擴充套件
  • 抽象裝飾類(Decorator):裝飾類將通過繼承該類來進行功能擴充套件,並且在該類中包含具體元件例項
  • 具體裝飾類(DecorateComponent):實現抽象裝飾類的具體裝飾類,用於對抽象裝飾類中的具體元件功能呼叫的擴充套件

結構圖如下:

image-20220625213854540.png

```java // 抽象元件 public interface Component { void operation(); }

//具體元件 public class SpecificComponent implements Component { @Override public void operation() { System.out.println("component base function."); } }

// 抽象裝飾類,實現抽象元件 public abstract class Decorator implements Component { private Component component;

public Decorator(Component component) {
    this.component = component;
}

@Override
public void operation() {
    component.operation();
}

}

// 具體裝飾類 1,繼承抽象裝飾類 public class DecorateComponent1 extends Decorator{ public DecorateComponent1(Component component) { super(component); }

@Override
public void operation() {
    System.out.println("operation before base function.");
    super.operation();
    System.out.println("operation after base function.");
}

}

// 具體裝飾類 2,繼承抽象裝飾類 public class DecorateComponent2 extends Decorator { public DecorateComponent2(Component component) { super(component); }

@Override
public void operation() {
    System.out.println("operation before base function.");
    super.operation();
    System.out.println("operation after base function.");
}

} ```

呼叫裝飾模式的程式碼如下:

```java public static void main(String[] args) { SpecificComponent component = new SpecificComponent(); component.operation();

System.out.println("==================");
System.out.println("running decorator1.");
DecorateComponent1 decorator1 = new DecorateComponent1(component);
decorator1.operation();

System.out.println("==================");
System.out.println("running decorator2.");
DecorateComponent2 decorator2 = new DecorateComponent2(component);
decorator2.operation();

} ```

控制檯輸出如下:

component base function. ================== running decorator1. operation before base function. component base function. operation after base function. ================== running decorator2. operation before base function. component base function. operation after base function.

我們可以看到示例程式碼中,首先建立 SpecificComponent 並呼叫 operation() 方法,此時執行基本功能。然後我們再將 SpecificComponent 傳入兩個裝飾類 DecorateComponent1 和 DecorateComponent2 並呼叫這兩個物件的 operation() 方法,可以看到不僅執行了基本功能程式碼,還可以在基本功能程式碼的前後新增新的功能,從而實現在不改變現有功能的基礎上對其進行增強。在平時開發中,往往會將抽象元件去除,讓抽象裝飾類直接繼承具體元件,從而簡化程式碼的結構。

1655826890701.png

1.3 裝飾模式示例

我們以手機的發展為例進行,首先我們從最原始的大哥大開始發展,這個大哥大就只有通話的功能,隨著時代的進步大哥大也進化成了小型手機,這時的手機能夠打電話、發簡訊。只有進入到了智慧手機時代,這個時候的手機能夠安裝 app 、拍照、影片聊天等等功能。雖然手機在二十多年的時間裡瘋狂發展,功能也變得花裡胡哨,但是其本質永遠都是手機。所以在這個場景下我們可以用裝飾模式,在手機這個元件的基礎上對其進行功能擴充套件,程式碼如下:

```java // 基本元件 Phone public interface Phone { // 這個電話具有的功能 void functions(); } // 具體元件 MobilePhone,實現基本元件的功能即可以打電話 public class MobilePhone implements Phone { @Override public void functions() { System.out.println("this phone can make calls. "); } } // 抽象裝飾器 public abstract class PhoneDecorator implements Phone { Phone phone;

public PhoneDecorator(Phone phone) {
    this.phone = phone;
}

@Override
public void functions() {
    phone.functions();
}

} // 普通手機,只能夠打電話和發簡訊 public class OrdinaryMobilePhone extends PhoneDecorator{ public OrdinaryMobilePhone(Phone phone) { super(phone); }

@Override
public void functions() {
    super.functions();
    System.out.println("this phone can send message. ");
}

} // 智慧手機,擁有上一代的功能,並且還能夠安裝 app、拍照 public class IntelligentMobilePhone extends PhoneDecorator {

public IntelligentMobilePhone(Phone phone) {
    super(phone);
}

@Override
public void functions() {
    super.functions();
    System.out.println("this phone can install apps. ");
    System.out.println("this phone can take photos. ");
}

} ```

在這段程式碼中我們對上面的手機發展進行模擬,首先定義一個基本的元件 Phone,表明這是一個手機的發展史,然後建立具體元件 MobilePhone,它的功能只有一個——打電話。再定義一個抽象裝飾類 PhoneDecorator,最後再抽象裝飾類基礎上定義兩個具體裝飾類——普通手機(OrdinaryMobilePhone)和智慧手機(IntelligentMobilePhone),普通手機不光有其上一代的功能,還能夠發簡訊,智慧手機更強大了,能夠裝 app、拍照等功能。接下來對這些類進行呼叫,程式碼如下:

java public static void main(String[] args) { // 第一代手機的功能 System.out.println("=========first generation=========="); MobilePhone mobilePhone = new MobilePhone(); mobilePhone.functions(); // 第二代手機的功能 System.out.println("=========second generation=========="); OrdinaryMobilePhone ordinaryMobilePhone = new OrdinaryMobilePhone(mobilePhone); ordinaryMobilePhone.functions(); System.out.println("=========third generation=========="); // 第三代手機的功能 IntelligentMobilePhone intelligentMobilePhone = new IntelligentMobilePhone(ordinaryMobilePhone); intelligentMobilePhone.functions(); }

控制檯輸出如下:

=========first generation========== this phone can make calls. =========second generation========== this phone can make calls. this phone can send message. =========third generation========== this phone can make calls. this phone can send message. this phone can install apps. this phone can take photos.

可以看到我們把一二三代的手機功能都列印了出來。在呼叫的程式碼裡,首先是建立了具體元件物件 mobilePhone,然後再將 mobilePhone 作為引數傳給了普通手機的建構函式構造 ordinaryMobilePhone 物件,這樣 ordinaryMobilePhone 物件就能夠擁有 mobilePhone 的功能,同時還有自身程式碼裡的傳送簡訊功能。最後我們將 ordinaryMobilePhone 物件作為引數傳入智慧手機建構函式中構造 intelligentMobilePhone 物件,此時的 intelligentMobilePhone 物件不僅擁有 mobilePhone 和 ordinaryMobilePhone 物件的功能,同時還有自身的安裝 app、拍照功能。

618.jpg

2. 裝飾模式在原始碼中的應用

2.1 裝飾模式在 JDK 中的應用

裝飾模式在 JDK 中的應用最著名的就是 IO 流了,下面就是我們常用的 InputStream 類和InputStreamReader 類體系。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-jgNDH7vB-1656341939501)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220625225754032.png)]

在 InputStream 體系中,讀取流的基本方式是使用 InputStream 類來一個一個地讀取位元組檔案,在 InputStream 讀取檔案的基礎上衍生出了FileInputStream、ByteArrayInputStream 等,使其不僅能夠讀取位元組,還能夠讀取位元組陣列、檔案等。

使用 InputStream 讀取檔案多少有點麻煩了,在 Reader 體系中,有 InputStreamReader 這個類,將 InputStream 作為引數傳入 Reader 的建構函式,Reader 的具體裝飾類便能對讀進行增強,從而讀取具體傳入的 InputStream。

2.2 裝飾模式在 MyBatis中的應用

在 Mybatis 原始碼中有一個類叫 TransactionalCache,該類實現了 Cache 介面,而該類構造方法中將 Cache 物件作為引數進行構造,程式碼如下:

```java public class TransactionalCache implements Cache {

private static final Log log = LogFactory.getLog(TransactionalCache.class);

private final Cache delegate; private boolean clearOnCommit; private final Map entriesToAddOnCommit; private final Set entriesMissedInCache;

public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<>(); this.entriesMissedInCache = new HashSet<>(); } …… } ```

TransactionalCache 類中同時實現了 Cache 的方法,於是在 getObject 這個方法中,我們可以看到首先呼叫了傳入的 Cache 物件的 getObject 物件,然後對沒獲取到物件時進行處理。這就是 MyBatis 的二級快取的事務緩衝區,使用了裝飾模式對 Cache 的方法進行了增強,使其能夠滿足事務相關快取功能需求。

java public Object getObject(Object key) { // issue #116 Object object = delegate.getObject(key); if (object == null) { entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } }

3. 總結

裝飾模式能夠動態的對已有功能新增新功能,開發中如果我們需要對現有功能進行增強時,可以考慮使用裝飾模式。在對現有功能進行新增新功能時,如果在原始碼上面進行修改,一方面增加程式碼邏輯複雜度,另一方面會影響到已使用到這部分功能的業務,使用裝飾模式可以在新的裝飾類中新增新的變數、新邏輯及新方法,並且不會對現有部分邏輯造成影響,這樣將功能的核心職責與額外新增職責進行分離。在使用時也只需將需要增強的核心功能作為引數傳入裝飾類中,便能夠呼叫到核心功能與裝飾功能。

1656341651185.png