
本文原創(chuàng)地址:jsbintask的博客(食用效果最佳),轉(zhuǎn)載請注明出處!
同系列文章:
從未這么明白的設(shè)計模式(二):觀察者模式
從未這么明白的設(shè)計模式(一):單例模式
前言
裝飾器模式是為了運(yùn)行時動態(tài)的擴(kuò)展一個類的功能。它謹(jǐn)遵開閉原則,它實(shí)現(xiàn)的關(guān)鍵在于繼承和組合的結(jié)合使用,解耦對象之間的關(guān)系。
各種設(shè)計模式學(xué)習(xí)地址:https://github.com/jsbintask22/design-pattern-learning
栗子
首先我們列舉一個案例,并且按照面向?qū)ο蟮乃枷雭韺?yīng)實(shí)體之間的關(guān)系。
有一個咖啡店,銷售各種各樣的咖啡,拿鐵,卡布奇洛,藍(lán)山咖啡等,在沖泡前,會詢問顧客是否要加糖,加奶,加薄荷等。這樣不同的咖啡配上不同的調(diào)料就會賣出不同的價格。

V1
針對上面的栗子,我們很容易就抽象出對應(yīng)的實(shí)現(xiàn),如上圖。接著,我們就要編寫對應(yīng)的類來實(shí)現(xiàn)對應(yīng)的功能。在這個例子中,主題當(dāng)然就是咖啡,并且它有一個屬性是名字,一個行為 價格,出于“面向?qū)ο蟆钡乃枷?,我們自然會設(shè)計出抽象類Coffee:

public abstract class Coffee {
/**
* 獲取咖啡得名字
*/
public abstract String getName();
/**
* 獲取咖啡的價格
*/
public abstract double getPrice();
}
接著,按照繼承的思想,我們要開始設(shè)計出具體的實(shí)現(xiàn)類,因為拿鐵,卡布奇洛,藍(lán)山搭配上不同的調(diào)料(上面三種)會有不同的價格,名字,所以我們至少得設(shè)計出 3 X 3 = 9 個類來分別對應(yīng)它們的名字和價格:

