設計模式之裝飾器模式(decorator pattern)

alpha_panda發表於2019-06-30

裝飾器模式主要對現有的類物件進行包裹和封裝,以期望在不改變類物件及其類定義的情況下,為物件新增額外功能。是一種物件結構型模式。需要注意的是,該過程是通過呼叫被包裹之後的物件完成功能新增的,而不是直接修改現有物件的行為,相當於增加了中間層。類似於python中的@裝飾器。

下面還是按照老規矩,先來了解一下該模式相關的概念和原理,然後通過兩個具體的例項體會一下如何在實際開發中應用該模式。

1. 目的

可以動態的為同一類的不同物件加以修飾以新增新的功能。

2. 動機

靈活的對類物件功能進行擴充套件。

3. 優缺點

優點:

  1. 相比較於類的繼承來擴充套件功能,對物件進行包裹更加的靈活;
  2. 裝飾類和被裝飾類相互獨立,耦合度較低;

缺點:

  1. 沒有繼承結構清晰;
  2. 包裹層數較多時,難以理解和管理;

4. 應用場景

  • 動態的增加物件的功能;
  • 不能以派生子類的方式來擴充套件功能;
  • 限制物件的執行條件;
  • 引數控制和檢查等;

5.  原理

下面是GoF介紹的典型的裝飾器模式的UML類圖:

Component:

 物件的介面類,定義裝飾物件和被裝飾物件的共同介面;

ConcreteComponent:

 被裝飾物件的類定義;

Decorator:

 裝飾物件的抽象類,持有一個具體的被修飾物件,並實現介面類繼承的公共介面;

ConcreteDecorator:

 具體的裝飾器,負責往被裝飾物件新增額外的功能;

說明:

 由於這個模式從實際的例子來理解更加的直觀方便,因此這裡不再單獨的實現上面的UML結構程式碼。

6.例項——畫圖

先來通過一個簡單的畫圖的例項來直觀感受一下。

前提:

系統中存在一個畫圓的類,該類只是用來畫圓,以及其他一些大小和位置等引數的控制。

新加需求:

  • 可以對圓的邊進行著色
  • 可以對圓填充顏色;
  • 可以同時對邊和內部著色;

這個需求的常規方法實現可能如下:

  1. 對畫圓類進行迭代,以支援邊和內部顏色填充 ;
  2. 畫圓類作為父類,分別定義三個子類,繼承父類的畫圓方法,子類分別實現對應的作色需求;

上面的兩個方法都是可行的,也是比較直觀的,這裡我們嘗試使用裝飾器模式來實現,作為以上兩種方法的對比。

下面來看一下裝飾器模式實現該需求的UML類圖:

介面類:shape

public interface Shape {
    void draw();
}

 畫圓類:Circle

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.print("a circle!");
    }
}

抽象裝飾器類:Decorator

public abstract class Decorator implements Shape {
    
    protected Shape circle;
    
    public Decorator(Shape shape) {
        circle = shape;
    } 
    
    public void draw() {
        circle.draw();
    }
}

為圓邊著色裝飾器類:CircleEdge

public class CircleEdge extends Decorator {
    
    public CircleEdge(Shape circle) {
        super(circle);
    }
    
    private void setEdgeColor() {
        System.out.print(", edge with color");
    }
    
    public void draw() {
        circle.draw();
        setEdgeColor();
    }
}

為圓填充顏色裝飾器類:CircleEdge

public class CircleFill extends Decorator {
    
    public CircleFill(Shape circle) {
        super(circle);
    }
    
    private void setEdgeFill() {
        System.out.print(", content with color");
    }
    
    public void draw() {
        circle.draw();
        setEdgeFill();
    }
}

演示:

public class Demo {
    public static void main(String[] args) {
        Shape circle = new Circle();
        circle.draw();
        System.out.println("");
        Decorator circleEdge = new CircleEdge(circle);
        circleEdge.draw();
        System.out.println("");
        Decorator circleFill = new CircleFill(circle);
        circleFill.draw();
        System.out.println("");
        Decorator circleEdgeFill = new CircleFill(circleEdge);
        circleEdgeFill.draw();
    }
}

結果:

a circle!
a circle!, edge with color
a circle!, content with color
a circle!, edge with color, content with color

 上面我們通過實現兩個裝飾器分別完成對邊著色和填充的需求,通過對裝飾器的進一步裝飾,我們完成了同時著色的需求。

7.例項——網路資料包封裝

接下來我們在使用網路資料傳輸的例子來體會一下裝飾器模式,下圖表示的是應用層的檔案傳輸協議FTP通過TCP來傳輸資料:

雖然應用層可以越過傳輸層直接使用網路層進行資料傳送(如,ICMP),但多數都會使用傳輸層的TCP或者UDP進行資料傳輸的。

下面我們用裝飾器模式來表示一下應用層資料通過傳輸層來傳送資料,UML類圖如下:

上述圖中表示了,應用層的資料通過新增TCP頭或者UDP頭,然後通過下面的網路層send資料。

 資料包介面類:Datagram

public interface Datagram {
    void send();    // 通過網路層傳送IP資料包
}

應用層資料類:AppDatagram

public class AppDatagram implements Datagram {
    @Override
    public void send() {
        System.out.print("send IP datagram!");
    }
}

傳輸層類(抽象裝飾器):TransportLayer

public abstract class TransportLayer implements Datagram {
    
    protected Datagram appData;
    
    public TransportLayer(Datagram appData) {
        this.appData = appData;
    } 
    
    public void send() {
        appData.send();
    }
}

新增TCP頭部類:UseTCP

public class UseTCP extends TransportLayer {
    
    public UseTCP(Datagram appData) {
        super(appData);
    }
    
    private void addHeader() {
        System.out.print("Appdata add TCP header, ");
    }
    
    public void send() {
        addHeader();
        appData.send();
    }
}

新增TCP頭部類:UseUDP

public class UseUDP extends TransportLayer {
    
    public UseUDP(Datagram appData) {
        super(appData);
    }
    
    private void addHeader() {
        System.out.print("Appdata add UDP header, ");
    }
    
    public void send() {
        addHeader();
        appData.send();
    }
}

演示:

public class Demo {
    public static void main(String[] args) {
        Datagram appData = new AppDatagram();
        appData.send();
        System.out.println("");
        TransportLayer tcpData = new UseTCP(appData);
        tcpData.send();
        System.out.println("");
        TransportLayer udpData = new UseUDP(appData);
        udpData.send();
        System.out.println("");
    }
}

結果:

send IP datagram!
Appdata add TCP header, send IP datagram!
Appdata add UDP header, send IP datagram!

當然這裡例子中已經新增過TCP頭部的資料包不能再使用UDP傳輸了,無意義,也被必要。

8. 總結

其實所謂裝飾器,本質上是對現有類物件的包裹,得到一個加強版的物件。

和python中@裝飾器不同的是:

  1. python中的裝飾器是作用於函式或者類定義的,並直接覆蓋掉了原來函式或者類的定義;
  2. 裝飾器模式僅僅是修改了了已經產生的物件的行為,和類定義沒有半點關係;

通過上面的兩個例子,應該對裝飾器模式有了一個簡單的認識。

另外,要體會到什麼時候用繼承什麼時候用裝飾器。

參考:

GoF《Design Patterns: Elements of Reusable Object-Oriented Software》

https://www.runoob.com/design-pattern/decorator-pattern.html

相關文章