裝飾者模式

裝飾者模式定義

裝飾模式又名包裝(Wrapper)模式。是一種在不改變?cè)袑?duì)象的基礎(chǔ)之上,將功能附加到對(duì)象上。提供了比繼承更有彈性的替代方案(擴(kuò)展原有對(duì)象功能)。

裝飾者模式能夠動(dòng)態(tài)地將責(zé)任附加到對(duì)象身上的一種設(shè)計(jì)模式,若要擴(kuò)展功能,裝飾者提供了比繼承更有彈性的替代方案,比生成子類更加靈活。通常在繼承關(guān)系中,為了擴(kuò)展功能需要新增子類進(jìn)行擴(kuò)展,而裝飾者模式,可以在不擴(kuò)展子類的情況下,將對(duì)象的功能進(jìn)行動(dòng)態(tài)的擴(kuò)展。

下面我們通過一個(gè)具體的例子,詳細(xì)介紹一下裝飾者模式:

某教育咨詢公司“學(xué)幫忙”,為了幫助學(xué)生更好的了解每個(gè)大學(xué)信息,打算做一個(gè)大學(xué)院校綜合實(shí)力評(píng)分系統(tǒng)。系統(tǒng)可以根據(jù)每個(gè)大學(xué)的信息,計(jì)算出大學(xué)的綜合實(shí)力得分:

  1. 普通本科記10分,獨(dú)立學(xué)院記8分,中外合辦記8分,高職??茖W(xué)校記5分。

  2. 211和985學(xué)校額外分別加20分。

  3. 每一個(gè)碩士點(diǎn)加1分。

  4. 每一個(gè)博士點(diǎn)加2分。

于是他們針對(duì)這種需求,進(jìn)行了軟件的設(shè)計(jì)和開發(fā)。

image.png

設(shè)計(jì)一個(gè)抽象的大學(xué)接口,然后每個(gè)大學(xué)對(duì)其進(jìn)行實(shí)現(xiàn),這樣就可以計(jì)算出每個(gè)大學(xué)的的綜合實(shí)力得分。

但是,當(dāng)他們開始對(duì)系統(tǒng)進(jìn)行實(shí)現(xiàn)時(shí),百度了一下中國(guó)有多少所大學(xué)。。。

image.png

將近3000所大學(xué),如果按照這個(gè)模式進(jìn)行實(shí)現(xiàn)的話,那么將有3000個(gè)具體的類實(shí)現(xiàn),很顯然,這是不現(xiàn)實(shí)的。。。

而且就算我們開始的時(shí)候發(fā)動(dòng)人力,對(duì)這3000所學(xué)校進(jìn)行了實(shí)現(xiàn),但是如果以后計(jì)算規(guī)則發(fā)生改變,比如一個(gè)211院校額外加15分,那么再回過頭去修改這3000個(gè)學(xué)校,將又是很大的一個(gè)工程。

那么有沒有辦法能不生成這么多類呢?聰明的你一定已經(jīng)想到,可以在子類里面標(biāo)記大學(xué)的具體信息,然后在基類里面計(jì)算大學(xué)得分。

image.png

結(jié)合代碼我們看一下

public abstract class University {
    public abstract String getUnvName();
    public abstract int getUnvType();
    public abstract boolean is211();
    public abstract boolean is985();
    public abstract int getMasterNum();
    public abstract int getDoctorNum();
    public int getPoint(){
        int point = 0;
        switch (getUnvType()){
            case 0: //普通本科
                point = 10;
                break;
            case 1: //獨(dú)立學(xué)院
            case 2: //中外合資
                point = 8;
                break;
            case 3: //高職院校
                point = 5;
                break;
        }

        if (is211()){
            point += 20;
        }

        if (is985()){
            point += 20;
        }

        point += getMasterNum();
        point += getDoctorNum() * 2;
        return point;
    }
}

我們結(jié)合上面的類圖和代碼,可以看出,我們?nèi)绻氲玫揭粋€(gè)大學(xué)的分?jǐn)?shù),只需要讓他繼承University類,然后調(diào)用getPoint方法即可,但是這種方案就沒有弊端了嗎?

設(shè)想一下幾個(gè)場(chǎng)景:

  1. 計(jì)算規(guī)則變了,211院校額外加25分。

  2. 新增了計(jì)算規(guī)則,對(duì)于有強(qiáng)基計(jì)劃的學(xué)校額外加10分。

