趣談裝飾器模式,讓你一輩子不會忘

語言: CN / TW / HK

本文節選自《設計模式就該這樣學》

1 使用裝飾器模式解決煎餅加碼問題

來看這樣一個場景,上班族大多有睡懶覺的習慣,每天早上上班都時間很緊張,於是很多人為了多睡一會兒,就用更方便的方式解決早餐問題,有些人早餐可能會吃煎餅。煎餅中可以加雞蛋,也可以加香腸,但是不管怎麼加碼,都還是一個煎餅。再比如,給蛋糕加上一些水果,給房子裝修,都是裝飾器模式。

下面用程式碼來模擬給煎餅加碼的業務場景,先來看不用裝飾器模式的情況。首先建立一個煎餅Battercake類。

```java

public class Battercake {

protected String getMsg(){
    return "煎餅";
}

public int getPrice(){
    return 5;
}

}

```

然後建立一個加雞蛋的煎餅BattercakeWithEgg類。

```java

public class BattercakeWithEgg extends Battercake{ @Override protected String getMsg() { return super.getMsg() + "+1個雞蛋"; }

@Override
//加1個雞蛋加1元錢
public int getPrice() {
    return super.getPrice() + 1;
}

}

```

再建立一個既加雞蛋又加香腸的BattercakeWithEggAndSausage類。

```java

public class BattercakeWithEggAndSausage extends BattercakeWithEgg{ @Override protected String getMsg() { return super.getMsg() + "+1根香腸"; }

@Override
//加1根香腸加2元錢
public int getPrice() {
    return super.getPrice() + 2;
}

}

```

最後編寫客戶端測試程式碼。

```java

public static void main(String[] args) {

    Battercake battercake = new Battercake();
    System.out.println(battercake.getMsg() + ",總價格:" + battercake.getPrice());

    Battercake battercakeWithEgg = new BattercakeWithEgg();
    System.out.println(battercakeWithEgg.getMsg() + ",總價格:" + 
        battercakeWithEgg.getPrice());

    Battercake battercakeWithEggAndSausage = new BattercakeWithEggAndSausage();
    System.out.println(battercakeWithEggAndSausage.getMsg() + ",總價格:" + 
        battercakeWithEggAndSausage.getPrice());

}

```

執行結果如下圖所示。

file

執行結果沒有問題。但是,如果使用者需要一個加2個雞蛋和1根香腸的煎餅,則用現在的類結構是建立不出來的,也無法自動計算出價格,除非再建立一個類做定製。如果需求再變,那麼一直加定製顯然是不科學的。 下面用裝飾器模式來解決上面的問題。首先建立一個煎餅的抽象Battercake類。

```java

public abstract class Battercake { protected abstract String getMsg(); protected abstract int getPrice(); }

```

建立一個基本的煎餅(或者叫基礎套餐)BaseBattercake。

```java

public class BaseBattercake extends Battercake { protected String getMsg(){ return "煎餅"; }

public int getPrice(){ return 5;  }

}

```

然後建立一個擴充套件套餐的抽象裝飾器BattercakeDecotator類。

```java

public abstract class BattercakeDecorator extends Battercake { //靜態代理,委派 private Battercake battercake;

public BattercakeDecorator(Battercake battercake) {
    this.battercake = battercake;
}
protected abstract void doSomething();

@Override
protected String getMsg() {
    return this.battercake.getMsg();
}
@Override
protected int getPrice() {
    return this.battercake.getPrice();
}

}

```

接著建立雞蛋裝飾器EggDecorator類。

```java

public class EggDecorator extends BattercakeDecorator { public EggDecorator(Battercake battercake) { super(battercake); }

protected void doSomething() {}

@Override
protected String getMsg() {
    return super.getMsg() + "+1個雞蛋";
}

@Override
protected int getPrice() {
    return super.getPrice() + 1;
}

}

```

建立香腸裝飾器SausageDecorator類。

```java

public class SausageDecorator extends BattercakeDecorator { public SausageDecorator(Battercake battercake) { super(battercake); }

protected void doSomething() {}

@Override
protected String getMsg() {
    return super.getMsg() + "+1根香腸";
}
@Override
protected int getPrice() {
    return super.getPrice() + 2;
}

}

```

再編寫客戶端測試程式碼。

