包裝模式就是這麼簡單啦

Java3y發表於2019-03-04

前言

只有光頭才能變強

回顧前面:

前一篇已經講解了代理模式了,今天要講解的就是裝飾模式啦~

在看到FilterInputStreamFilterOutputStream時看到了之前常聽見的裝飾模式(對IO一定了解的同學可能都會知道那麼一句話:在IO用得最多的就是裝飾模式了)!

其實無論是代理模式還是裝飾模式。本質上我認為就是對原有物件增強的方式~

那麼接下來就開始吧,如果文章有錯誤的地方請大家多多包涵,不吝在評論區指正哦~

宣告:本文使用JDK1.8

一、物件增強的常用方式

很多時候我們可能對Java提供給我們的物件不滿意,不能滿足我們的功能。此時我們就想對Java原物件進行增強,能夠實現我們想要的功能就好~

一般來說,實現物件增強有三種方式:

1.1繼承

最簡單的方式就是繼承父類,子類擴充套件來達到目的。雖然簡單,但是這種方式的缺陷非常大

  • 一、如果父類是帶有資料、資訊、屬性的話,那麼子類無法增強。
  • 二、子類實現了之後需求無法變更,增強的內容是固定的。

1.1.1第一點

第一點就拿以前在學JDBC的時候來說:

  • 當時想要自己寫一個簡易的JDBC連線池,連線池由List<Connection>來管理。顯然我們的物件是Connection,當寫到close()方法的時候卡住了。
  • 因為我們想要的功能是:呼叫close()是讓我們的Connection返回到“連線池”(集合)中,而不是關閉掉。
  • 此時我們不能使用繼承父類的方式來實現增強。因為Connection物件是由資料庫廠商來實現的,在得到Connection物件的時候繫結了各種資訊(資料庫的username、password、具體的資料庫是啥等等)。我們子類繼承Connection是無法得到對應的資料的!就更別說呼叫close()方法了。

1.1.2第二點

第二點我也舉個例子:

現在我設計一個電話類:


public class Phone {
    // 可以打電話
    public void call() {
        System.out.println("打電話給周圍的人關注公眾號Java3y");
    }
}
複製程式碼

此時,我想打電話之前能聽彩鈴,於是我繼承Phone類,實現我想要的功能。


public class MusicPhone extends Phone {
    
    // 聽彩鈴
    public void listenMusic() {
        System.out.println("我懷念的是無話不說,我懷念的是一起做夢~~~~~~");
    }

    @Override
    public void call() {

        // 在打電話之前聽彩鈴
        listenMusic();

        super.call();
    }
}
複製程式碼

我們的功能就做好了:

包裝模式就是這麼簡單啦

  • 此時,我又突然想實現多一個需求了,我想要聽完電話之後告訴我一下當前的時間是多少。沒事,我們又繼承來增強一下:

// 這裡繼承的是MusicPhone類
public class GiveCurrentTimePhone extends MusicPhone {

    // 給出當前的時間
    public void currentTime() {
        System.out.println("當前的時間是:" + System.currentTimeMillis());
    }

    @Override
    public void call() {
        super.call();

        // 打完電話提示現在的時間是多少啦
        currentTime();
    }
}

複製程式碼

所以我們還是可以完成任務滴:

包裝模式就是這麼簡單啦

可是我需求現在又想變了:

  • 我不想聽彩鈴了,只想聽完電話通知一下時間就好了........(可是我們的通知時間電話類是繼承在聽彩鈴的電話類基礎之上的),,,
  • 我又有可能:我想在聽電話之前報告一下時間,聽完電話聽音樂!...
  • 如果需求變動很大的情況下,而我們又用繼承的方式來實現這樣會導致一種現象:類爆炸(類數量激增)!並且繼承的層次可能會比較多~

所以,我們可以看到子類繼承父類這種方式來擴充套件是十分侷限的,不靈活的~

因此我們就有了裝飾模式

1.2裝飾模式

首先我們來看看裝飾模式是怎麼用的吧。

1.2.1前提程式碼

電話介面:


// 一個良好的設計是抽取成介面或者抽象類的
public interface Phone {

    // 可以打電話
    void call();
}

複製程式碼

具體的實現


public class IphoneX implements Phone {


    @Override
    public void call() {
        System.out.println("打電話給周圍的人關注公眾號Java3y");
    }
}

複製程式碼

1.2.2包裝模式實現

上面我們已經擁有了一個介面還有一個預設實現。包裝模式是這樣乾的:

首先我們弄一個裝飾器,它實現了介面,以組合的方式接收我們的預設實現類


// 裝飾器,實現介面
public abstract class PhoneDecorate implements Phone {

    // 以組合的方式來獲取預設實現類
    private Phone phone;
    public PhoneDecorate(Phone phone) {
        this.phone = phone;
    }

    @Override
    public void call() {
        phone.call();
    }
}