如果發(fā)生這樣的需求變更的話,作為開發(fā)者的我們都知道,需求是肯定會(huì)變更的。。。當(dāng)需求變更了,我們必須要修改基類了,不是不可以,但是這樣做違背了軟件設(shè)計(jì)的開閉原則(類應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。)

那么怎么才能避免上面這些問題呢?這就需要用到本文介紹的設(shè)計(jì)模式了,裝飾者模式:

在不改變?cè)袑?duì)象的基礎(chǔ)之上,將功能附加到對(duì)象上。提供了比繼承更有彈性的替代方案(擴(kuò)展原有對(duì)象功能)

我們先來看一下裝飾者模式的類圖。

image.png

通過上面的類圖我們可以了解裝飾者模式的具體結(jié)構(gòu)樣式,把這個(gè)模式套用到大學(xué)評(píng)分系統(tǒng)中,可以得到如下的類圖。

image.png

在該系統(tǒng)中,四種不同的大學(xué)類型,是我們擴(kuò)展大學(xué)基類后獲得的不同類型院校,分別是普通本科,獨(dú)立學(xué)院,中外合辦還有高職專科。UnvFeature類是裝飾者的基類,用來標(biāo)識(shí)大學(xué)的綜合實(shí)力,也就是評(píng)分的標(biāo)準(zhǔn)。最下面四個(gè)是具體的裝飾者,分別用于標(biāo)識(shí)大學(xué)是否是211,985以及大學(xué)的碩士和博士點(diǎn)個(gè)數(shù)。

大學(xué)裝飾者模式的類我們都已經(jīng)實(shí)現(xiàn)好了,那么怎么才能計(jì)算出大學(xué)的得分呢?

image.png

比如我們新建一個(gè)天津大學(xué),然后分別用211,985和碩士博士的裝飾者對(duì)其進(jìn)行裝飾,這樣一來,我們最后得到的一個(gè)包裹了很多層,但是依然是University的對(duì)象,調(diào)用這個(gè)University的getPoint方法,那么就可以逐層計(jì)算得分,最后返回一個(gè)具體得分。

下面我們看一下具體的代碼實(shí)現(xiàn):

**
 * 大學(xué)基類
 */
public abstract class University {
    public abstract String getUnvName();
    public abstract int getPoint();
}
/**
 * 普通本科
 */
public class NormalUniversity extends University {
    String name;

    public NormalUniversity(String name) {
        this.name = name;
    }

    @Override
    public String getUnvName() {
        return name;
    }

    @Override
    public int getPoint() {
        return 10;
    }
}
/**
 * 大學(xué)特征裝飾者
 */
public abstract class UnvFeature extends University{
}
/**
 * 211院校
 */
public class Unv211 extends UnvFeature {
    University unv;

    public Unv211(University unv) {
        this.unv = unv;
    }

    @Override
    public String getUnvName() {
        return null;
    }

    @Override
    public int getPoint() {
        return unv.getPoint() + 20;
    }
}
/**
 * 碩士點(diǎn)
 */
public class UnvMaster extends UnvFeature {
    University unv;
    int masterNum;

    public UnvMaster(University unv, int num) {
        this.unv = unv;
        this.masterNum = num;
    }

    @Override
    public String getUnvName() {
        return null;
    }

    @Override
    public int getPoint() {
        return unv.getPoint() + masterNum;
    }
}

這里只列舉幾個(gè)特征類的實(shí)現(xiàn)方式,其他類的實(shí)現(xiàn)也與此類似,因篇幅問題不在具體列出。

那么我們計(jì)算一個(gè)大學(xué)的得分點(diǎn),只需要執(zhí)行如下代碼

University tju = new NormalUniversity("天津大學(xué)");
tju = new Unv211(tju);
tju = new Unv985(tju);
tju = new UnvMaster(tju, 39);
tju = new UnvDoctor(tju, 29);
int point = tju.getPoint();

到此,我們的大學(xué)評(píng)分系統(tǒng)設(shè)計(jì)完成?;剡^頭看一看我們上面的兩個(gè)問題是否得到解決:

問題1.如果211大學(xué)評(píng)分標(biāo)準(zhǔn)發(fā)生改變,那么我們只需要修改211大學(xué)這個(gè)裝飾者的類即可,不需要修改基類,也不需要變動(dòng)計(jì)算方法。

問題2.如果新增計(jì)算標(biāo)準(zhǔn),那么我們只需要新增一個(gè)裝飾者即可。比如新增雙一流大學(xué)可額外加10分,我們可添加如下新的裝飾者:

