1. 程式人生 > 實用技巧 >設計模式學習筆記(十二):裝飾模式

設計模式學習筆記(十二):裝飾模式

1 概述

1.1 引言

裝飾模式可以在不改變一個物件本身功能的基礎上給物件增加額外的新行為。比如,一張照片,不改變照片本身,增加一個相框。

裝飾模式是一種用於替代繼承的技術,無須定義子類即可給物件動態增加職責,使用物件之間的關聯關係來代替繼承關係,在裝飾模式中引入了裝飾類,在裝飾類中既可以呼叫待裝飾的原有類方法,還可以增加新的方法,以擴充原有的類功能。

1.2 定義

裝飾模式:動態地給物件增加一些額外的職責。

就增加物件功能來說,裝飾模式比生成子類實現更為靈活,裝飾模式是一種物件結構型模式。

1.3 結構圖

1.4 角色

  • Component(抽象構件類):是具體構件以及抽象裝飾類的父類,聲明瞭在具體構件中實現的業務方法,它的引入可以使客戶端以一致的方式處理未被裝飾之後的物件,以實現客戶端的透明操作
  • ConcreteComponent(具體構件類):是抽象構件類的子類,用於定義具體的構件物件,實現了在抽象構件中宣告的方法,裝飾器可以給它增加額外的職責
  • Decorator(抽象裝飾類):用於給具體構件類增加職責,但是具體職責在子類實現。抽象裝飾類維護一個指向抽象構件的引用,通過該引用可以呼叫裝飾之前構件物件的方法,並通過子類擴充套件該方法以達到裝飾的目的
  • ConcreteDecorator(具體裝飾類):負責向構件中新增新的職責,每一個具體裝飾類都定義了一些新的行為,可以呼叫抽象裝飾類中定義的方法,並可以增加新的職責用以擴充物件的行為

2 典型實現

2.1 步驟

  • 定義抽象構件類:可以是抽象類或者介面,宣告業務方法
  • 定義具體構件類:繼承或實現抽象構件,實現具體業務方法
  • 定義抽象裝飾類:繼承或實現抽象構件,增加一個抽象構件私有成員,通過該成員可以呼叫裝飾之前具體構件的方法
  • 定義具體裝飾類:繼承抽象裝飾類,並且增加裝飾行為,在裝飾之前呼叫具體構件方法,接著呼叫裝飾方法

2.2 抽象構件類

簡化只有一個業務方法:

abstract class Component
{
    abstract void operation();
}

2.3 具體構件類

繼承抽象構件:

class ConcreteComponent extends Component
{
    public void operation()
    {
        System.out.println("具體構件方法");
    }
}

2.4 抽象裝飾類

class Decorator extends Component
{
    private Component component;

    public Decorator(Component component)
    {
        this.component = component;
    }

    public void operation()
    {
        component.operation();
    }
}

抽象裝飾類需要包含一個抽象構件的私有成員,以便可以通過setter或構造方法注入不同的具體構件,同時在業務方法中方便呼叫具體構件未裝飾之前的方法。

2.5 具體裝飾類

class ConcreteDecorator extends Decorator
{
    public ConcreteDecorator(Component component)
    {
        super(component);
    }

    public void operation()
    {
        super.operation();
        newBehavior();
    }

    public void newBehavior()
    {
        System.out.println("裝飾方法");
    }
}

繼承抽象裝飾類,在業務方法中首先呼叫父類(抽象裝飾類)的方法再呼叫新的裝飾方法。

3 例項

設計一個圖形介面構件庫,具體構件有窗體,文字框以及列表框,裝飾方法包括新增滾動條與新增黑邊框,使用裝飾模式對系統進行設計。

設計如下:

  • 抽象構件類:Component
  • 具體構件類:Window+TextBox+ListBox
  • 抽象裝飾類:Decorator
  • 具體裝飾類:ScrollBarDecorator+BlackBorderDecorator
public class Test
{
    public static void main(String[] args) {
        Component component = new Window();
        Component decorator = new ScrollBarDecorator(component);
        decorator.display();
    }
}

abstract class Component
{
    abstract void display();
}

class Window extends Component
{
    public void display()
    {
        System.out.println("顯示視窗");
    }
}

class TextBox extends Component
{
    public void display()
    {
        System.out.println("顯示文字框");
    }
}

class ListBox extends Component
{
    public void display()
    {
        System.out.println("顯示列表框");
    }
}

class Decorator extends Component
{
    private Component component;

    public Decorator(Component component)
    {
        this.component = component;
    }

    public void display()
    {
        component.display();
    }
}

class ScrollBarDecorator extends Decorator
{
    public ScrollBarDecorator(Component component)
    {
        super(component);
    }

    public void display()
    {
        addScrollBar();
        super.display();
    }

