設計模式 | 裝飾者模式及典型應用

小旋鋒發表於2018-09-18

前言

本文的主要內容:

  • 介紹裝飾者模式
  • 示例
  • 原始碼分析裝飾者模式的典型應用
    • Java I/O 中的裝飾者模式
    • spring session 中的裝飾者模式
    • Mybatis 快取中的裝飾者模式
  • 總結

裝飾者模式

裝飾者模式(Decorator Pattern):動態地給一個物件增加一些額外的職責,增加物件功能來說,裝飾模式比生成子類實現更為靈活。裝飾模式是一種物件結構型模式。

在裝飾者模式中,為了讓系統具有更好的靈活性和可擴充套件性,我們通常會定義一個抽象裝飾類,而將具體的裝飾類作為它的子類

角色

Component(抽象構件):它是具體構件和抽象裝飾類的共同父類,宣告瞭在具體構件中實現的業務方法,它的引入可以使客戶端以一致的方式處理未被裝飾的物件以及裝飾之後的物件,實現客戶端的透明操作。

ConcreteComponent(具體構件):它是抽象構件類的子類,用於定義具體的構件物件,實現了在抽象構件中宣告的方法,裝飾器可以給它增加額外的職責(方法)。

Decorator(抽象裝飾類):它也是抽象構件類的子類,用於給具體構件增加職責,但是具體職責在其子類中實現。它維護一個指向抽象構件物件的引用,通過該引用可以呼叫裝飾之前構件物件的方法,並通過其子類擴充套件該方法,以達到裝飾的目的。

ConcreteDecorator(具體裝飾類):它是抽象裝飾類的子類,負責向構件新增新的職責。每一個具體裝飾類都定義了一些新的行為,它可以呼叫在抽象裝飾類中定義的方法,並可以增加新的方法用以擴充物件的行為。

由於具體構件類和裝飾類都實現了相同的抽象構件介面,因此裝飾模式以對客戶透明的方式動態地給一個物件附加上更多的責任,換言之,客戶端並不會覺得物件在裝飾前和裝飾後有什麼不同。裝飾模式可以在不需要創造更多子類的情況下,將物件的功能加以擴充套件。

裝飾模式的核心在於抽象裝飾類的設計

示例

煎餅抽象類

public abstract class ABattercake {
    protected abstract String getDesc();
    protected abstract int cost();
}
複製程式碼

煎餅類,繼承了煎餅抽象類,一個煎餅 8 塊錢

public class Battercake extends ABattercake {
    @Override
    protected String getDesc() {
        return "煎餅";
    }
    @Override
    protected int cost() {
        return 8;
    }
}
複製程式碼

抽象裝飾類,需要注意的是,抽象裝飾類通過成員屬性的方式將 煎餅抽象類組合進來,同時也繼承了煎餅抽象類,且這裡定義了新的業務方法 doSomething()

public abstract class AbstractDecorator extends ABattercake {
    private ABattercake aBattercake;

    public AbstractDecorator(ABattercake aBattercake) {
        this.aBattercake = aBattercake;
    }
    
    protected abstract void doSomething();

    @Override
    protected String getDesc() {
        return this.aBattercake.getDesc();
    }
    @Override
    protected int cost() {
        return this.aBattercake.cost();
    }
}
複製程式碼

雞蛋裝飾器,繼承了抽象裝飾類,雞蛋裝飾器在父類的基礎上增加了一個雞蛋,同時價格加上 1 塊錢

public class EggDecorator extends AbstractDecorator {
    public EggDecorator(ABattercake aBattercake) {
        super(aBattercake);
    }

    @Override
    protected void doSomething() {

    }

    @Override
    protected String getDesc() {
        return super.getDesc() + " 加一個雞蛋";
    }

    @Override
    protected int cost() {
        return super.cost() + 1;
    }
    
    public void egg() {
        System.out.println("增加了一個雞蛋");
    }
}
複製程式碼

香腸裝飾器,與雞蛋裝飾器類似,繼承了抽象裝飾類,給在父類的基礎上加上一根香腸,同時價格增加 2 塊錢

public class SausageDecorator extends AbstractDecorator{
    public SausageDecorator(ABattercake aBattercake) {
        super(aBattercake);
    }
    @Override
    protected void doSomething() {

    }

    @Override
    protected String getDesc() {
        return super.getDesc() + " 加一根香腸";
    }
    @Override
    protected int cost() {
        return super.cost() + 2;
    }
}
複製程式碼

測試,購買煎餅

1、購買一個煎餅

