本章可以稱為“給愛(ài)用繼承的人一個(gè)全新的設(shè)計(jì)眼界” ,我們即將再度探討典型的繼承濫用問(wèn)題。你將在本章學(xué)到如何使用對(duì)象組合的方式,做到在運(yùn)行時(shí)裝飾類。一旦你熟悉了裝飾的技巧,你將能夠在不修改任何底層代碼的情況下,給你的(或別人的)對(duì)象賦予新的職責(zé)。
本章目錄如下:
????????一、階段一
????????二、階段二
? ??????三、階段三
????????四、java中的裝飾者
????????五、模式問(wèn)答
????????六、設(shè)計(jì)原則總結(jié)
本章需求是設(shè)計(jì)一個(gè)星巴克點(diǎn)咖啡應(yīng)用,并通過(guò)代碼迭代、優(yōu)化過(guò)程得出前人的設(shè)計(jì)經(jīng)驗(yàn)之一裝飾者模式。首先我們先分析需求的變化之處:創(chuàng)造新調(diào)制方式的咖啡和原料價(jià)格變化。我們所設(shè)計(jì)的系統(tǒng)必須松耦合,且能夠很好的適應(yīng)變化才行。下面以代碼的迭代演化過(guò)程為線索介紹。
一、階段一
? ? 需求分析(最差設(shè)計(jì)):首先,我們要明確咖啡是一個(gè)對(duì)象,是一個(gè)由各種原料調(diào)配出來(lái)的對(duì)象,所以不能像超市購(gòu)物一樣羅列所有原料來(lái)計(jì)算價(jià)錢,所以每一種咖啡都對(duì)應(yīng)一個(gè)類。階段一不把公共操作抽象出來(lái)繼承,每種咖啡各自實(shí)現(xiàn)自己的功能。類圖如下:

????缺點(diǎn):
? ? ? ? ? ? 1、代碼復(fù)用度很低,幾乎沒(méi)有。
? ? ? ? ? ? 2、彈性非常低,一點(diǎn)也不能應(yīng)對(duì)變化。比如某一調(diào)料價(jià)格變化后,更改每個(gè)咖啡價(jià)格的工作是災(zāi)難性的。
二、階段二
????需求分析:把公共操作抽象出來(lái),通過(guò)繼承實(shí)現(xiàn)每種咖啡。類圖如下:

? ? 缺點(diǎn):只能適應(yīng)部分變化,如調(diào)料價(jià)格變化;但是有些變化不適應(yīng),如創(chuàng)造新調(diào)制方式的咖啡時(shí)不能復(fù)用代碼。最終要的是違反了“開(kāi)放—關(guān)閉”原則。且子類數(shù)量還是爆炸式增長(zhǎng)。
三、階段三
????繼承的缺點(diǎn)+組合的優(yōu)點(diǎn):利用繼承設(shè)計(jì)子類的行為,是在編譯時(shí)靜態(tài)決定的,而且所有的子類都會(huì)繼承到相同的行為。然而,如果能夠利用組合的做法擴(kuò)展對(duì)象的行為,就可以在運(yùn)行時(shí)動(dòng)態(tài)地進(jìn)行擴(kuò)展。即通過(guò)動(dòng)態(tài)地組合對(duì)象,可以通過(guò)寫新代碼來(lái)添加新功能,而無(wú)須修改現(xiàn)有代碼。這樣就沒(méi)有改變現(xiàn)有代碼,那么引進(jìn)bug或產(chǎn)生意外副作用的機(jī)會(huì)將大幅度減少。
? ? 我的理解:組合并不是拋棄繼承,它是多個(gè)繼承的組合,等于說(shuō)以前那個(gè)龐大的繼承被拆分了。
????如何評(píng)作設(shè)中的好壞?對(duì)于變化是否能做到使原有代碼免于修改!
? ? 何為變化?從不同緯度看增、刪、改、查!
? ??設(shè)計(jì)原則4(開(kāi)放—關(guān)閉原則):類應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。該原則目標(biāo)是允許類容易擴(kuò)展,在不修改現(xiàn)有代碼的情況下,就可搭配新的行為。這樣的設(shè)計(jì)具有彈性,可以接受新的功能來(lái)應(yīng)對(duì)需求的改變。
? ? 階段三需求分析:以飲料為主體,然后在運(yùn)行時(shí)以調(diào)料來(lái)“裝飾”飲料??梢园蜒b飾對(duì)象理解為一個(gè)空心管,只要符合指定接口的對(duì)象都可以插入空心管進(jìn)行包裝,包裝后的對(duì)象符合指定接口就可以被再次包裝。比如要制作一個(gè)加奶泡(Whip)加摩卡(mocha)的深焙咖啡(DarkRoast),那么包裝圖如下:

