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

Tom彈架構發表於2021-11-01

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

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

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

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


public class Battercake {

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

    public int getPrice(){
        return 5;
    }

}

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


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

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

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


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

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

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


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類。


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

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


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

    public int getPrice(){ return 5;  }
}

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


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類。


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類。


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;
    }
}

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


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框架搭建而成。客戶端呼叫如下。


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

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


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類。


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例項。最終在新的日誌體系中,使用方式如下。


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

    public static void main(String[] args) {

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

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

file

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

【推薦】Tom彈架構:收藏本文,相當於收藏一本“設計模式”的書

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

相關文章