public class Test {
    public static void main(String[] args) {
        ABattercake aBattercake = new Battercake();
        System.out.println(aBattercake.getDesc() + ", 銷售價格: " + aBattercake.cost());
    }
}
複製程式碼

輸出

煎餅, 銷售價格: 8
複製程式碼

2、購買一個加雞蛋的煎餅

public class Test {
    public static void main(String[] args) {
        ABattercake aBattercake = new Battercake();
        aBattercake = new EggDecorator(aBattercake);
        System.out.println(aBattercake.getDesc() + ", 銷售價格: " + aBattercake.cost());
    }
}
複製程式碼

輸出

煎餅 加一個雞蛋, 銷售價格: 9
複製程式碼

3、購買一個加兩個雞蛋的煎餅

public class Test {
    public static void main(String[] args) {
        ABattercake aBattercake = new Battercake();
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new EggDecorator(aBattercake);
        System.out.println(aBattercake.getDesc() + ", 銷售價格: " + aBattercake.cost());
    }
}
複製程式碼

輸出

煎餅 加一個雞蛋 加一個雞蛋, 銷售價格: 10
複製程式碼

4、購買一個加兩個雞蛋和一根香腸的煎餅

public class Test {
    public static void main(String[] args) {
        ABattercake aBattercake = new Battercake();
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new SausageDecorator(aBattercake);
        System.out.println(aBattercake.getDesc() + ", 銷售價格: " + aBattercake.cost());
    }
}
複製程式碼

輸出

煎餅 加一個雞蛋 加一個雞蛋 加一根香腸, 銷售價格: 12
複製程式碼

畫出UML類圖如下所示

裝飾者模式類圖

小結一下

由於具體構件類和裝飾類都實現了相同的抽象構件介面,因此裝飾模式以對客戶透明的方式動態地給一個物件附加上更多的責任,換言之,客戶端並不會覺得物件在裝飾前和裝飾後有什麼不同。

譬如我們給煎餅加上一個雞蛋可以這麼寫 aBattercake = new EggDecorator(aBattercake);,客戶端仍然可以把 aBattercake 當成原來的 aBattercake一樣,不過現在的 aBattercake已經被裝飾加上了雞蛋

裝飾模式可以在不需要創造更多子類的情況下,將物件的功能加以擴充套件。

透明裝飾模式與半透明裝飾模式

在上面的示例中,裝飾後的物件是通過抽象構建類型別 ABattercake 的變數來引用的,在雞蛋裝飾器這個類中我們新增了 egg() 方法,如果此時我們想要單獨呼叫該方法是呼叫不到的

除非引用變數的型別改為 EggDecorator,這樣就可以呼叫了

EggDecorator eggBattercake = new EggDecorator(aBattercake); 
eggBattercake.egg();
複製程式碼

在實際使用過程中,由於新增行為可能需要單獨呼叫,因此這種形式的裝飾模式也經常出現,這種裝飾模式被稱為半透明(Semi-transparent)裝飾模式,而標準的裝飾模式是透明(Transparent)裝飾模式

(1) 透明裝飾模式

在透明裝飾模式中,要求客戶端完全針對抽象程式設計,裝飾模式的透明性要求客戶端程式不應該將物件宣告為具體構件型別或具體裝飾型別,而應該全部宣告為抽象構件型別。

(2) 半透明裝飾模式

透明裝飾模式的設計難度較大,而且有時我們需要單獨呼叫新增的業務方法。為了能夠呼叫到新增方法,我們不得不用具體裝飾型別來定義裝飾之後的物件,而具體構件型別還是可以使用抽象構件型別來定義,這種裝飾模式即為半透明裝飾模式。

半透明裝飾模式可以給系統帶來更多的靈活性,設計相對簡單,使用起來也非常方便;但是其最大的缺點在於不能實現對同一個物件的多次裝飾,而且客戶端需要有區別地對待裝飾之前的物件和裝飾之後的物件。

裝飾模式注意事項

(1) 儘量保持裝飾類的介面與被裝飾類的介面相同,這樣,對於客戶端而言,無論是裝飾之前的物件還是裝飾之後的物件都可以一致對待。這也就是說,在可能的情況下,我們應該儘量使用透明裝飾模式。

(2) 儘量保持具體構件類是一個“輕”類,也就是說不要把太多的行為放在具體構件類中,我們可以通過裝飾類對其進行擴充套件。

(3) 如果只有一個具體構件類,那麼抽象裝飾類可以作為該具體構件類的直接子類。

原始碼分析裝飾者模式的典型應用

Java I/O中的裝飾者模式

