設計模式之裝飾器模式

2022-08-10 18:05:51

本文由老王將建好的書房計劃請小王來幫忙,小王卻想謀權篡位,老王通過教育他引出裝飾器設計模式,第二部分針對老王提出的建設性意見實現裝飾器模式,第三部分針對裝飾器模式在Jdk中的IO、Spring中的快取管理器、Mybatis的運用來加強我們的理解,第四部分說明裝飾器模式和代理模式的區別及他們各自的應用場景。

讀者可以拉取完整程式碼到本地進行學習,實現程式碼均測試通過後上傳到碼雲

一、引出問題

上篇文章對老王的書架改造以後,老王是相當的滿意,看小王能力突出,這不老王又有了新的需求。

經過組合模式以後老王的書被管理的井井有條,但是隨著書的增多,老王就有一些忙不過來了,老王就想讓小王幫他處理一些額外的事,比如在買書之前打掃一下書房,在晚上的時候把書房的門鎖一下;或者有人借書之前做一下記錄,借書者還書以後小王接收一下,等等。

小王聽完說這有何難,說完擼起袖子就準備改老王的程式碼。老王急忙攔住了他,你真是個呆瓜,我寫的程式碼你憑什麼要動,你改了會不會影響我的業務邏輯,平時讓你多看書你不聽,之前學的設計模式呢?不拿出來用,眼看著讓他吃灰。

小王不好意思的撓撓頭,翻出來了他的設計模式寶典,開始尋找合適的設計模式。

小王大喊有了,之前說過的代理模式可以很好的解決這個問題,代理模式可以動態的增強物件的一些特性,我準備使用代理模式完成這個需求。

老王聽完止不住的搖搖頭,看來你是打算謀權篡位了,你是想要我整個書房的權利呀!

老王解釋說,代理模式是可以實現這個需求,但是在這個場景下顯然代理模式不合適,代理模式是著重對物件的控制,而我們今天的需求是在該物件的基礎之上增加他的一些功能,我們各自的業務獨立發展互不干擾。

二、裝飾器模式概念與使用

實際上,在原物件的基礎之上增加其功能就是屬於裝飾器模式。

裝飾器模式(Decorator Pattern)允許向一個現有的物件新增新的功能,同時又不改變其結構。這種型別的設計模式屬於結構型模式,它是作為現有的類的一個包裝。

在裝飾器模式中應該是有四個角色:

①Component抽象構件(老王抽象方法)
②ConcreteComponent 具體構件(老王實現方法)
③Decorator裝飾角色(裝飾者小王)
④ConcreteDecorator 具體裝飾角色(裝飾者小王實現方法)

在裝飾器模式中,需要增強的類(被裝飾者)要實現介面,裝飾者繼承被裝飾者的介面,並將被裝飾者的範例傳進去,在具體裝飾角色中呼叫被裝飾者的方法,在其前後定義增強的方法,在實際應用中往往裝飾角色和具體裝飾角色合二為一。

我們看下具體的程式碼實現:

抽象構件:

/**
 * 書的抽象構件
 * @author tcy
 * @Date 10-08-2022
 */
public abstract class ComponentBook {

    /**
     * 借書
     */
    public abstract void borrowBook();

    /**
     * 買書
     */
    public abstract void buyBook();

}

書的具體構件:

/**
 * 書的具體構件
 * @author tcy
 * @Date 10-08-2022
 */
public class ConcreteComponentBook extends ComponentBook{
    @Override
    public void borrowBook() {
        System.out.println("老王的書借出去...");

    }

    @Override
    public void buyBook() {
        System.out.println("老王的書買回來...");

    }
}

裝飾角色:

/**
 * 書的裝飾者
 * @author tcy
 * @Date 10-08-2022
 */
public class DecoratorBook extends ComponentBook{

    private ComponentBook componentBook;

    DecoratorBook(ComponentBook componentBook){
        this.componentBook=componentBook;
    }

    @Override
    public void borrowBook() {
        this.componentBook.borrowBook();
    }

    @Override
    public void buyBook() {
        this.componentBook.buyBook();
    }
}

書的具體裝飾角色:

/**
 * 子類裡寫了並且使用了無參的構造方法但是它的父類別(祖先)中卻至少有一個是沒有無參構造方法的
 * @author tcy
 * @Date 10-08-2022
 */
public class ConcreteDecoratorBook1 extends DecoratorBook{

    ConcreteDecoratorBook1(ComponentBook componentBook) {
        super(componentBook);
    }

    public void cleanRoom(){
        System.out.println("打掃書房...");
    }

    public void shutRoom(){
        System.out.println("關閉書房...");
    }

    public void recordBook(){
        System.out.println("記錄借出記錄...");
    }

    public void returnBook(){
        System.out.println("收到借出去的書...");
    }

