原始碼中的設計模式--裝飾器模式

北漂程式設計師發表於2022-05-02

一、模式入場

有一句很經典的小品臺詞是“換個馬甲我就不認識你了嗎”,哈哈,這個比方正好用在今天要分享的裝飾器模式上。先看下《head first 設計模式》中給的釋義。

裝飾者模式  動態地將責任附加到物件上。若要擴充套件功能,裝飾者提供了比基層更有彈性的替代方案。

細心的小夥伴發現了這個釋義怎麼是裝飾者模式,今天說的不是裝飾器模式嗎,其實這兩個名稱所代表的意思是一樣的,為了保持和書上一致這裡是裝飾者模式,後續統一稱為裝飾器。

這個釋義太抽象太理論了,下面通俗的講下。說到“裝飾”二字,肯定第一時間想到的就是要有裝飾者和被裝飾者,正如前面說到的“換個馬甲我就不認識你了嗎”,這裡的馬甲可以理解為裝飾者,穿馬甲的就是被裝飾者,放到裝飾器模式裡稍微有些不同,我們繼續往下說。“裝飾”,可以簡單的理解為“偽裝”,可以偽裝成另外一個樣子,也可以偽裝成某種不同於原物的一種行為,所以在裝飾者和被裝飾者之間肯定存在某種相似,才可以使用裝飾物去裝飾被裝飾者。用在java的設計模式中,我們講的更多的是行為,也就是一個類所能完成的操作是可以用來裝飾的。

下面簡單的根據一個UML圖來了解下裝飾器模式,

 

上圖中Component是一個介面,有兩個方法methodA、methodB,有三個實現類ComponentA、ComponentB、ComponentDecoratorA,可以看到ComponentDecoratorA和其他兩個實現類是不一樣的,它有一個Component的屬性,其他的從UML中看不出其他,當然在methodA、methodB方法中別有洞天,後面會說到。這裡的CompoentDecoratorA其實就是一個裝飾器類,任何實現了Component介面的類,都可以被它裝飾,完成相應的功能。

可以看到裝飾器模式中有實現(繼承),還有組合。

二、深入裝飾器模式

上面對裝飾器模式已經大體有了一個瞭解,下面通過一個具體的例子來實現一個簡單的裝飾器模式。

Component.java

package com.example.decorator;

public interface Component {
    String methodA(String params);
    void methodB();
}

ComponentA.java

package com.example.decorator;

public class ComponentA implements Component{
    //返回字串
    @Override
    public String methodA(String params) {
        return "ComponentA methodA";
    }
    //列印字串
    @Override
    public void methodB() {
        System.out.println("ComponentA methodB");
    }
}

ComponentB.java

package com.example.decorator;

public class ComponentB implements Component{
    //返回字串
    @Override
    public String methodA(String params) {
        return "ComponentB methodA";
    }
   //列印字串
    @Override
    public void methodB() {
        System.out.println("ComponentB methodB");
    }
}

ComponentDecoratorA.java

package com.example.decorator;

public class ComponentDecoratorA implements Component{
    //Component的例項
    private Component component;
   //建構函式
    public ComponentDecoratorA(Component component){
        this.component=component;
    }
   //呼叫component的methodA方法,返回字串
    @Override
    public String methodA(String params) {
        String decoratorStr=component.methodA(params);

        return "ComponentDecoratorA methodA,"+decoratorStr;
    }
   //呼叫component的methodB方法,列印字串
    @Override
    public void methodB() {
        component.methodB();
        System.out.println("ComponentDecoratorA methodB");
    }
}

下面看測試程式碼,

package com.example.decorator;

public class TestDecorator {
    public static void main(String[] args) {
       //一個Component例項,被包裝的例項
        Component component=new ComponentA();
       //使用ComponentDecoratorA進行包裝
        Component component1=new ComponentDecoratorA(component);
        String str=component1.methodA("");
        System.out.println(str);
    }
}

看下測試結果,

ComponentDecoratorA methodA,ComponentA methodA

Process finished with exit code 0

符合測試預期。

很簡單吧,這就是裝飾器模式,總結以下要點,

1、裝飾者和被裝飾者要實現統一的介面;

2、在裝飾者物件中持有被裝飾者的物件例項;

3、在裝飾者行為中,主動呼叫被裝飾者行為;

很多小夥伴會問,裝飾者和被裝飾者必須實現統一的介面(interface)嗎,使用抽象類可以嗎,其實是可以的,上述的介面可以理解為介面和抽象類,我們說只要他們有共同的行為即可,不必太拘泥於定義。

