本文節選自《設計模式就該這樣學》
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彈架構”原創,轉載請注明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力。關注微信公眾號『 Tom彈架構 』可獲取更多技術干貨!