前言:使用裝飾者的目的(why)
裝飾者採用運行時擴展,遠比編譯時期的繼承威力大。它還能夠在不修改底層代碼的情況下,給你的或者別人的對象賦予新的職責。
先來杯咖啡
Boss的咖啡店與時俱進準備搞一套網上點咖啡系統,它們最初的代碼設計是這樣的:
咖啡要加調料的,例如:牛奶、豆漿、摩卡等,所以設計修改爲:
類太多,而且每個類有自己的cost()方法,一旦牛奶價格上漲,所有與之有關的類就得修改,維護很麻煩。
我們可以利用實例變量與繼承減少類,調料定義爲布爾型實例變量,判斷有無添加調料,代碼如下:
public abstract class Beverage {
public String des="咖啡";
public boolean milk;
public boolean soy;
public boolean mocha;
public boolean whip;
private double cost;
private double milkCost=5.00;
private double mochaCost=6.00;
public String getDes() {
return des;
}
public double cost(){
if (isMocha()) {
cost+=mochaCost;
}else if (isMilk()){
cost+=milkCost;
}
return cost;
}
public boolean isMilk() {
return milk;
}
public void setMilk(boolean milk) {
this.milk = milk;
}
public boolean isSoy() {
return soy;
}
public void setSoy(boolean soy) {
this.soy = soy;
}
public boolean isMocha() {
return mocha;
}
public void setMocha(boolean mocha) {
this.mocha = mocha;
}
public boolean isWhip() {
return whip;
}
public void setWhip(boolean whip) {
this.whip = whip;
}
}
此時父類的cost()方法不再是抽象的,它需要通過if-else語句判斷是否加了某種調料,從而加上調料費用。
而它的子類,還需要加上本身咖啡的費用,於是最終費用=調料+子類本身費用。只舉其中一個子類飲料的列子,代碼如下:
public class DarkRoast extends Beverage {
private double darkRoastCost = 1.99;
public DarkRoast() {
des = "超優深焙";
}
@Override
public double cost() {
return darkRoastCost+super.cost();
}
}
此時我點了杯"超優深焙"的咖啡,並加了牛奶作爲調料,價格爲1.99+5=6.99,運行代碼如下:
public class DecorateTest {
public static void main(String[] args) {
DarkRoast darkRoast = new DarkRoast();
darkRoast.setMilk(true);
System.out.println(darkRoast.getDes());
System.out.println(darkRoast.cost());
}
}
運行結果如預料:
然而,當需求改變,比如調料價格改變、添加新調料、新飲料等,代碼也得隨之改變。我們需要遵循開放-關閉原則:類應該對擴展開放,對修改關閉。使用裝飾者模式就是個好例子。
什麼是裝飾者模式
裝飾者模式動態的將責任附加到對象上。若要擴展功能,裝飾者提供了比繼承更有彈性的方案。類圖如下:
我們的咖啡點餐系統代碼套用此類圖:
抽象組件:飲料基類Beverage的代碼不需要改變原始設計,如下:
public abstract class Beverage {
public String des="咖啡";
public String getDes() {
return des;
}
public abstract double cost();
}
爲了讓調料抽象裝飾者CondimentDecorator能夠取代飲料Beverage,所以CondimentDecorator擴展自Beverage,使用繼承是爲了有正確的類型,而不是繼承它的行爲。getDes()定義爲抽象方法,由子類重新實現,抽象類代碼如下:
public abstract class CondimentDecorator extends Beverage{
public abstract String getDes();
}
具體組件飲料的代碼,使用構造器初始化飲料描述,des實例變量繼承自基類Beverage,並實現cost()方法。代碼如下:
public class DarkRoast extends Beverage {
public DarkRoast() {
des = "超優深焙";
}
@Override
public double cost() {
return 1.99;
}
}
我們點了摩卡Mocha(具體裝飾者)口味的,具體裝飾者代碼如下:
public class Mocha extends CondimentDecorator {
//定義一個實例變量記錄被裝飾者
Beverage beverage;
//使用構造器來記錄被裝飾者
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDes() {
return beverage.getDes() + " Mocha";
}
/**
* 首先把調用委託給被裝飾着計算費用,再加上Mocha的費用
* @return
*/
@Override
public double cost() {
return beverage.cost() + 0.2;
}
}
我們還加了豆漿(具體裝飾者),代碼如下:
public class Soy extends CondimentDecorator {
//定義一個實例變量記錄被裝飾者
Beverage beverage;
//使用構造器來記錄被裝飾者
public Soy(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDes() {
return beverage.getDes() + " Soy";
}
@Override
public double cost() {
return beverage.cost() + 0.15;
}
}
我們點餐完畢,運行程序代碼:
public class StartTest {
public static void main(String[] args) {
Beverage darkRoast = new DarkRoast();
Soy soy = new Soy(darkRoast);
Mocha mocha = new Mocha(soy);
System.out.println(mocha.getDes());
System.out.println(mocha.cost());
}
}
運行結果如下:
裝飾者模式典型案例:java I/O
和上面咖啡的設計相比,java.io並沒有多大差異。
現在我們應用下裝飾者把輸入流內所有大寫字符轉換成小寫。具體裝飾者代碼如下:
public class LowerCaseInputStream extends FilterInputStream {
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected LowerCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int c = super.read();
return (c==-1?c:Character.toLowerCase((char)c));
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
for(int i = off;i<len+result;i++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return result;
}
}
運行代碼如下:
public class InputTest {
public static void main(String[] args) throws IOException {
int c;
InputStream lowerCaseInputStream = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("D:/test.txt")));
System.out.println(lowerCaseInputStream.read());
while ((c=lowerCaseInputStream.read())>=0) {
System.out.print((char)c);
}
lowerCaseInputStream.close();
}
}
D盤的test.txt內容爲:OWeN_JaMES,運行代碼,結果如下: