本文節選自《設計模式就該這樣學》
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());
}
執行結果如下圖所示。
執行結果沒有問題。但是,如果使用者需要一個加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());
}
}
執行結果如下圖所示。
最後來看類圖,如下圖所示。
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即可,這樣的實現,也會更快更方便地被其他開發者接受和習慣。最後看如下圖所示的類圖。
裝飾器模式最本質的特徵是將原有類的附加功能抽離出來,簡化原有類的邏輯。通過這樣兩個案例,我們可以總結出來,其實抽象的裝飾器是可有可無的,具體可以根據業務模型來選擇。
本文為“Tom彈架構”原創,轉載請註明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。關注微信公眾號『 Tom彈架構 』可獲取更多技術乾貨!