天天看點

Decorator(裝飾器) 模式

12.1 Decorator 模式

  假如有一塊蛋糕,如果隻塗上奶油,其他什麼也不加,就是奶油蛋糕。如果加上草莓,就是草莓奶油蛋糕。如果再加上一塊黑色巧克力闆,上面用白色巧克力寫上姓名,然後插上蠟燭,就變成了一塊生日蛋糕。

  不論是蛋糕、奶油蛋糕、草莓奶油蛋糕,他們的核心都是蛋糕。不過,經過一系列裝飾後,蛋糕的味道變得更加甜美了,目的也變得更加明确了。

  程式中的對象與蛋糕十分相似。首先有一個相當于蛋糕的對象,然後不斷地裝飾蛋糕一樣地不斷地對齊增加功能,它就程式設計了使用目的更加明确的對象。

  像這樣不斷地為對象添加裝飾的設計模式被稱為 Decorator 模式。

12.2 示例程式

  示例程式的功能是給文字添加裝飾邊框。指用 “-”、“\”、“+”、“|” 組成的邊框。

Decorator(裝飾器) 模式

示例程式類圖

|| Display 類

  Display 類是可以顯示多行字元串的抽象類。

  getColumns 方法和 getRows 方法分别用于擷取橫向字元數和縱向行數。都是抽象方法需要子類實作。getRowText 方法使用者擷取指定的某一行的字元串。

  show 是顯示所有行的字元串方法。在show 内部,程式會調用 getRows 方法擷取行數,調用 getRowText 方法擷取改行需要顯示的字元串,然後循環顯示字元串。屬于 Template Method 模式。

public abstract class Display {

    public abstract int getColumns(); // 擷取橫向字元數

    public abstract int getRows(); // 擷取行數

    public abstract String getRowText(int row); // 擷取 row 行的字元串

    public void show() { // 列印所有
        for (int i = 0; i < getRows(); i++) {
            System.out.println(getRowText(i));
        }
    }
}
           
|| StringDisplay 類

  StringDisplay 類是用于顯示單行字元串的類。是 Display 的子類,是以肩負着實作 Display 類中聲明的抽象方法的重任。

  string 字段中儲存的是要顯示的字元串。由于 StringDisplay 類隻顯示一行字元串,是以 getColumns 傳回 string.getBytes().length 的值,getRows 方法傳回固定值1。

  僅當要擷取第 0 行時 getRowText 方法才會傳回 string 字段。以本章開頭的蛋糕的比喻來說, StringDisplay 類就相當于生日蛋糕中的核心蛋糕。

/**
* 顯示核心類.
* 隻顯示一行字元串。
*/
public class StringDisplay extends Display{

    private String string;

    public StringDisplay(String string) {
        this.string = string;
    }

    @Override
    public int getColumns() {
        return string.length();
    }

    @Override
    public int getRows() {
        return 1;
    }

    @Override
    public String getRowText(int row) {
        if (row == 0) {
            return string;
        }
        return null;
    }
}
           
|| Border 類

  Border 類是裝飾邊框的抽象類。雖然它所表示的是裝飾邊框,但它也是 Display 的子類。

  也就是說,通過繼承,裝飾邊框與被裝飾物具有了相同的方法。具體而言,Border 類繼承了父類的各方法。從接口角度而言,裝飾邊框與被裝飾物具有相同的方法也就意味着它們具有一緻性。

  在裝飾邊框 Border 類中有一個 Display 類型的 display 字段,它表示裝飾物。不過,其所表示的裝飾物并不僅限于 StringDisplay 的執行個體。因為 Border 也是 Display 的子類,display 字段也可以表示其他的裝飾邊框,而且那個邊框中也有一個 display 字段。

/**
* 裝飾邊框的抽象類.
*/
public abstract class Border extends Display {

    protected Display display;

    protected Border(Display display) {
        this.display = display;
    }

}
           
|| SideBorder 類

  SideBorder 類是一種具體的裝飾邊框,是 Border 類的子類。SideBorder 類是指定的字元裝飾字元串的左右兩側。例如,字元為 ‘|’

|被裝飾物|

  getColumns 方法是用于擷取橫向字元數的方法。隻需要在被裝飾物的字元數的基礎上,再加上兩側邊框的字元數即可。

1 + display.getColumns() + 1

  因為 SideBorder 類并不會在字元串的上下兩側添加字元,是以 getRows 方法直接傳回 display.getRows() 即可。

  getRowText 方法可以擷取參數指定的那一行的字元數,是以,像下面這樣,在 display.getRowText(row) 的字元串兩側,加上 borderchar 這個裝飾邊框即可。