    @Override
    public void buyBook() {
        this.cleanRoom();
        super.buyBook();
        this.shutRoom();
        System.out.println("----------------------------");
    }

    @Override
    public void borrowBook() {
        this.recordBook();
        super.borrowBook();
        this.returnBook();
        System.out.println("----------------------------");
    }
}

如果讀者的Java基礎紮實,理解裝飾器還是比較輕鬆的,裝飾器的實現方式很直觀,需要特別指出的是,在書的具體裝飾角色中,要顯示的定義一個構造方法。

基礎不太紮實的讀者可能會有一個疑問,在Java的類中預設不是會有一個無參的構造方法嗎?為什麼這裡還需要定義呢?

在java中一個類只要有父類別,那麼在它範例化的時候,一定是從頂級的父類別開始建立。

也就是說當你用子類的無參建構函式建立子類物件時,會去先遞迴呼叫父類別的無參構造方法,這時候如果某個類的父類別沒有無參構造方法就會編譯出差。所以我們在子類中可以手動定義一個無參方法,或者在父類別中顯示的定義一個構造方法。

使用者端:

/**
 * @author tcy
 * @Date 09-08-2022
 */
public class  {
    public static void main(String[] args) {

        ComponentBook componentBook=new ConcreteComponentBook();
        componentBook=new ConcreteDecoratorBook1(componentBook);

        componentBook.borrowBook();

        componentBook.buyBook();
    }
}

方法呼叫後我們可以看到執行結果:

記錄借出記錄...
老王的書借出去...
收到借出去的書...
----------------------------
打掃書房...
老王的書買回來...
關閉書房...
----------------------------

在老王借書和買書的這個事件中,成功的織入進去小王的方法,這樣也就實現了裝飾器模式。

為了加強理解我們接著看裝飾器模式在我們經常接觸的原始碼中的運用。

三、應用

1、jdk中的應用IO

裝飾器在java中最典型的應用就是IO,我們知道在IO家族中有各種各樣的流,而流往往都是作用在子類之上,然後增加其附加功能,我們以InputStream 舉例。

InputStream 是位元組輸入流,此抽象類是表示位元組輸入流的所有類的超類。

FileInputStream是InputStream 的一個實現父類別,BufferedInputStream是FileInputStream的實現父類別。

實際BufferedInputStream就是裝飾者,InputStream 就是抽象構件,FileInputStream是具體構件,BufferedInputStream就是對FileInputStream進行了包裝。

我們看具體的應用:

FileInputStream fileInputStream = new FileInputStream(filePath); 
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

直接將FileInputStream的範例傳給BufferedInputStream的構造方法,就能呼叫BufferedInputStream增強的一些方法了。

我們具體看BufferedInputStream裝飾器類:

public class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = 8192;

    protected int marklimit;

    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

  	...
    //增強的方法
    private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
        if (markpos < 0)
            pos = 0;            /* no mark: throw away the buffer */
        else if (pos >= buffer.length)  /* no room left in buffer */
            if (markpos > 0) {  /* can throw away early part of the buffer */
                int sz = pos - markpos;
                System.arraycopy(buffer, markpos, buffer, 0, sz);
                pos = sz;
                markpos = 0;
            } else if (buffer.length >= marklimit) {
                markpos = -1;   /* buffer got too big, invalidate mark */
                pos = 0;        /* drop buffer contents */
            } else if (buffer.length >= MAX_BUFFER_SIZE) {
                throw new OutOfMemoryError("Required array size too large");
            } else {            /* grow buffer */
                int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                        pos * 2 : MAX_BUFFER_SIZE;
                if (nsz > marklimit)
                    nsz = marklimit;
                byte nbuf[] = new byte[nsz];
                System.arraycopy(buffer, 0, nbuf, 0, pos);
                if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                    // Can't replace buf if there was an async close.
                    // Note: This would need to be changed if fill()
                    // is ever made accessible to multiple threads.
                    // But for now, the only way CAS can fail is via close.
                    // assert buf == null;
                    throw new IOException("Stream closed");
                }
                buffer = nbuf;
            }
        count = pos;
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;
    }

   ...

BufferedInputStream類中有許多其他的方法,就是對FileInputStream類的增強。

2、Spring中的運用

Spring使用裝飾器模式有兩個典型的特徵,一個是類名中含有Wrapper,另一類是含有Decorator,功能也即動態的給某些類增加一些額外的功能。

TransactionAwareCacheDecorator是處理spring有事務的時候快取的類,我們在使用spring的cache註解實現快取的時候,當出現事務的時候,那麼快取的同步性就需要做相應的處理了,於是就有了這個裝飾者。

public class TransactionAwareCacheDecorator implements Cache {

	//抽象構件
   private final Cache targetCache;

