關於Decorator裝飾器模式巢狀包裝的個人的改進設想

Senolytics發表於2024-05-24

本文系筆者在學習軟體構造課程期間所寫,不保證通用性和正確性,僅供參考。
基於課程要求,本文所涉及語言為Java。

目錄

  1. Decorator介紹
  2. 內部呼叫法
  3. 列表構造法
  4. 在Decorator中嵌入Visitor
  5. 結語

一、Decorator介紹

裝飾器模式(Decorator Pattern)允許向一個現有的物件新增新的功能,同時又不改變其結構。這種型別的設計模式屬於結構型模式,作為現有的類的一個包裝,裝飾器模式透過將物件包裝在裝飾器類中,動態地修改其行為。這種模式建立了一個裝飾類,用來包裝原有的類,並在保持類方法簽名完整性的前提下,提供了額外的功能。

可以認為主要是用到了委託的思想,委託另外一個類完成介面要求的功能,而自己做的是在另外一個類工作的基礎上進行行為修改。

下文中我們將以一個普通的列表為例進行改進說明。這個列表非常簡單,只有新增一個int的功能。舉例中沒有具體實現,都用列印資訊來代替。

首先是基本的介面:

public interface List {
    void add(int i);
}

然後,有一個實現這個介面的最基本的類:

public class BaseList implements List{
    @Override
    public void add(int i){
        System.out.println("BL: add " + i);
    }
}

接下來定義一個ListDecorator抽象類,它透過委託另外一個List來實現List介面。

public abstract class ListDecorator implements List{
    protected List list;

    public ListDecorator(List list){
        this.list = list;
    }

    @Override
    public void add(int i) {
        list.add(i);
    }
}

二、內部呼叫法

現在我們實現兩個Decorator,一個是UndoList,它可以記錄新增歷史,並可以呼叫undo()方法撤銷上一次add()操作。

class UndoList extends ListDecorator{
    public UndoList(List list) {
        super(list);
    }

    @Override
    public void add(int i) {
        list.add(i);
        System.out.println("UL: record this add");
    }

    public void undo() {
        System.out.println("UL: last add has been removed.");
    }
}

還有一個是ShoutList,它並不能幹什麼,只會大叫,你可以呼叫SHOUT()方法來讓它大叫一下。

class ShoutList extends ListDecorator{
    public ShoutList(List list) {
        super(list);
    }

    @Override
    public void add(int i) {
        list.add(i);
        System.out.println("SL: HEY GUYS I JUST ADD " + i + " TO MY LIST!!!");
    }

    public void SHOUT(){
        System.out.println("SL: SHOOOOOOOUT!!!!!");
    }
}

現在如果我們希望構建一個既可以撤銷又可以大叫的列表,那麼可以這麼寫:

UndoList myList = new UndoList(new ShoutList(new BaseList()));

但是這樣有個問題:因為ShoutList被巢狀在了裡面,我就沒法讓這個列表大叫了,這不好。

當然,可以先在其他地方把ShoutList構造出來,再巢狀到myList裡面,想大叫的時候呼叫那個ShoutList即可,但這樣終究不太優雅,怎樣直接呼叫myList就可以大叫呢?

注意到ListDecorator中,委託的list是protected的,所以在內部我們可以沿著list一直深入,直到底層。這樣,當向內部找到ShoutList的時候,讓它大叫不就可以了嗎?用這個樸素的思想,我們為ListDecorator新增兩個方法:

private List inner(){
    return list;
}

public void call(MethodType method){
    List toCall = this;
    switch(method){
        case Undo:
            while(toCall instanceof ListDecorator){
                if(!(toCall instanceof UndoList)){
                    toCall = ((ListDecorator) toCall).inner();
                }
                else{
                    ((UndoList) toCall).undo();
                    break;
                }
            }
            break;
        case SHOUT:
            while(toCall instanceof ListDecorator){
                if(!(toCall instanceof ShoutList)){
                    toCall = ((ListDecorator) toCall).inner();
                }
                else{
                    ((ShoutList) toCall).SHOUT();
                    break;
                }
            }
            break;
    }
}

解釋一下:

inner()返回這個decorator委託的內部list,由於我們不希望使用者直接呼叫這個方法,因此將其設為private。