另外,在裝飾器模式中,運用了實現(繼承)和組合設計原則

三、追尋原始碼

上面我們已經學會了使用裝飾器模式,讓我們繼續在原始碼中找尋它的影子,學習下優秀的人是怎麼使用裝飾器模式的,讓我們的程式碼越來越好。

1、java檔案系統

在Java實現的API中已經有了裝飾器模式的使用,而且在日常開發中很常用,不知道你注意到沒有,如果沒有下次在使用檔案操作類的時候可以留心下哦。

在java的檔案系統中,有位元組流和字元流,又分為輸入和輸出,分別是InputStream、OutputStream、Reader、Writer。以InputStream來舉例吧,在inputStream下有一個FilterInputStream,這是一個抽象類,該類便是一個裝飾者類的介面,裝飾所有實現了InputStream的類,

另外,這裡的InputStream是抽象類,

看下其重要的read方法,在裝飾者FilterInputStream中是怎麼實現的,

可以看到呼叫的是具體被裝飾者的read方法,由於FilterInputStream是抽象的,我們看下其具體的一個實現類也就是具體的一個裝飾者的實現,看下BufferedInputStream,

public
class BufferedInputStream extends FilterInputStream {
   //預設的緩衝大小
    private static int DEFAULT_BUFFER_SIZE = 8192;
   //最大
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
   //快取區
    protected volatile byte buf[];

    protected int count;

    protected int pos;
    
    protected int markpos = -1;

    protected int marklimit;

    /**
     * Creates a <code>BufferedInputStream</code>
     * and saves its  argument, the input stream
     * <code>in</code>, for later use. An internal
     * buffer array is created and  stored in <code>buf</code>.
     *
     * @param   in   the underlying input stream.
     */
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

該類的程式碼有刪改,可以看到BufferedInputStream中定義了很多屬性,這些資料都是為了可緩衝讀取來作準備的,看到其有構造方法會傳入一個InputStream的例項。實際編碼如下

//被裝飾的物件,檔案輸入流
InputStream in=new FileInputStream("/root/doc/123.txt");
//裝飾物件,可緩衝
InputStream bufferedIn=new BufferedInputStream(in);
bufferedIn.read();

上面的程式碼便使用的裝飾器模式進行的可緩衝的檔案讀取,程式碼很眼熟吧,其實你已經使用了裝飾器模式。

上面僅是拿InputStream進行了舉例說明其實,在java的IO系統中,FilterInputStream、FilterOutputStream、FilterReader、FilterWriter抽象類都是裝飾器模式的體現,其抽象類的子類都是裝飾者類。

2、mybatis快取系統

mybatis自帶一級快取,其快取設計就是使用的裝飾器模式,我們先來看下其cache介面

上圖紅框中標出的是Cache介面的直接實現PerpetualCache,這個類可以作為被裝飾者,再看其他的實現均在org.apache.ibatis.cache.decorators包中,那麼也就是裝飾者,看下LruCache的實現,僅貼出部分程式碼,

public class LruCache implements Cache {
    //Cache例項
    private final Cache delegate;
   //實現LRU演算法的輔助map
    private Map<Object, Object> keyMap;
    private Object eldestKey;
   //建構函式,傳入一個Cache,用來初始胡delegate和其他引數
    public LruCache(Cache delegate) {
        this.delegate = delegate;
        this.setSize(1024);
    }
}

這個程式碼和最開始演示的Component的那個例子很像,至於LRU快取怎麼實現的,各位小夥伴可以自行了解。下次再使用到mybatis的快取,你就可以自豪的說這是裝飾器模式。

3、mybatis的Executor執行器

在mybatis中真正負責執行sql語句的是Executor介面,

該介面有以下幾個實現類:CachingExecutor、BaseExecutor、SimpleExecutor等,重點看下CachingExecutor、SimpleExecutor

CachingExecutor應該是裝飾者,看下SimpleExecutor

這個應該是被裝飾者,它在執行具體的操作。

四、總結

本文分享了裝飾器模式及在原始碼中的使用,需要幾種以下幾點,

1、裝飾者和被裝飾者要實現統一的介面;

2、在裝飾者物件中持有被裝飾者的物件例項;

3、在裝飾者行為中,主動呼叫被裝飾者行為;

裝飾器模式很好的體現了繼承(實現)和組合的設計原則。

 

相關文章