borderchar + display.getRowText(row) + borderchar

/**
* 具體的裝飾器-在兩端加上字元
*/
public class SideBorder extends Border {

    private char borderChar;

    protected SideBorder(Display display, char ch) {
        super(display);
        this.borderChar = ch;
    }

    @Override
    public int getColumns() {
        return 1 + display.getColumns() + 1; // 因為在兩端加上字元,可以表示為 1 + 字元數 + 1
    }

    @Override
    public int getRows() {
        return display.getRows(); // 未改變 Row
    }

    @Override
    public String getRowText(int row) {
        return borderChar + display.getRowText(row) + borderChar;
    }
}
           
|| FullBorder 類

  FullBorder 類與 sideBorder 類相似。其會在字元串的上下左右都加上裝飾邊框。

/**
* 在字元串的上下左右都加上裝飾邊框.
* +-----+
* |Hello|
* +-----+
*/
public class FullBorder extends Border {

    public FullBorder(Display display) {
        super(display);
    }

    @Override
    public int getColumns() {
        return 1 + display.getColumns() + 1;
    }

    @Override
    public int getRows() {
        return 1 + display.getRows() + 1;
    }

    @Override
    public String getRowText(int row) {
        if (row == 0) {
            return '+' + makeLine('-', display.getColumns()) + '+'; // 上邊框
        } else if (row == display.getRows() + 1) {
            return '+' + makeLine('-', display.getColumns()) + '+'; // 下邊框
        } else {
            return '|' + display.getRowText(row - 1) + '|'; // 其他邊框
        }
    }

    // 用于連續的顯示某個字元
    private String makeLine(char ch, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(ch);
        }
        return sb.toString();
    }
}
           
|| Main 類

  用于測試程式行為的類。b4 用于加多重邊框。

public class Main {
    public static void main(String[] args) {
        Display b1 = new StringDisplay("Hello, world.");
        Display b2 = new SideBorder(b1, '#');
        Display b3 = new FullBorder(b2);
        b1.show();
        b2.show();
        b3.show();
        Display b4 = new SideBorder(
                new FullBorder(
                        new FullBorder(
                                new SideBorder(
                                        new FullBorder(
                                                new StringDisplay("Hello, World.")
                                        ), '*'
                                )
                        )
                ), '/'
        );
        b4.show();
    }
}
           

運作結果:

Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+
/+-------------------+/
/|+-----------------+|/
/||*+-------------+*||/
/||*|Hello, World.|*||/
/||*+-------------+*||/
/|+-----------------+|/
/+-------------------+/
           

12.3 Decorator 模式中登場角色

  在 Decorator 模式中有以下登場角色。

  ◆ Component

  增加功能時的核心角色。以開頭場景為例,裝飾前的蛋糕就是 Component 角色。Component 角色隻是定義了蛋糕的接口(API)。在示例程式中,由 Display 類扮演此角色。

  ◆ ConcreteComponent

  該角色是實作了 Component 角色所定義的接口的具體蛋糕。在示例程式中,由 StringDisplay 類扮演此角色。

  ◆ Decorator(裝飾物)

  該角色具有與 Component 角色相同的接口。在它内部儲存了被裝飾對象 - Component 角色。Decorator 角色知道自己要裝飾的對象。在示例程式中,由 Border 類扮演此角色。

  ◆ ConcreteDecorator (具體的裝飾物)

  該角色是具體的 Decorator 角色。在示例程式中,由 SideBorder 類和 FullBorder 類扮演此角色。

Decorator(裝飾器) 模式

Decorator 模式的類圖

12.4 拓展思路的要點

|| 接口(API)的透明性

  在 Decorator 模式中,裝飾邊框與被裝飾物具有一緻性。具體而言,在示例程式中,表示裝飾邊框的 Border 類是表示被裝飾物的 Display 類的子類,這就展現了它們之間的一緻性。也就是說 Border 類(及其子類)與表示被裝飾物的 Display 類具有相同的接口。

  這樣,即使被裝飾物被邊框裝飾起來了,接口(API)也不會被隐藏起來。其他類依然可以調用 getColumns、getRows、getRowText 以及 show 方法。這就是接口(API)的透明性。

  得益于接口的透明性,Decorator 模式中也形成了類似于 Composite 模式中的遞歸結構。也就是說,裝飾邊框裡面的 “被裝飾物” 實際上又是别的物體的 “裝飾邊框” 。不過,Decorator 模式雖然與 Composite 模式一樣,都具有遞歸結構,但是它們的使用目的不同。Decorator 模式主要的是通過添加裝飾物來增加對象的功能。