    public void addScrollBar()
    {
        System.out.println("新增滾動條");
    }
}

class BlackBorderDecorator extends Decorator
{
    public BlackBorderDecorator(Component component)
    {
        super(component);
    }

    public void display()
    {
        addBlackBorder();
        super.display();
    }

    public void addBlackBorder()
    {
        System.out.println("新增黑邊框");
    }
}

輸出如下:

核心部分就是客戶端的程式碼:

Component component = new Window();
Component decorator = new ScrollBarDecorator(component);
decorator.display();

建立具體構件後,再建立具體裝飾器,把具體構件傳入具體裝飾器的構造方法中,這樣具體裝飾器就能在裝飾之後(在新增滾動條之後)呼叫具體構件的方法(呼叫顯示視窗)。

另外,如果向增加新的裝飾方法,比如增加了滾動條後,再增加黑邊框,只需要將”滾動條裝飾器“本身再裝飾一次:

Component component = new Window();
Component decorator = new ScrollBarDecorator(component);
decorator = new BlackBorderDecorator(decorator);
decorator.display();

也就是把已經對具體構件進行裝飾之後的具體裝飾器,注入到另一個具體裝飾器的構造方法再一次裝飾。

4 透明裝飾與半透明裝飾

4.1 透明裝飾模式

標準的裝飾模式就是透明裝飾,比如上述例子。在透明裝飾模式中,要求客戶端完全針對抽象構件程式設計,也就是將物件全部宣告為抽象構件型別,而不是具體構件型別或具體裝飾器型別。

透明裝飾模式的優點如下:

  • 客戶端透明地使用裝飾前以及裝飾後的物件,無須關心兩者區別
  • 能對已裝飾過的物件進行多次裝飾

在實現透明裝飾模式時,要求具體裝飾類的業務方法覆蓋抽象裝飾類的業務方法,需要呼叫原有具體構件物件的業務方法以及新增裝飾方法。

4.2 半透明裝飾模式

對於有時使用者需要單獨呼叫裝飾方法,這時候需要使用具體裝飾型別定義裝飾後的物件,而具體構件物件還是可以使用抽象構件定義,這種裝飾模式就叫半透明裝飾模式。對於客戶端來說:

  • 具體構件型別無需關心,是透明的
  • 具體裝飾型別必須指定,是不透明的

例子如下,修改上面的滾動條具體裝飾類:

class ScrollBarDecorator extends Decorator
{
    public ScrollBarDecorator(Component component)
    {
        super(component);
    }

    public void display()
    {
        super.display();
    }

    public void addScrollBar()
    {
        System.out.println("新增滾動條");
    }
}

其中addScrollBar由客戶端單獨呼叫:

Component component = new Window();
ScrollBarDecorator decorator = new ScrollBarDecorator(component);
decorator.display();
decorator.addScrollBar();

半透明裝飾可以帶來更大的靈活性,使用起來更加方便,客戶端可以單獨呼叫裝飾方法來進行裝飾,但是缺點就是不能對同一個物件進行多次裝飾,

5 注意事項

  • 保持介面相同:儘量保持裝飾類的介面與被裝飾類的介面相同,這樣對客戶端而言裝飾前/後的物件可以一致對待,也就是儘量使用透明裝飾模式
  • 減少具體構件行為:過多的行為不需要放在具體構件類中,通過具體裝飾類進行擴充套件
  • 去除抽象構件:如果只有一個具體構件類,那麼抽象裝飾類可以作為該具體構件類的直接子類,也就是說將原來的抽象構件用具體構件代替

6 主要優點

  • 動態擴充套件靈活:對於擴充套件一個物件的功能,裝飾模式比繼承更加靈活,不會導致類的個數急劇增加。通過選擇不同的具體裝飾類,可以動態擴充套件物件的行為
  • 多次裝飾:可以對一個物件進行多次裝飾,使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造很多不同行為的組合
  • 構件與裝飾類獨立變化:具體構件類以及具體裝飾類可以獨立變化,使用者可以根據需要增加新的具體構件類或者具體裝飾類,無須修改原有程式碼,符合開閉原則

7 主要缺點

  • 物件較多:使用裝飾模式會產生很多小物件,這些物件的區別在於相互連線方式的不同,小物件過多會一定程度上影響效能
  • 排查繁瑣:儘管裝飾模式比繼承更加靈活,但也意味著比繼承更加容易出錯,排錯也很困難,對於多次裝飾後的物件可能需要逐級排查

8 適用場景

  • 在不影響其他物件的情況下,以動態和透明的方式給單個物件增加職責
  • 在不能採用繼承擴充套件系統或者採用繼承不利於對系統擴充套件和維護時可以使用裝飾模式

9 總結