```java

public class BattercakeTest { public static void main(String[] args) { Battercake battercake; //買一個煎餅 battercake = new BaseBattercake(); //煎餅有點小,想再加1個雞蛋 battercake = new EggDecorator(battercake); //再加1個雞蛋 battercake = new EggDecorator(battercake); //很餓,再加1根香腸 battercake = new SausageDecorator(battercake);

    //與靜態代理的最大區別就是職責不同
    //靜態代理不一定要滿足is-a的關係
    //靜態代理會做功能增強,同一個職責變得不一樣

    //裝飾器更多考慮的是擴充套件
    System.out.println(battercake.getMsg() + ",總價:" + battercake.getPrice());
}

}

```

執行結果如下圖所示。

file

最後來看類圖,如下圖所示。

file

2 使用裝飾器模式擴充套件日誌格式輸出

為了加深印象,我們再來看一個應用場景。需求大致是這樣的,系統採用的是SLS服務監控專案日誌,以JSON格式解析,因此需要將專案中的日誌封裝成JSON格式再列印。現有的日誌體系採用Log4j + Slf4j框架搭建而成。客戶端呼叫如下。

```java

private static final Logger logger = LoggerFactory.getLogger(Component.class); logger.error(string);

```

這樣打印出來的是毫無規則的一行行字串。當考慮將其轉換成JSON格式時,筆者採用裝飾器模式。目前有的是統一介面Logger和其具體實現類,筆者要加的就是一個裝飾類和真正封裝成JSON格式的裝飾產品類。建立裝飾器類DecoratorLogger。

```java

public class DecoratorLogger implements Logger {

public Logger logger;

public DecoratorLogger(Logger logger) {

    this.logger = logger;
}

public void error(String str) {}

public void error(String s, Object o) {

}
//省略其他預設實現

}

```

建立具體元件JsonLogger類。

```java

public class JsonLogger extends DecoratorLogger { public JsonLogger(Logger logger) { super(logger); }

@Override
public void info(String msg) {

    JSONObject result = composeBasicJsonResult();
    result.put("MESSAGE", msg);
    logger.info(result.toString());
}

@Override
public void error(String msg) {

    JSONObject result = composeBasicJsonResult();
    result.put("MESSAGE", msg);
    logger.error(result.toString());
}

public void error(Exception e) {

    JSONObject result = composeBasicJsonResult();
    result.put("EXCEPTION", e.getClass().getName());
    String exceptionStackTrace = Arrays.toString(e.getStackTrace());
    result.put("STACKTRACE", exceptionStackTrace);
    logger.error(result.toString());
}

private JSONObject composeBasicJsonResult() {
    //拼裝了一些執行時的資訊
    return new JSONObject();
}

}

```

可以看到,在JsonLogger中,對於Logger的各種介面,我們都用JsonObject物件進行一層封裝。在列印的時候,最終還是呼叫原生介面logger.error(string),只是這個String引數已經被裝飾過了。如果有額外的需求,則可以再寫一個函式去實現。比如error(Exception e),只傳入一個異常物件,這樣在呼叫時就非常方便。 另外,為了在新老交替的過程中儘量不改變太多程式碼和使用方式,筆者又在JsonLogger中加入了一個內部的工廠類JsonLoggerFactory(這個類轉移到DecoratorLogger中可能更好一些)。它包含一個靜態方法,用於提供對應的JsonLogger例項。最終在新的日誌體系中,使用方式如下。

```java

private static final Logger logger = JsonLoggerFactory.getLogger(Client.class);

public static void main(String[] args) {

    logger.error("錯誤資訊");
}

```

對於客戶端而言,唯一與原先不同的地方就是將LoggerFactory改為JsonLoggerFactory即可,這樣的實現,也會更快更方便地被其他開發者接受和習慣。最後看如下圖所示的類圖。

file

裝飾器模式最本質的特徵是將原有類的附加功能抽離出來,簡化原有類的邏輯。通過這樣兩個案例,我們可以總結出來,其實抽象的裝飾器是可有可無的,具體可以根據業務模型來選擇。

關注『 Tom彈架構 』回覆“設計模式”可獲取完整原始碼。

【推薦】Tom彈架構:30個設計模式真實案例(附原始碼),挑戰年薪60W不是夢

本文為“Tom彈架構”原創,轉載請註明出處。技術在於分享,我分享我快樂!如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。關注『 Tom彈架構 』可獲取更多技術乾貨!