call()呼叫一個方法,以MethodType方法型別作為引數。這是一個列舉型別,可以定義在介面裡或者其他什麼地方,當然用字串也可以,只是作為方法的標識而已。然後從本身開始,逐步往內檢查有沒有想要的型別,如果有,就呼叫它對應的方法。如果沒有想要的型別,可以拋異常可以返回false,這裡就不列了。當有新方法時,只需要增加一個列舉和一個switch內的case即可。

三、列表構造法

現在我們再來看一下構造這個列表的語句:

UndoList myList = new UndoList(new ShoutList(new BaseList()));

感覺也有些不太美觀。如果這是一個給使用者的API,要是使用者可以直接在構造的時候把想要的型別作為引數傳給構造方法,那看起來就直觀多了。我們還是可以用類似上面的思想來實現。定義一個新類ConcreteList:

public class ConcreteList implements List{
    private List list;

    public ConcreteList(){
        list = new BaseList();
    }
    public ConcreteList(DecoratorType... decoratorTypes){
        list = new BaseList();
        for(DecoratorType type : decoratorTypes){
            switch (type){
                case ShoutList:
                    list = new ShoutList(list);
                    break;
                case UndoList:
                    list = new UndoList(list);
                    break;
            }
        }
    }

    @Override
    public void add(int i) {
        list.add(i);
    }

    public void call(MethodType method){
        if(list instanceof ListDecorator){
            ((ListDecorator) list).call(method);
        }
    }
}

這裡用了"..."這一語法糖,構造時可以傳入任意數量的DecoratorType。這也是一個列舉型別,標識各個decorator。然後遍歷所有引數,每次都包裝上一個decorator即可。

現在再來構造一個又能撤銷又能大叫的列表,看起來漂亮很多:

ConcreteList myList = new ConcreteList(DecoratorType.ShoutList, DecoratorType.UndoList);
myList.call(MethodType.SHOUT);
myList.call(MethodType.Undo);

四、與Visitor並行使用

Decorator模式還有一個問題是新包裹的類只能在內部委託類完成其工作的前後進行額外的操作,而不能在其工作中間插入操作。從思想上來說這其實並不是一個問題,因為decorator的思想就是基於內部已經封裝好的前提下進行裝飾,並不考慮內部實現。但如果真的想在內部實現的中間插入一段操作,則可以利用visitor模式進行處理。

我們希望實現一個SmellyList,它讓列表中實際存入的資料是輸入資料再加上114514。就這個例子而言它是可以單純用decorator實現的,但是我們試用visitor來解決。

首先定義visitor類的介面AddVisitor:

public interface AddVisitor {
    int value(int i);
}

還有一個最基本的visitor,它將傳入的資料原封不動地返回:

public class BaseAdd implements AddVisitor{
    @Override
    public int value(int i) {
        return i;
    }
}

將BaseList的add過程新增上visitor的介入:

public class BaseList implements List{
    private AddVisitor addVisitor;

    public BaseList(){
        addVisitor = new BaseAdd();
    }

    public void setVisitor(AddVisitor addVisitor){
        this.addVisitor = addVisitor;
    }

    @Override
    public void add(int i){
        i = addVisitor.value(i);
        System.out.println("BL: add " + i);
    }
}

我們很容易定義一個SmellyAdd:

public class SmellyAdd implements AddVisitor{
    @Override
    public int value(int i) {
        return i + 114514;
    }
}

這樣,構造一個BaseList,再setVisitor(new SmellyAdd()),就可以達到我們想要的效果。綜合第二節和第三節的內容,可以比較容易地把visitor也封裝到ConcreteList中,對使用者而言這兩者就不存在什麼區別了。另外,visitor本身也可以巢狀使用decorator模式,使單一的一個visitor可以裝飾上豐富的功能。

但是這樣也有問題:例如在本例中,雖然存入的資料改變了,但是UndoList和ShoutList獲得的資料並沒有改變,這就容易導致程式出現預料外的bug,不便於規範地進行程式碼實現。因此,這一節的內容實際上可能並沒有太大的實用意義,只是作為一個頭腦風暴吧。

五、結語

以上是我在第一次接觸decorator後,自己瞎想得出來的方案。這種做法是早就有了並且很通用,還是有更好的解決方法,還是非常簡單完全不值一提,亦或並不優雅不建議使用,我一概不知。所以也不敢說多有用吧,看個樂就好~

相關文章