嗯!我想不用說這樣設(shè)計得缺陷也很明顯了! 由于不同的咖啡和不同的調(diào)料得各種任意組合,使得出現(xiàn)了
類爆炸的現(xiàn)象。既然有這么明顯的缺陷,那我們當(dāng)然得改! 我們可以考慮把各種調(diào)料當(dāng)作屬性加入到Coffee這個抽象類中,接著在實(shí)現(xiàn)類中計算價格和名字時,分別判斷是否加入了各種調(diào)料包,得到不同的名字和價格!
按照上面的思想,我們的Coffee類現(xiàn)在變成了這樣:
public abstract class Coffee {
// 是否加了牛奶
protected boolean addedMilk;
// 是否加了糖
protected boolean addedSugar;
// 是否加了薄荷
protected boolean addedMint;
/**
* 獲取咖啡得名字
*/
public abstract String getName();
/**
* 獲取咖啡的價格
*/
public abstract double getPrice();
}
接著,我們實(shí)現(xiàn)一種咖啡,藍(lán)山咖啡:
public class BuleCoffee extends Coffee {
@Override
public String getName() {
StringBuilder name = new StringBuilder();
name.append("藍(lán)山");
if (addedMilk) {
name.append("牛奶");
}
if (addedMilk) {
name.append("薄荷");
}
if (addedSugar) {
name.append("加糖");
}
return name.toString();
}
@Override
public double getPrice() {
double price = 10;
if (addedMilk) {
price += 1.1;
}
if (addedMilk) {
price += 3.2;
}
if (addedSugar) {
price += 2.7;
}
return price;
}
}
嗯!現(xiàn)在似乎比上面愉快多了。其實(shí)不然!我們仔細(xì)分析這種設(shè)計,會發(fā)現(xiàn)它似乎不太符合”封裝的思想“,比如說針對拿鐵,對于加薄荷而言,對他總是多余的! 而對于藍(lán)山而言,牛奶又顯得很多余! 所以這種設(shè)計也并不合理。 另外,我們假設(shè)coffee,拿鐵等實(shí)體類來自第三方類庫,我們并不能改動這些類的實(shí)現(xiàn), 又要怎么得到名字和價格呢?
這個時候,我們就得使用裝飾器模式來動態(tài)的擴(kuò)展類行為! 所以我們設(shè)計出V3版本。
V3
開閉原則
首先,我們需要了解一個面向?qū)ο蟮囊粋€基本設(shè)計原則:開閉原則,它指的是類應(yīng)該對修改關(guān)閉,對擴(kuò)展開放。
怎么理解呢? 就比如我們上方說的:假如cofee和它的一眾實(shí)現(xiàn)拿鐵,卡布奇洛,藍(lán)山來自第三方類庫,并且這個類庫已經(jīng)很”適合“,”實(shí)用“了。 而我們?yōu)榱说玫郊尤氩煌{(diào)料的咖啡的名字和價格,我們就得修改這些實(shí)現(xiàn),而這樣的修改,總是免不了穩(wěn)定性的改變。對原本的系統(tǒng)來說也是一種風(fēng)險! 所以我們應(yīng)該 對修改關(guān)閉,對擴(kuò)展開放;
繼承和組合
遵循開閉原則,那我們就得對外擴(kuò)展,那怎么對外擴(kuò)展呢? 這也是裝飾器模式實(shí)現(xiàn)的關(guān)鍵,利用繼承和組合的結(jié)合; 現(xiàn)在我們可以考慮設(shè)計出一個裝飾類,它也繼承自coffee,并且它內(nèi)部有一個coffee的實(shí)例對象:

現(xiàn)在,我們多了一個
咖啡裝飾器: CoffeeDecorator:
public abstract class CoffeeDecorator implements Coffee {
private Coffee delegate;
public CoffeeDecorator(Coffee coffee) {
this.delegate = coffee;
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public double getPrice() {
return delegate.getPrice();
}
}
接著,我們將牛奶,薄荷作為抽象出一個類,繼承自CoffeeDecorator,所以,現(xiàn)在類圖就成了這樣:

我們實(shí)現(xiàn)一個
MilkCoffeeDecorator:
public class MilkCoffeeDecorator extends CoffeeDecorator {
public MilkCoffeeDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getName() {
return "牛奶, " + super.getName();
}
@Override
public double getPrice() {
return 1.1 + super.getPrice();
}
}
按同樣的方法可以實(shí)現(xiàn)出MintCoffeeDecorator,SugarCoffeeDecorator。接著我們寫一個測試類:
public class App {
public static void main(String[] args) {
// 得到一杯原始的藍(lán)山咖啡
Coffee blueCoffee = new BlueCoffee();
System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
// 加入牛奶
blueCoffee = new MilkCoffeeDecorator(blueCoffee);
System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
// 再加入薄荷
blueCoffee = new MintCoffeeDecorator(blueCoffee);
System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
// 再加入糖
blueCoffee = new SugarCoffeeDecorator(blueCoffee);
System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
}
}

從結(jié)果我們可以看出,隨著不斷加入各種調(diào)料,價格,名字都在改變! 這說明我們加入不同的調(diào)料,動態(tài)的改變了咖啡的名字和價格!
思考
從上面的最后的裝飾器模式的實(shí)現(xiàn)來看,我們可以得出以下結(jié)論:
- 通過裝飾器模式可以動態(tài)的將責(zé)任附加到原有的對象上,而不改變原有的code。
- 遵循
開閉原則 - 裝飾者和被裝飾者有相同的父類(如栗子中的Coffee)
- 可以用多個裝飾器裝飾同一個對象。(見運(yùn)行類)
- 裝飾者可以在被裝飾者的行為之前或之后動態(tài)的加上自己的行為。(參考裝飾實(shí)現(xiàn))
- 組合比繼承更加的靈活(上面的coffee代理)
擴(kuò)展
到現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了一個自己的裝飾器,我們來看看jdk中用到的裝飾器實(shí)現(xiàn).
IO
我們可以查看FilterInputStream:

它的主要是實(shí)現(xiàn)者為
BufferedInputStream:
所以我們經(jīng)??梢允褂肂ufferedInputStream裝飾一個InputStream,比如FileInputStream:
new BufferedInputStream(FileInputStream);這就是裝飾器模式的典型應(yīng)用。
tomcat
在tomcat的HttpServletRequest的內(nèi)部實(shí)現(xiàn)代碼中,RequestFacde繼承自HttpServlet,而它內(nèi)部的實(shí)現(xiàn)也是通過代理Request對象,而Request對象繼承自HttpServlet,Request內(nèi)部代理了org.apache.coyote.Request來實(shí)現(xiàn)的。
總結(jié)
裝飾器模式充分展示了組合的靈活。利用它來實(shí)現(xiàn)擴(kuò)展。它同時也是開閉原則的體現(xiàn)。 如果相對某個類實(shí)現(xiàn)運(yùn)行時功能動態(tài)的擴(kuò)展。 這個時候你就可以考慮使用裝飾者模式!
關(guān)注我,這里只有干貨!