3年工作必備 裝飾器模式
今天我給大家分享設(shè)計(jì)模式中的裝飾器模式。用貼切的生活故事,以及真實(shí)項(xiàng)目場(chǎng)景來(lái)講設(shè)計(jì)模式,最后用一句話來(lái)總結(jié)這個(gè)設(shè)計(jì)模式。
故事
古話說(shuō)的好:人靠衣裳馬靠鞍。下面先帶大家來(lái)熟悉這句話的背景:
人靠衣裝馬靠鞍,狗配鈴鐺跑的歡出自沈自晉《望湖亭記》第十出:“雖然如此,佛靠金裝,人靠衣裝,打扮也是很要緊的。”《醒世恒言》卷一?兩縣令競(jìng)義婚孤女:”常言道:’佛是金裝,人是衣裝,世人眼孔淺的多,只有皮相,沒(méi)有骨相。’”俗語(yǔ)我們會(huì)說(shuō)成人靠衣裝馬靠鞍。
這個(gè)經(jīng)典故事,讓我想起了一個(gè)設(shè)計(jì)模式:裝飾器模式。
什么是裝飾器模式呢?請(qǐng)聽(tīng)老田慢慢道來(lái)。
裝飾器模式概述
裝飾器模式(Decorator Pattern)也叫作包裝器模式(Wrapper Pattern),指在不改變?cè)袑?duì)象的基礎(chǔ)上,動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)。就增加功能來(lái)說(shuō),裝飾器模式相比生成子類更為靈活,屬于結(jié)構(gòu)型設(shè)計(jì)模式。
英文:
Attach additional responsibilities to an object dynamicallykeeping the same interface.Decorators provide a flexible alternativeto subclassing for extending functionality.
裝飾器模式提供了比繼承更有彈性的替代方案(擴(kuò)展原有對(duì)象的功能)將功能附加到對(duì)象上。因此,裝飾器模式的核心是功能擴(kuò)展。使用裝飾器模式可以透明且動(dòng)態(tài)地?cái)U(kuò)展類的功能。
生活中的案例
一套毛坯房,沒(méi)有裝修之前,看起來(lái)非常難看,但只要稍微裝修一番,那就漂亮多了,并且能洗澡、睡覺(jué)、做飯等,但本質(zhì)還是房子。
一輛汽車,原本就是一輛代步的車,但是瑪麗加大,配置提升,然后就成了豪車,但本質(zhì)還是一輛代步的車。
一個(gè)女生,原本很平凡,長(zhǎng)相一般,但是經(jīng)過(guò)一番化妝,再穿點(diǎn)好看的衣服,然后就成了很多人心中的女神了。
總之,經(jīng)過(guò)點(diǎn)裝飾后,就是不一樣了,功能增強(qiáng)了。
裝飾器模式通用代碼實(shí)現(xiàn)
我們還是用代碼來(lái)實(shí)現(xiàn)一把,程序員都喜歡先搞個(gè)demo,然后再慢慢研究。
- //抽象組件
- public abstract class Component {
- public abstract void operation();
- }
- //具體組件
- public class ConcreteComponent extends Component {
- @Override
- public void operation() {
- System.out.println("ConcreteComponent operation");
- }
- }
- //裝飾器抽象
- public abstract class Decorator extends Component {
- protected Component component;
- public Decorator(Component component) {
- this.component = component;
- }
- @Override
- public void operation() {
- component.operation();
- }
- }
- //具體裝飾器
- public class ConcreteDecorator extends Decorator {
- public ConcreteDecorator(Component component) {
- super(component);
- }
- @Override
- public void operation() {
- System.out.println("開(kāi)始前搞點(diǎn)事");
- super.operation();
- System.out.println("結(jié)束后搞點(diǎn)事");
- }
- }
- //測(cè)試
- public class Client {
- public static void main(String[] args) {
- Component component = new ConcreteDecorator(new ConcreteComponent());
- component.operation();
- }
- }
運(yùn)行結(jié)果:
- 開(kāi)始前搞點(diǎn)事
- ConcreteComponent operation
- 結(jié)束后搞點(diǎn)事
以上便是裝飾器模式的通用代碼實(shí)現(xiàn),下面我們來(lái)分析一下。
裝飾器模式UML圖
從UML途中可以看出,其中的角色
裝飾器模式中的角色
- 抽象組件(Component):可以是一個(gè)接口或者抽象類,充當(dāng)被裝飾類的原始對(duì)象,規(guī)定了被裝飾對(duì)象的行為。
- 具體組件(ConcreteComponent):實(shí)現(xiàn)/繼承Component的一個(gè)具體對(duì)象,即被裝飾對(duì)象。
- 抽象裝飾器(Decorator):通用的裝飾ConcreteComponent的裝飾器,其內(nèi)部必然有一個(gè)屬性指向Component;其實(shí)現(xiàn)一般是一個(gè)抽象類,主要為了讓其子類按照其構(gòu)造形式傳入一個(gè)Component,這是強(qiáng)制的通用行為。如果系統(tǒng)中裝飾邏輯單一,則并不需要實(shí)現(xiàn)許多裝飾器,可以直接省略該類,而直接實(shí)現(xiàn)一個(gè)具體裝飾器即可。
- 具體裝飾器(ConcreteDecorator):Decorator的具體實(shí)現(xiàn)類,理論上,每個(gè)ConcreteDecorator都擴(kuò)展了Component對(duì)象的一種功能。
小結(jié)
裝飾器模式角色分配符合設(shè)計(jì)模式的里氏替換原則、依賴倒置原則,從而使得其具備很強(qiáng)的擴(kuò)展性,最終滿足開(kāi)閉原則。
裝飾器模式的實(shí)現(xiàn)原理是,讓裝飾器實(shí)現(xiàn)與被裝飾類(例如ConcreteComponent)相同的接口(例如Component),使得裝飾器與被擴(kuò)展類類型一致,并在構(gòu)造函數(shù)中傳入該接口對(duì)象,然后在實(shí)現(xiàn)這個(gè)接口的被包裝類對(duì)象的現(xiàn)有功能上添加新功能。由于裝飾器與被包裝類屬于同一類型(均為Component),且構(gòu)造函數(shù)的參數(shù)為其實(shí)現(xiàn)接口類(Component),因此裝飾器模式具備嵌套擴(kuò)展功能,這樣就能使用裝飾器模式一層一層地對(duì)底層被包裝類進(jìn)行功能擴(kuò)展了。
實(shí)戰(zhàn)
在實(shí)際開(kāi)發(fā)中,都會(huì)存在系統(tǒng)與系統(tǒng)之間的調(diào)用,假如說(shuō)我們現(xiàn)在有個(gè)支付功能,現(xiàn)在一切都是沒(méi)問(wèn)題的,但是 我們此時(shí)需要對(duì)發(fā)起支付前的請(qǐng)求參數(shù)和支付后的相應(yīng)參數(shù)。進(jìn)行統(tǒng)一處理,原功能不變,只是在原功能上做了一點(diǎn)擴(kuò)展(增強(qiáng))。
老功能代碼如下:
- /**
- * @author 田先生
- * @date 2021-06-02
- *
- * 歡迎關(guān)注公眾號(hào):java后端技術(shù)全棧
- */
- public interface IOrderPayService {
- String payment(Long orderId, BigDecimal amount);
- }
- public class OrderPayServiceImpl implements IOrderPayService {
- @Override
- public String payment(Long orderId, BigDecimal amount) {
- //先調(diào)用余額查詢是否足夠
- System.out.println("發(fā)起支付,訂單號(hào):" + orderId + ", 支付金額:" + amount.toString());
- //調(diào)用支付系統(tǒng)
- String result = "訂單id=" + orderId + "支付完成";
- System.out.println("支付結(jié)果:" + result);
- return result;
- }
- }
- public class OrderClient {
- public static void main(String[] args) {
- IOrderPayService orderPayService = new OrderPayServiceImpl();
- orderPayService.payment(10001L,new BigDecimal("5000"));
- }
- }
運(yùn)行輸出:
- 發(fā)起支付,訂單號(hào):10001, 支付金額:5000
- 支付結(jié)果:訂單id=10001支付完成
新需求,需要把這些請(qǐng)求參數(shù)和相應(yīng)結(jié)果進(jìn)行單獨(dú)搜集處理,此時(shí)為了不影響原有功能,于是我們可以對(duì)其進(jìn)行功能增強(qiáng)。
- /**
- * @author 田先生
- * @date 2021-06-02
- *
- * 歡迎關(guān)注公眾號(hào):java后端技術(shù)全棧
- */
- public class OrderPayDecorator implements IOrderPayService {
- private IOrderPayService orderPayService;
- public OrderPayDecorator(IOrderPayService orderPayService) {
- this.orderPayService = orderPayService;
- }
- @Override
- public String payment(Long orderId, BigDecimal amount) {
- System.out.println("把這個(gè)訂單信息(發(fā)起支付)" + "訂單id=" + orderId + "支付金額=" + amount.toString() + " 【發(fā)送給MQ】");
- String result = orderPayService.payment(orderId, amount);
- System.out.println("把訂單支付結(jié)果信息" + result + " 【發(fā)送給MQ】");
- return result;
- }
- }
- public class OrderClient {
- public static void main(String[] args) {
- IOrderPayService orderPayService =new OrderPayDecorator(new OrderPayServiceImpl());
- orderPayService.payment(10001L,new BigDecimal("5000"));
- }
- }
運(yùn)行輸出:
- 把這個(gè)訂單信息(發(fā)起支付)訂單id=10001支付金額=5000 【發(fā)送給MQ】
- 發(fā)起支付,訂單號(hào):10001, 支付金額:5000
- 支付結(jié)果:訂單id=10001支付完成
- 把訂單支付結(jié)果信息訂單id=10001支付完成 【發(fā)送給MQ】
整個(gè)過(guò)程,大家有沒(méi)有發(fā)現(xiàn),我們并沒(méi)動(dòng)原有的代碼,僅僅只是做了功能增強(qiáng)。
裝飾器模式在新項(xiàng)目中基本上不會(huì)用到,通常都是在老項(xiàng)目中使用,因?yàn)橐延械墓δ懿蛔?,只是做了一些功能增?qiáng)。
大神們是怎么用的
裝飾器設(shè)計(jì)模式在JDK源碼、Spring源碼以及Mybatis源碼中都有。
JDK源碼中
裝飾器模式比較經(jīng)典的應(yīng)用就是 JDK 中的 java.io 包下,InputStream、OuputStream、Reader、Writer 及它們的子類。
以 InputStream 為例
- FileInputStream 是 InputStream 的子類,用來(lái)讀取文件字節(jié)流
- BufferedInputStream 是 InputStream 的子類的子類,可緩存的字節(jié)流
- DataInputStream 也是 InputStream 的子類的子類,可直接讀取 Java 基本類型的字節(jié)流
UML圖
DataInputStream 中構(gòu)造器入?yún)⒈闶亲约旱母割?InputStream)。
如果希望提供一個(gè)可以讀取文件 + 可緩存的字節(jié)流,使用繼承方式,就需要派生 FileBufferedInputStream;
如果希望提供一個(gè)可以讀取文件 + 直接讀取基本類型的字節(jié)流,使用繼承方式,就需要派生 FileDataInputStream。
字節(jié)流功能的增強(qiáng)還包括支持管道 pipe、字節(jié)數(shù)組 bytearray、字節(jié)對(duì)象 object、字節(jié)流字符流的轉(zhuǎn)換 等維度,如果用繼承方式,那類的層級(jí)與種類會(huì)多到爆炸。
為了解決問(wèn)題,這邊就使用了裝飾器模式。
Spring源碼中
在Spring中,我們可以嘗試?yán)斫庖幌耇ransactionAwareCacheDecorator類,這個(gè)類主要用來(lái)處理事務(wù)緩存,代碼如下。
- public class TransactionAwareCacheDecorator implements Cache {
- private final Cache targetCache;
- //構(gòu)造方法入?yún)㈩愋蜑樽约旱母割悾ń涌陬愋停?nbsp;
- public TransactionAwareCacheDecorator(Cache targetCache) {
- Assert.notNull(targetCache, "Target Cache must not be null");
- this.targetCache = targetCache;
- }
- public Cache getTargetCache() {
- return this.targetCache;
- }
- //...
- }
TransactionAwareCacheDecorator就是對(duì)Cache的一個(gè)包裝,因此,這里也是使用了裝飾器模式。
Mybatis源碼中
MyBatis中關(guān)于Cache和CachingExecutor接口的實(shí)現(xiàn)類也使用了裝飾者設(shè)計(jì)模式。Executor是MyBatis執(zhí)行器,是MyBatis 調(diào)度的核心,負(fù)責(zé)SQL語(yǔ)句的生成和查詢緩存的維護(hù);CachingExecutor是一個(gè)Executor的裝飾器,給一個(gè)Executor增加了緩存的功能。此時(shí)可以看做是對(duì)Executor類的一個(gè)增強(qiáng),故使用裝飾器模式是合適的。
在CachingExecutor 中
- public class CachingExecutor implements Executor {
- //持有組件對(duì)象
- private Executor delegate;
- private TransactionalCacheManager tcm = new TransactionalCacheManager();
- //構(gòu)造方法,傳入組件對(duì)象
- public CachingExecutor(Executor delegate) {
- this.delegate = delegate;
- delegate.setExecutorWrapper(this);
- }
- @Override
- public int update(MappedStatement ms, Object parameterObject) throws SQLException {
- //轉(zhuǎn)發(fā)請(qǐng)求給組件對(duì)象,可以在轉(zhuǎn)發(fā)前后執(zhí)行一些附加動(dòng)作
- flushCacheIfRequired(ms);
- return delegate.update(ms, parameterObject);
- }
- //...
- }
總結(jié)
看完裝飾器模式后,你是否有感覺(jué),裝飾器模式和代理模式非常的相像,下面我們就來(lái)做個(gè)對(duì)比。
1.裝飾器模式可以理解為一種特殊的代理模式。
2.裝飾器模式強(qiáng)調(diào)自身的功能擴(kuò)展,透明的擴(kuò)展(即用戶想增強(qiáng)什么功能就增強(qiáng)什么功能),可動(dòng)態(tài)定制的擴(kuò)展。
3.代理模式強(qiáng)調(diào)的是代理過(guò)程的控制。
優(yōu)點(diǎn)
- 裝飾器是繼承的有力補(bǔ)充,比繼承靈活,在不改變?cè)袑?duì)象的情況下,動(dòng)態(tài)地給一個(gè)對(duì)象擴(kuò)展功能,即插即用。
- 通過(guò)使用不同裝飾類及這些裝飾類的排列組合,可以實(shí)現(xiàn)不同效果。
- 裝飾器模式完全遵守開(kāi)閉原則。
缺點(diǎn)
- 會(huì)出現(xiàn)更多的代碼、更多的類,增加程序的復(fù)雜性。
- 動(dòng)態(tài)裝飾在多層裝飾時(shí)會(huì)更復(fù)雜。
- 好了,今天的分享就到此結(jié)束,希望你能徹底掌握裝飾器模式,如果還有疑問(wèn),或者技術(shù)探討之類的,歡迎加我微信,一起探討。
文轉(zhuǎn)載自微信公眾號(hào)「Java后端技術(shù)全?!?,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java后端技術(shù)全棧公眾號(hào)。