|| 在不改變被裝飾物的前提下增加功能

  在 Decorator 模式中,裝飾邊框與被裝飾物具有相同的接口(API)。雖然接口是相同的,但裝飾越多,功能則越多。例如,用 SideBorder 裝飾 Display 後,就可以在字元串的左右兩側加上裝飾字元。如果再用 FullBorder 裝飾,那麼就可以在字元串的四周加上邊框。此時,我們完全不需要對被裝飾的類做任何修改。這樣,我們就實作了不修改被裝飾的類即可增加功能。

|| 可以動态的增加功能

  Decorator 模式中用到了委托,它使類之間形成了弱關聯關系。是以,不用改變架構代碼,就可以生成一個與其他對象具有不同關系的新對象。

|| 隻需要一些裝飾物即可添加許多功能

  使用 Decorator 模式可以為程式添加許多功能。隻要準備一些裝飾邊框,即使這些裝飾邊框都隻具有非常簡單的功能,也可以将它們自由組合成為新的對象。

  就像我們可以自由選擇香草味冰激淩、巧克力冰激淩、草莓味冰激淩一樣。如果冰激淩店要為顧客準備冰激淩成品那就太麻煩了。是以,冰激淩店隻會準備各種香料,當顧客下單後隻需要在冰激淩上加上各種香料就可以了。Decorator 模式就是可以應對這種多功能對象的需求的一種模式。

|| java.io 包與 Decorator 模式

  java.io 包是用于輸入輸出的包。在其中,使用了 Decorator 模式。

  首先,可以像下面這樣生成一個讀取檔案的執行個體。

Render render = new FileReader("data.txt");
           

  然後,像下面這樣在讀取檔案時将檔案内容放入緩沖區。

Reader reader = new BufferedReader(new FileReader("data.txt"));
           

  這樣,在生成 BufferedReader 類的執行個體時,會指定将檔案讀取到 FileReader 類的執行個體中。

  在然後,我們可以想下面這樣管理行号。

Reader reader = new LineNumberReader(new BufferedReader(new FileReader("data.txt")));
           

  可以看出,無論是 LineNumberReader 還是 BufferedReader 類的構造器,都可以接收 Reader 類的執行個體作為參數。

  除了 java.io 包以外,還在 javax.swing.border 包中使用了 Decorator 模式。其為我們提供了可為界面中的控件添加裝飾邊框的類。

|| 導緻增加許多很小的類

  Decorator 模式的一個缺點就是會導緻程式增加許多功能類似的很小的類。

12.6 延伸閱讀:繼承和委托中的一緻性

  我們再稍微了解一下 “一緻性”,即 “可以将不同的東西當作同一種東西看待” 的知識。

|| 繼承-父類與子類的一緻性

  子類和父類具有一緻性。如:

class Parent {
    ...
    void parentMethod() {
        ...
    }
}

class Child extends Parent {
    ...
    void childMethod() {
        ...
    }
}
           

  此時,Child 類的執行個體可以儲存在 Parent 類型的變量中,也可以調用從 Parent 類中繼承的方法。也就是說,可以像操作 Parent 類的執行個體一樣操作 Child 類的執行個體。這是将子類當作父類看待。

  但是,反過來,将父類當作子類一樣操作,則需要先進行類型轉換(必須是子類的對象)。

Parent obj = new Child();
((Child)obj).childMethod();
           
|| 委托-自己和被委托對象的一緻性

  使用委托讓接口具有透明性,自己和被委托對象具有一緻性。

  下面我們來看一個稍微有點生硬的例子。

class Rose {
    Violet obj = ...
    void method() {
        obj.method();
    }
}

class Violet {
    void method() {
        ...
    }
}
           

  Rose 和 Violet 都有相同的 method 方法。Rose 将 method 方法的處理委托給了 Violet。這樣會讓人有一種好像這兩個類有所關聯,又好像沒有關聯的感覺。

  因為這兩個類雖然都有 method 方法,但是卻沒有明确地在代碼中提現出這個 “共通性”。如果要明确地表示 method 方法是共通的,隻需要像下面這樣編寫一個共通的抽象類 Flower 就可以了。

abstract class Flower {
    abstract void method();
}

class Rose extends Flower {
    Violet obj = ...
    void method() {
        obj.method();
    }
}

class Violet extends Flower {
    void method() {
        ...
    }
}
           

  或是将 Flower 作為接口也行。

interface Flower {
    void method();
}
           

  至此,可能會産生這樣的疑問,即 Rose 類中的 Obj 字段被指定為具體的 Violet 真的好嗎?如果指定為抽象類型 Flower 會不會更好呢?究竟應該怎麼做才好,其實沒有固定的答案,需求不同,做法也會不同。

繼續閱讀