複製程式碼

有了裝飾器以後,我們的擴充套件都可以以裝飾器為基礎進行擴充套件,繼承裝飾器來擴充套件就好了!

我們想要在打電話之前聽音樂


// 繼承著裝飾器來擴充套件
public class MusicPhone extends PhoneDecorate {

    public MusicPhone(Phone phone) {
        super(phone);
    }

    // 定義想要擴充套件的功能
    public void listenMusic() {

        System.out.println("繼續跑 帶著赤子的驕傲,生命的閃耀不堅持到底怎能看到,與其苟延殘喘不如縱情燃燒");

    }

    // 重寫打電話的方法
    @Override
    public void call() {

        // 在打電話之前聽音樂
        listenMusic();
        super.call();
    }
}

複製程式碼

包裝模式就是這麼簡單啦

現在我也想在打完電話後通知當前的時間,於是我們也繼承裝飾類來擴充套件


// 這裡繼承的是MusicPhone裝飾器類
public class GiveCurrentTimePhone extends PhoneDecorate  {


    public GiveCurrentTimePhone(Phone phone) {
        super(phone);
    }

    // 自定義想要實現的功能:給出當前的時間
    public void currentTime() {
        System.out.println("當前的時間是:" + System.currentTimeMillis());
    }

    // 重寫要增強的方法
    @Override
    public void call() {
        super.call();
        // 打完電話後通知一下當前時間
        currentTime();
    }
}
複製程式碼

可以完成任務:

包裝模式就是這麼簡單啦

目前這樣看起來,比我直接繼承父類要麻煩,而功能效果是一樣的....我們繼續往下看~~

此時,我不想在打電話之前聽到彩鈴了,很簡單:我們不裝飾它就好了!

包裝模式就是這麼簡單啦

此時,我想在打電話前報告一下時間,在打完電話之後聽彩鈴。

  • 注意:雖然說要改動類中的程式碼,但是這種改動是合理的。因為我定義出的GiveCurrentTimePhone類MusicPhone類本身從語義上就沒有規定擴充套件功能的執行順序
  • 而繼承不一樣:先繼承Phone->實現MusicPhone->再繼承MusicPhone實現GiveCurrentTimePhone。這是固定的,從繼承的邏輯上已經寫死了具體的程式碼,是難以改變的。

包裝模式就是這麼簡單啦

包裝模式就是這麼簡單啦

所以我們還是可以很簡單地完成功能:

包裝模式就是這麼簡單啦

二、裝飾模式講解

可能有的同學在看完上面的程式碼之後,還是迷迷糊糊地不知道裝飾模式是怎麼實現“裝飾”的。下面我就再來解析一下:

  • 第一步:我們有一個Phone介面,該介面定義了Phone的功能
  • 第二步:我們有一個最簡單的實現類iPhoneX
  • 第三步:寫一個裝飾器抽象類PhoneDecorate,以組合(建構函式傳遞)的方式接收我們最簡單的實現類iPhoneX。其實裝飾器抽象類的作用就是代理(核心的功能還是由最簡單的實現類iPhoneX來做,只不過在擴充套件的時候可以新增一些沒有的功能而已)。
  • 第四步:想要擴充套件什麼功能,就繼承PhoneDecorate裝飾器抽象類,將想要增強的物件(最簡單的實現類iPhoneX或者已經被增強過的物件)傳進去,完成我們的擴充套件!

再來看看下面的圖,就懂了!

包裝模式就是這麼簡單啦

往往我們的程式碼可以省略起來,成了這個樣子(是不是和IO的非常像!)


    // 先增強聽音樂的功能,再增強通知時間的功能
    Phone phone = new GiveCurrentTimePhone(new MusicPhone(new IphoneX()));
複製程式碼

結果是一樣的:

包裝模式就是這麼簡單啦

2.1裝飾模式的優缺點

優點:

  • 裝飾類和被裝飾類是可以獨立的,低耦合的。互相都不用知道對方的存在
  • 裝飾模式是繼承的一種替代方案,無論包裝多少層,返回的物件都是is-a的關係(上面的例子:包裝完還是Phone型別)。
  • 實現動態擴充套件,只要繼承了裝飾器就可以動態擴充套件想要的功能了。

缺點:

  • 多層裝飾是比較複雜的,提高了系統的複雜度。不利於我們除錯~

三、總結

最後來補充一下包裝模式和代理模式的類圖:

包裝模式就是這麼簡單啦

包裝模式就是這麼簡單啦

物件增強的三種方式:

  • 繼承
  • 包裝模式
  • 代理模式

那麼只要遇到Java提供給我們的API不夠用,我們增強一下就行了。在寫程式碼時,某個類被寫死了,功能不夠用,增強一下就可以了!

理解包裝模式,接下來就開始IO之旅咯~~~

參考資料:

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y

文章的目錄導航

相關文章