設計模式之裝飾器模式

来源: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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...