趣談裝飾器模式,讓你一輩子不會忘
本文節選自《設計模式就該這樣學》
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());
}
```
執行結果如下圖所示。
執行結果沒有問題。但是,如果使用者需要一個加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());
}
}
```
執行結果如下圖所示。
最後來看類圖,如下圖所示。
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即可,這樣的實現,也會更快更方便地被其他開發者接受和習慣。最後看如下圖所示的類圖。
裝飾器模式最本質的特徵是將原有類的附加功能抽離出來,簡化原有類的邏輯。通過這樣兩個案例,我們可以總結出來,其實抽象的裝飾器是可有可無的,具體可以根據業務模型來選擇。
關注『 Tom彈架構 』回覆“設計模式”可獲取完整原始碼。
【推薦】Tom彈架構:30個設計模式真實案例(附原始碼),挑戰年薪60W不是夢
本文為“Tom彈架構”原創,轉載請註明出處。技術在於分享,我分享我快樂!如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。關注『 Tom彈架構 』可獲取更多技術乾貨!
- 為什麼MySQL索引結構採用B 樹?
- 為什麼Netty執行緒池預設大小為CPU核數的2倍
- 談談你對深克隆和淺克隆的理解
- 什麼是代理,為什麼要用動態代理?
- 什麼是零拷貝,Netty是如何實現的?
- 3分鐘輕鬆理解單執行緒下的HashMap工作原理
- 被面試官問爛的Spring AOP原理,你是怎麼答的?
- Spring為何需要三級快取解決迴圈依賴,而不是二級快取?
- 為什麼Spring中每個Bean都要定義作用域?
- 談談你對Spring Bean的理解
- 趣談裝飾器模式,讓你一輩子不會忘
- 掌握這些招數,你也能寫出HR眼中的高分簡歷
- MongoDB高階應用之資料轉存與恢復(5)
- 圖解MongoDB叢集部署原理(3)
- 爆肝30天,肝出來史上最透徹Spring原理和27道高頻面試題總結
- Spring核心原理之IoC容器初體驗(2)
- Spring核心原理分析之MVC九大元件(1)
- 30個類手寫Spring核心原理之動態資料來源切換(8)
- 【緊急】Log4j又發新版2.17.0,只有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂
- 30個類手寫Spring核心原理之自定義ORM(上)(6)