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

前言

本文的主要內容:html

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

裝飾者模式

裝飾者模式(Decorator Pattern):動態地給一個對象增長一些額外的職責,增長對象功能來講,裝飾模式比生成子類實現更爲靈活。裝飾模式是一種對象結構型模式。java

在裝飾者模式中,爲了讓系統具備更好的靈活性和可擴展性,咱們一般會定義一個抽象裝飾類,而將具體的裝飾類做爲它的子類spring

角色

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

ConcreteComponent(具體構件):它是抽象構件類的子類,用於定義具體的構件對象,實現了在抽象構件中聲明的方法,裝飾器能夠給它增長額外的職責(方法)。編程

Decorator(抽象裝飾類):它也是抽象構件類的子類,用於給具體構件增長職責,可是具體職責在其子類中實現。它維護一個指向抽象構件對象的引用,經過該引用能夠調用裝飾以前構件對象的方法,並經過其子類擴展該方法,以達到裝飾的目的。設計模式

ConcreteDecorator(具體裝飾類):它是抽象裝飾類的子類,負責向構件添加新的職責。每個具體裝飾類都定義了一些新的行爲,它能夠調用在抽象裝飾類中定義的方法,並能夠增長新的方法用以擴充對象的行爲。數組

因爲具體構件類和裝飾類都實現了相同的抽象構件接口,所以裝飾模式以對客戶透明的方式動態地給一個對象附加上更多的責任,換言之,客戶端並不會以爲對象在裝飾前和裝飾後有什麼不一樣。裝飾模式能夠在不須要創造更多子類的狀況下,將對象的功能加以擴展。緩存

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

示例

煎餅抽象類微信

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;
    }
}
複製代碼

測試,購買煎餅

一、購買一個煎餅

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

輸出

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

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

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
複製代碼

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

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
複製代碼

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

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

關注【小旋鋒】微信公衆號
相關文章
相關標籤/搜索