/**
 * 雙一流院校
 */
public class UnvDual extends UnvFeature {
    University unv;

    public UnvDual(University unv) {
        this.unv = unv;
    }

    @Override
    public String getUnvName() {
        return null;
    }

    @Override
    public int getPoint() {
        return unv.getPoint() + 10;
    }
}

對(duì)此,我們的問題得到圓滿解決。

裝飾者模式的在實(shí)際工作中的運(yùn)用

在Android開發(fā)過程中,我們最常用的四大組件之一就是Activity了,那你知道Activity中所包含的設(shè)計(jì)模式嗎?要搞清楚這一點(diǎn),我們先來看看Activity相關(guān)的類圖。

image.png

Context是個(gè)抽象類,它的唯一實(shí)現(xiàn)是ContextImpl。ContextWrapper通過包裝ContextImpl來實(shí)現(xiàn)擴(kuò)展,保證了ContextImpl類最原始的上下文特性。然后通過ContextWrapper的多態(tài)實(shí)現(xiàn),來生成系統(tǒng)組件Activity,Service,Application。ContextWrapper是Android系統(tǒng)中一個(gè)很重要的類,從名稱上不難看出就是使用了裝飾者模式。通過ContextWrapper構(gòu)造方法發(fā)現(xiàn)傳入了一個(gè)Context對(duì)象,并賦給mBase變量,ContextWrapper就是包裝了mBase。

public class ContextWrapper extends Context {
    Context mBase;

    public ContextWrapper(Context base) {
        mBase = base;
    }

    /**
     * Set the base context for this ContextWrapper.  All calls will then be
     * delegated to the base context.  Throws
     * IllegalStateException if a base context has already been set.
     *
     * @param base The new base context for this wrapper.
     */
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
    //......

而Application,Activity和Service都相當(dāng)于具體的裝飾者,他們分別具有自己的特性,同時(shí)又對(duì)Context進(jìn)行了包裝,因此他們都可以提供各種獲取系統(tǒng)環(huán)境的方法, 比如getResource, getPackageManager, getContentProvider等。

如果Android系統(tǒng)后期考慮添加新的系統(tǒng)組件,可以通過繼承ContextWrapper這個(gè)類,并且實(shí)現(xiàn)它自己應(yīng)有的功能,這樣不需要修改已經(jīng)很完善的Activity等類,就可以通過添加新的裝飾者實(shí)現(xiàn)新的需求。

裝飾者模式的優(yōu)點(diǎn)和缺點(diǎn)

1.繼承的有力補(bǔ)充,比繼承靈活,不改變?cè)袑?duì)象的情況下給一個(gè)對(duì)象擴(kuò)展功能。(繼承在擴(kuò)展功能是靜態(tài)的,必須在編譯時(shí)就確定好,而使用裝飾者可以在運(yùn)行時(shí)決定,裝飾者也建立在繼承的基礎(chǔ)之上的)

2.通過使用不同裝飾類以及這些類的排列組合,可以實(shí)現(xiàn)不同的效果。

3.符合開閉原則

同樣,裝飾者模式也是有一定的缺點(diǎn)的

1.會(huì)出現(xiàn)更多的代碼,更多的類,增加程序的復(fù)雜性。

2.動(dòng)態(tài)裝飾時(shí),多層裝飾時(shí)會(huì)更復(fù)雜。(使用繼承來拓展功能會(huì)增加類的數(shù)量,使用裝飾者模式不會(huì)像繼承那樣增加那么多類的數(shù)量但是會(huì)增加對(duì)象的數(shù)量,當(dāng)對(duì)象的數(shù)量增加到一定的級(jí)別時(shí),無疑會(huì)大大增加我們代碼調(diào)試的難度)

比如我們看一下JAVA中的裝飾者模式


image.png

當(dāng)我們第一次接觸InputSteam相關(guān)內(nèi)容時(shí),是不是很難理解每一個(gè)具體裝飾者的作用?只有當(dāng)我們對(duì)每一個(gè)具體InputSteam都進(jìn)行嘗試之后,才知道什么時(shí)候該用哪個(gè)去實(shí)現(xiàn)我們所需要的功能。

所以在程序設(shè)計(jì)中,要根據(jù)具體使用場(chǎng)景,選擇最適合的設(shè)計(jì)模式,發(fā)揚(yáng)其優(yōu)點(diǎn),盡量避免其缺點(diǎn)的擴(kuò)大。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容