設計模式之裝飾器模式

来源:https://www.cnblogs.com/tianClassmate/archive/2022/08/10/16572493.html
-Advertisement-
Play Games

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


本文由老王將建好的書房計劃請小王來幫忙,小王卻想謀權篡位,老王通過教育他引出裝飾器設計模式,第二部分針對老王提出的建設性意見實現裝飾器模式,第三部分針對裝飾器模式在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的實現類就是具體構件。

image-20220810114927963

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

比如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類的繼承結構。

image-20220810113008409

我們將關鍵的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的源代碼本地進行調試研究

四、總結

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

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

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

如果我們的需求是日誌收集、攔截器,代理模式是最適合的。如果是動態的增加對象的功能、限制對象的執行條件、參數控制和檢查等使用適配器模式就更加合適了。

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

創建型設計模式

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

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

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

結構型設計模式

四、設計模式之代理模式

五、設計模式之適配器模式

六、橋接模式

七、組合模式


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 前言 JMeter是apache公司基於java開發的一款開源壓力測試工具,體積小,功能全,使用方便,是一個比較輕量級的測試工具,使用起來非常簡單。而且 ...
  • 因為喜愛,人們會將二次元形象製作成玩偶手辦,然而沒有生命氣息的冰冷模型並不能滿足人們互動性的情感需求。如何能讓帶有情感寄托的玩偶手辦更具表現力和感染力呢? 近日,HMS Core 3D建模服務上線自動骨骼綁定能力,可以讓已建成模型的二足人形物體根據自定義動作活動起來,甚至與用戶產生互動,不再只是冰冷 ...
  • Vue.use()的作用及原理 點擊打開視頻講解 在Vue中引入使用第三方庫通常我們都會採用import的形式引入進來 但是有的組件在引入之後又做了Vue.use()操作 有的組件引入進來又進行了Vue.prototype.$axios = axios 那麼它們之間有什麼聯繫呢? 例如:Vue.us ...
  • 在B/S系統開發中,前後端分離開發設計已成為一種標準,而VUE作為前端三大主流框架之一,越來越受到大家的青睞,Antdv是Antd在Vue中的實現。本系列文章主要通過Antdv和Asp.net WebApi開發學生信息管理系統,簡述前後端分離開發的主要相關內容,僅供學習分享使用,如有不足之處,還請指... ...
  • Teleport 是一種能夠將我們的模板移動到 DOM 中 Vue app 之外的其他位置的技術,不受父級style、v-show等屬性影響,但data、prop數據依舊能夠共用的技術;類似於 React 的 Portal。主要解決的問題 因為Teleport節點掛載在其他指定的DOM節點下,完全不 ...
  • 解決某些情況下 ECharts 餅圖多行標簽重疊問題 對於多行標簽的重疊問題,其實一直沒有一個完美的解決方案。 我能在網上查到的比較全面的解決方法就是這個:https://zhuanlan.zhihu.com/p/272710806 但我的項目中某些東西是明確的:Label的行數、字體大小、數據個數 ...
  • 在前端開發過程中,我們也有可能遇到噪點插畫風格的設計稿,應用基礎的前端開發知識,能不能實現噪點風格的樣式呢,本文主要內容主要就是通過幾個示例來實現幾種噪點效果。本文包含的知識點包括:CSS 屬性 mask 遮罩、SVG 濾鏡 feTurbulence、CSS 屬性 filter 濾鏡、CSS 屬性 ... ...
  • 本文結合自身後臺開發經驗,從高可用、高性能、易維護和低風險(安全)角度出發,嘗試總結業界常見微服務介面設計原則,幫助大家設計出優秀的微服務。 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...