   /**
    * Create a new TransactionAwareCache for the given target Cache.
    * @param targetCache the target Cache to decorate
    */
   public TransactionAwareCacheDecorator(Cache targetCache) {
      Assert.notNull(targetCache, "Target Cache must not be null");
      this.targetCache = targetCache;
   }

   /**直接呼叫未增強
    * Return the target Cache that this Cache should delegate to.
    */
   public Cache getTargetCache() {
      return this.targetCache;
   }

	//直接呼叫未增強
   @Override
   public String getName() {
      return this.targetCache.getName();
   }

//直接呼叫未增強
   @Override
   public Object getNativeCache() {
      return this.targetCache.getNativeCache();
   }

//直接呼叫未增強
   @Override
   @Nullable
   public ValueWrapper get(Object key) {
      return this.targetCache.get(key);
   }

//直接呼叫未增強
   @Override
   public <T> T get(Object key, @Nullable Class<T> type) {
      return this.targetCache.get(key, type);
   }

//直接呼叫未增強
   @Override
   @Nullable
   public <T> T get(Object key, Callable<T> valueLoader) {
      return this.targetCache.get(key, valueLoader);
   }

//先進行判斷確定是否需要增強
   @Override
   public void put(final Object key, @Nullable final Object value) {
      if (TransactionSynchronizationManager.isSynchronizationActive()) {
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
               TransactionAwareCacheDecorator.this.targetCache.put(key, value);
            }
         });
      }
      else {
         this.targetCache.put(key, value);
      }
   }
}

Cache是抽象構件,TransactionAwareCacheDecorator就是裝飾者,而Cache的實現類就是具體構件。

因為並非所有的方法都會使用事務,有的普通方法就不需要裝飾,有的就需要,所以就使用了裝飾者模式來完成。

比如put()方法:

  @Override
   public void put(final Object key, @Nullable final Object value) {
      if (TransactionSynchronizationManager.isSynchronizationActive()) {
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
               TransactionAwareCacheDecorator.this.targetCache.put(key, value);
            }
         });
      }
      else {
         this.targetCache.put(key, value);
      }
   }

會在put前判斷是否開啟了事務TransactionSynchronizationManager.isSynchronizationActive(),如果開啟事務就呼叫一下額外的方法,如果沒有開始事務就呼叫預設的方法。

我們舉的這個例子呼叫的就是 TransactionSynchronizationManager.registerSynchronization()方法,也即是為當前執行緒註冊一個新的事務同步。

在Spring中將裝飾角色和具體裝飾角色合二為一,直接在裝飾者中實現要增加的方法。

3、MyBatista的運用

瞭解過MyBatis的大致執行流程的讀者應該知道,Executor是MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護;CachingExecutor是一個Executor的裝飾器,給一個Executor增加了快取的功能。此時可以看做是對Executor類的一個增強,故使用裝飾器模式是合適的。

我們首先看下Executor類的繼承結構。

我們將關鍵的CachingExecutor程式碼放上:

public class CachingExecutor implements Executor {
    //持有元件物件
  private Executor delegate;
  private TransactionalCacheManager tcm = new TransactionalCacheManager();
    //構造方法,傳入元件物件
  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
      //轉發請求給元件物件,可以在轉發前後執行一些附加動作
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }
  //...
 }

Executor就是抽象構件,BaseExecutor是具體構件的實現,CachingExecutor就是裝飾角色,那具體裝飾角色在哪呢?

實際中具體裝飾角色直接在裝飾角色中整合了,並沒有將具體裝飾角色完全獨立出來。

另外,Mybatis的一級快取和二級快取也是使用的裝飾者模式,有興趣的讀者可以拉取Mybatis的原始碼本地進行偵錯研究

四、總結

到此為止,我們就將裝飾器模式的內容講解清楚了,看到這讀者可能發現,針對某一類需求可能會有很多設計模式都能完成需求,但一定是有最合適的那一個,就像我們今天舉的例子無論是用裝飾器模式還是代理模式都可以實現這個需求。

但我們看代理模式中我們列舉的例子是以租房做例子,中介將房子的權利完全移交過去,中介完全控制房子做一些改造,今天書房的需求只是讓小王來幫忙的,還是以老王為主體,小王只是做一些附加。

裝飾器模式就是在瓶裡面插了一朵花,而代理模式是把瓶子都給人家了,讓人家隨便折騰。

如果我們的需求是紀錄檔收集、攔截器,代理模式是最適合的。如果是動態的增加物件的功能、限制物件的執行條件、引數控制和檢查等使用介面卡模式就更加合適了。

推薦讀者,參考軟體設計七大原則 認真閱讀往期的文章,認真體會。

建立型設計模式

一、設計模式之工廠方法和抽象工廠

二、設計模式之單例和原型

三、設計模式之建造者模式

結構型設計模式

四、設計模式之代理模式

五、設計模式之介面卡模式

六、橋接模式

七、組合模式