階段三的類圖如下:

????讀者可能不太理解上圖,下面我們說(shuō)明一下裝飾者模式是什么,然后讀者就明白了。
????裝飾者模式定義:裝飾者模式動(dòng)態(tài)地將責(zé)任附加到對(duì)象上。若要擴(kuò)展功能,裝飾者提供了比繼承更有彈性的替代方案。裝飾者模式的類圖如下:

? ? 下面說(shuō)明階段三的代碼:
======================類圖中的Beverage基類=================
public abstract class Beverage {
????String description = "Unknown Beverage";??
????public String getDescription() {
????????return description;
????}?
????public abstract double cost();
}
======================類圖中的調(diào)料抽象類CondimentDecorator=================
public abstract class CondimentDecorator extends Beverage {
????public abstract String getDescription();//使所有的調(diào)料裝飾者都必須重新實(shí)現(xiàn)該方法,因?yàn)樵谡{(diào)料具體實(shí)現(xiàn)類中我們要返回調(diào)料名+被裝飾者的getDescription()對(duì)象,所以此處必須抽象化,以保證子類必須實(shí)現(xiàn)該方法
}
======================類圖中的飲料實(shí)現(xiàn)類=================
public class Espresso extends Beverage {? ?//濃縮咖啡
????public Espresso() {
????????description = "Espresso";
????}??
????public double cost() {
????????return 1.99;
????}
}
public class HouseBlend extends Beverage {//綜合咖啡
????public HouseBlend() {
????????description = "House Blend Coffee";
????}?
????public double cost() {????
????????return .89;
????}
}
public class DarkRoast extends Beverage {//深焙咖啡
????public DarkRoast() {
????????description = "Dark Roast Coffee";
????}?
????public double cost() {
????????return .99;
????}
}
public class Decaf extends Beverage {//低咖啡因
????public Decaf() {
????????description = "Decaf Coffee";
????}?
????public double cost() {
????????return 1.05;
????}
}
======================類圖中的裝飾者實(shí)現(xiàn)類,即調(diào)料實(shí)現(xiàn)類=================
public class Mocha extends CondimentDecorator {//摩卡
????Beverage beverage;?
????public Mocha(Beverage beverage) {
????????this.beverage = beverage;
????}?
????public String getDescription() {
????????return beverage.getDescription() + ", Mocha";
????}?
????public double cost() {
????????return .20 + beverage.cost();
????}
}
public class Milk extends CondimentDecorator {//奶
????Beverage beverage;
????public Milk(Beverage beverage) {
????????this.beverage = beverage;
????}
????public String getDescription() {
????????return beverage.getDescription() + ", Milk";
????}
????public double cost() {
????????return .10 + beverage.cost();
????}
}
public class Soy extends CondimentDecorator {//豆?jié){
????Beverage beverage;
????public Soy(Beverage beverage) {????
????????this.beverage = beverage;
????}
????public String getDescription() {
????????return beverage.getDescription() + ", Soy";
????}
????public double cost() {
????????return .15 + beverage.cost();
????}
}
public class Whip extends CondimentDecorator {//奶泡
????Beverage beverage;?
????public Whip(Beverage beverage) {
????????this.beverage = beverage;
????}?
????public String getDescription() {
????????return beverage.getDescription() + ", Whip";
????}?
????public double cost() {
????????return .10 + beverage.cost();
????}
}
======================供應(yīng)咖啡,即點(diǎn)餐=================
public class StarbuzzCoffee {? public static void main(String args[]) {
?????//濃縮咖啡,不需要調(diào)料
????Beverage beverage = new Espresso();
????System.out.println(beverage.getDescription()? + " $" + beverage.cost());?
????//深焙咖啡,加雙份摩卡,加一份奶泡
????Beverage beverage2 = new DarkRoast();
????beverage2 = new Mocha(beverage2);
????beverage2 = new Mocha(beverage2);
????beverage2 = new Whip(beverage2);
????System.out.println(beverage2.getDescription()? + " $" + beverage2.cost());?
????//綜合咖啡,加豆?jié){、摩卡、奶泡
????Beverage beverage3 = new HouseBlend();
????beverage3 = new Soy(beverage3);
????beverage3 = new Mocha(beverage3);
????beverage3 = new Whip(beverage3);
????System.out.println(beverage3.getDescription()? + " $" + beverage3.cost());
}
}
四、java中的裝飾者
java中最常見(jiàn)的裝飾者io類圖如下:

Java IO引出裝飾者模式的“缺點(diǎn)”:
? ? (1)、裝飾者模式常常造成設(shè)計(jì)中有大量的小類,可能會(huì)造成使用此API程序員的困擾。但是,現(xiàn)在你已經(jīng)了解了裝飾者的工作原理,以后當(dāng)使用別人的大量裝飾的API時(shí),就可以很容易地辨別出他們的裝飾者類是如何組織的,以方便用包裝方式取得想要的行為。
? ? (2)、采用裝飾者在實(shí)例化組件時(shí)將增加代碼的復(fù)雜度。因?yàn)橐坏┦褂醚b飾者模式,不只需要實(shí)例化組件,還要把此組件包裝進(jìn)裝飾者中,可能會(huì)有很多個(gè)。但是,可以通過(guò)工廠模式(Factory)/生成器模式(Builder)封裝裝飾者模式的創(chuàng)建過(guò)程來(lái)解決該問(wèn)題。
下面我們編寫一個(gè)自己的java/io裝飾者,代碼如下
public class LowerCaseInputStream extends FilterInputStream {
????public LowerCaseInputStream(InputStream in) {
????????super(in);
????}?
????public int read() throws IOException {
????????int c = in.read();
????????return (c == -1 ? c : Character.toLowerCase((char)c));
????}
????public int read(byte[] b, int offset, int len) throws IOException {
????????int result = in.read(b, offset, len);
????????for (int i = offset; i < offset+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;
????????try {
????????????InputStream in =? new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt")));
????????????while((c = in.read()) >= 0) {
????????????????System.out.print((char)c);
????????????}
????????????in.close();
????????} catch (IOException e) {
????????e.printStackTrace();
????????}
????}
}
五、模式問(wèn)答
1、如何讓設(shè)計(jì)的每個(gè)部分都遵循開(kāi)放-關(guān)閉原則?
????答: 通常,你辦不到。要讓00設(shè)計(jì)同時(shí)具備開(kāi)放性和關(guān)閉性,又不修改現(xiàn)有的代碼,需要花費(fèi)許多時(shí)間和努力。一般來(lái)說(shuō),我們實(shí)在沒(méi)有閑工夫把設(shè)計(jì)的每個(gè)部分都這么設(shè)計(jì)(而且,就算做得到,也可能只是一種浪費(fèi))。遵循開(kāi)放-關(guān)閉原則,通常會(huì)引入新的抽象層次,增加代碼的復(fù)雜度。你需要把注意力集中在設(shè)計(jì)中最有可能改變的地方,然后應(yīng)用開(kāi)放-關(guān)閉原則。
2、使用裝飾者模式,你必須管理更多的對(duì)象,所以犯錯(cuò)的機(jī)會(huì)會(huì)增加。那么如何避免呢?
????裝飾者通常是用其他類似于工廠或生成器這樣的模式來(lái)包裝的。一旦我們講到這兩個(gè)模式,你就會(huì)明白具體的組件及其裝飾者的創(chuàng)建過(guò)程,它們會(huì)“封裝得很好” ,所以不會(huì)有這種問(wèn)題。
3、裝飾者知道這一連串裝飾鏈條中其他裝飾者的存在嗎?
? ? 不能。裝飾者該做的事就是增加行為到被包裝對(duì)象上。當(dāng)需要窺視裝飾者鏈中的每一個(gè)裝飾者時(shí),這就超出了他們的天賦了。
六、設(shè)計(jì)原則總結(jié)
設(shè)計(jì)原則1:找出應(yīng)用中可能需要變化之處,把它們獨(dú)立出來(lái),不要和那些不需要變化 T個(gè)T的代碼混在一起。該設(shè)計(jì)原則作用: “把會(huì)變化的部分取出并封裝起來(lái),以便以后可以輕易地改動(dòng)或擴(kuò)充此部分,而不影響不需要變化的其他部分”。
設(shè)計(jì)原則2:針對(duì)接口編程,而不是針對(duì)實(shí)現(xiàn)編程。===我的理解是這個(gè)原則的使用優(yōu)先級(jí)排在“設(shè)計(jì)原則1:變化原則”之后,即該原則是一個(gè)在大模式已確定需要實(shí)現(xiàn)具體類時(shí)針對(duì)實(shí)現(xiàn)類采用的原則,針對(duì)范圍比較小。
設(shè)計(jì)原則3:為了交互對(duì)象之間的松耦合設(shè)計(jì)而努力??偨Y(jié)為“多用組合少用繼承”!目前我見(jiàn)過(guò)兩種組合方式:實(shí)例作為另外實(shí)例的屬性,如策略模式、裝飾者模式;實(shí)例作為另外實(shí)例的集合屬性的成員,如觀察者模式!
設(shè)計(jì)原則4(開(kāi)放—關(guān)閉原則):類應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。該原則目標(biāo)是允許類容易擴(kuò)展,在不修改現(xiàn)有代碼的情況下,就可搭配新的行為。這樣的設(shè)計(jì)具有彈性,可以接受新的功能來(lái)應(yīng)對(duì)需求的改變。