使用 Java I/O 的時候總是有各種輸入流、輸出流、字元流、位元組流、過濾流、緩衝流等等各種各樣的流,不熟悉裡邊的設計模式的話總會看得雲裡霧裡的,現在通過設計模式的角度來看 Java I/O,會好理解很多。

先用一幅圖來看看Java I/O到底是什麼,下面的這幅圖生動的刻畫了Java I/O的作用。

Java I/O的作用圖

由上圖可知在Java中應用程式通過輸入流(InputStream)的Read方法從源地址處讀取位元組,然後通過輸出流(OutputStream)的Write方法將流寫入到目的地址。

流的來源主要有三種:本地的檔案(File)、控制檯、通過socket實現的網路通訊

下面的圖可以看出Java中的裝飾者類和被裝飾者類以及它們之間的關係,這裡只列出了InputStream中的關係:

InputStream部分類關係

由上圖可以看出只要繼承了FilterInputStream的類就是裝飾者類,可以用於包裝其他的流,裝飾者類還可以對裝飾者和類進行再包裝。

這裡總結幾種常用流的應用場景

流名稱 應用場景
ByteArrayInputStream 訪問陣列,把記憶體中的一個緩衝區作為 InputStream 使用,CPU從快取區讀取資料比從儲存介質的速率快10倍以上
StringBufferInputStream 把一個 String 物件作為。InputStream。不建議使用,在轉換字元的問題上有缺陷
FileInputStream 訪問檔案,把一個檔案作為 InputStream ,實現對檔案的讀取操作
PipedInputStream 訪問管道,主要線上程中使用,一個執行緒通過管道輸出流傳送資料,而另一個執行緒通過管道輸入流讀取資料,這樣可實現兩個執行緒間的通訊
SequenceInputStream 把多個 InputStream 合併為一個 InputStream . “序列輸入流”類允許應用程式把幾個輸入流連續地合併起來
DataInputStream 特殊流,讀各種基本型別資料,如byte、int、String的功能
ObjectInputStream 物件流,讀物件的功能
PushBackInputStream 推回輸入流,可以把讀取進來的某些資料重新回退到輸入流的緩衝區之中
BufferedInputStream 緩衝流,增加了緩衝功能

下面看一下Java中包裝流的例項

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class StreamDemo {
    public static void main(String[] args) throws IOException{
        DataInputStream in=new DataInputStream(new BufferedInputStream(new  FileInputStream("D:\\hello.txt")));
        while(in.available()!=0) {
            System.out.print((char)in.readByte());
        }
        in.close();
    }
}
複製程式碼

輸出結果

hello world!
hello Java I/O!
複製程式碼

上面程式中對流進行了兩次包裝,先用 BufferedInputStream將FileInputStream包裝成緩衝流也就是給FileInputStream增加緩衝功能,再DataInputStream進一步包裝方便資料處理。

如果要實現一個自己的包裝流,根據上面的類圖,需要繼承抽象裝飾類 FilterInputStream

譬如來實現這樣一個操作的裝飾者類:將輸入流中的所有小寫字母變成大寫字母

import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

public class UpperCaseInputStream extends FilterInputStream {
    protected UpperCaseInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        int c = super.read();
        return (c == -1 ? c : Character.toUpperCase(c));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result = super.read(b, off, len);
        for (int i = off; i < off + result; i++) {
            b[i] = (byte) Character.toUpperCase((char) b[i]);
        }
        return result;
    }

    public static void main(String[] args) throws IOException {
        int c;
        InputStream in = new UpperCaseInputStream(new FileInputStream("D:\\hello.txt"));
        try {
            while ((c = in.read()) >= 0) {
                System.out.print((char) c);
            }
        } finally {
            in.close();
        }
    }
}
複製程式碼

輸出

HELLO WORLD!
HELLO JAVA I/O!
複製程式碼

整個Java IO體系都是基於字元流(InputStream/OutputStream) 和 位元組流(Reader/Writer)作為基類,下面畫出OutputStream、Reader、Writer的部分類圖,更多細節請檢視其它資料

OutputStream類圖

Reader類圖

Writer類圖

spring cache 中的裝飾者模式

org.springframework.cache.transaction 包下的 TransactionAwareCacheDecorator 這個類

public class TransactionAwareCacheDecorator implements Cache {
    private final Cache targetCache;
    
    public TransactionAwareCacheDecorator(Cache targetCache) {
        Assert.notNull(targetCache, "Target Cache must not be null");
        this.targetCache = targetCache;
    }
    
    public <T> T get(Object key, Class<T> type) {
        return this.targetCache.get(key, type);
    }

    public void put(final Object key, final Object value) {
        // 判斷是否開啟了事務
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            // 將操作註冊到 afterCommit 階段
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                public void afterCommit() {
                    TransactionAwareCacheDecorator.this.targetCache.put(key, value);
                }
            });
        } else {
            this.targetCache.put(key, value);
        }
    }
    // ...省略...
}
複製程式碼

該類實現了 Cache 介面,同時將 Cache 組合到類中成為了成員屬性 targetCache,所以可以大膽猜測 TransactionAwareCacheDecorator 是一個裝飾類,不過這裡並沒有抽象裝飾類,且 TransactionAwareCacheDecorator 沒有子類,這裡的裝飾類關係並沒有Java I/O 中的裝飾關係那麼複雜

spring cache中類圖關係

該類的主要功能:通過 Spring 的 TransactionSynchronizationManager 將其 put/evict/clear 操作與 Spring 管理的事務同步,僅在成功的事務的 after-commit 階段執行實際的快取 put/evict/clear 操作。如果沒有事務是 active 的,將立即執行 put/evict/clear 操作

spring session 中的裝飾者模式

注意:介面卡模式的結尾也可能是 Wrapper

ServletRequestWrapper 的程式碼如下:

public class ServletRequestWrapper implements ServletRequest {
    private ServletRequest request;
    
    public ServletRequestWrapper(ServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");
        }
        this.request = request;
    }
    
    @Override
    public Object getAttribute(String name) {
        return this.request.getAttribute(name);
    }
    //...省略...
}    
複製程式碼

可以看到該類對 ServletRequest 進行了包裝,這裡是一個裝飾者模式,再看下圖,spring session 中 SessionRepositoryFilter 的一個內部類 SessionRepositoryRequestWrapperServletRequestWrapper 的關係

ServletRequest類圖

可見 ServletRequestWrapper 是第一層包裝,HttpServletRequestWrapper 通過繼承進行包裝,增加了 HTTP 相關的功能,SessionRepositoryRequestWrapper 又通過繼承進行包裝,增加了 Session 相關的功能

Mybatis 快取中的裝飾者模式

org.apache.ibatis.cache 包的檔案結構如下所示

Mybatis cache 中的裝飾者模式

我們通過類所在的包名即可判斷出該類的角色,Cache 為抽象構件類,PerpetualCache 為具體構件類,decorators 包下的類為裝飾類,沒有抽象裝飾類

通過名稱也可以判斷出裝飾類所要裝飾的功能

裝飾者模式總結

裝飾模式的主要優點如下:

  1. 對於擴充套件一個物件的功能,裝飾模式比繼承更加靈活性,不會導致類的個數急劇增加。
  2. 可以通過一種動態的方式來擴充套件一個物件的功能,通過配置檔案可以在執行時選擇不同的具體裝飾類,從而實現不同的行為。
  3. 可以對一個物件進行多次裝飾,通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行為的組合,得到功能更為強大的物件。
  4. 具體構件類與具體裝飾類可以獨立變化,使用者可以根據需要增加新的具體構件類和具體裝飾類,原有類庫程式碼無須改變,符合 “開閉原則”。

裝飾模式的主要缺點如下:

  1. 使用裝飾模式進行系統設計時將產生很多小物件,這些物件的區別在於它們之間相互連線的方式有所不同,而不是它們的類或者屬性值有所不同,大量小物件的產生勢必會佔用更多的系統資源,在一定程式上影響程式的效能。
  2. 裝飾模式提供了一種比繼承更加靈活機動的解決方案,但同時也意味著比繼承更加易於出錯,排錯也很困難,對於多次裝飾的物件,除錯時尋找錯誤可能需要逐級排查,較為繁瑣。

適用場景

  1. 在不影響其他物件的情況下,以動態、透明的方式給單個物件新增職責。
  2. 當不能採用繼承的方式對系統進行擴充套件或者採用繼承不利於系統擴充套件和維護時可以使用裝飾模式。不能採用繼承的情況主要有兩類:第一類是系統中存在大量獨立的擴充套件,為支援每一種擴充套件或者擴充套件之間的組合將產生大量的子類,使得子類數目呈爆炸性增長;第二類是因為類已定義為不能被繼承(如Java語言中的final類)。

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析
Java日誌框架:slf4j作用及其實現原理
HankingHu:由裝飾者模式來深入理解Java I/O整體框架
HryReal:Java的io類的使用場景

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用

更多內容可訪問我的個人部落格:laijianfeng.org

關注【小旋鋒】微信公眾號

相關文章