1. 程式人生 > >設計模式(三)—— 裝飾者模式

設計模式(三)—— 裝飾者模式

由於之前看的容易忘記,因此特記錄下來,以便學習總結與更好理解,該系列博文也是第一次記錄,所有有好多不完善之處請見諒與留言指出,如果有幸大家看到該博文,希望報以參考目的看瀏覽,如有錯誤之處,謝謝大家指出與留言。

一、咖啡館訂單系統專案

咖啡館訂單系統專案:

咖啡館訂單專案:

1)、咖啡種類:Espresso、ShortBlack(濃縮)、LongBlack、Decaf (無糖)(這些就是相當咖啡的基礎,單品咖啡,基礎元素,所有的咖啡都是在此基礎上混合起來了的)

2)、調料:Milk、Soy、Chocolate  (往基礎品種中加入牛奶,巧克力等等,就組成了類似巧克力味咖啡,摩卡,卡夫奇諾等等。我們一般在咖啡廳一般都是直接點卡夫奇諾,其價格就是有單品咖啡和各種調料相加在一起的價格。所以下面的案例系統就是咖啡完整系統)

3)、咖啡館訂單專案設計原則(或者是需求):擴充套件性好、改動方便、維護方便

(1)、先來看一個差的方案(簡單的通過oo模式實現)

就是把所有的咖啡或者調料設計成一個超類;如圖,就是把所有飲料設計成一個抽象的超類Drink,有一個變數描述description,這個描述其實就是咖啡的名字,調料的名字等等。還有個方法getDescription()用來呼叫這個類,來指定是具體是什麼型別的咖啡。最後是一個抽象的方法cost(),再具體的單品咖啡咖啡中去實現,比如表示單品咖啡的價格,或者是單品咖啡加調料的價格。而這上面那些單品咖啡去實現這個抽象類,通過實現description來呈現具體的事哪一種單品種類的咖啡,就能得到具體的型別。像Decaf等類在new的時候就知道了。cost裡面就直接return返回多少錢。程式碼實現就是使用者new一個單品咖啡Decaf。然後在呼叫getDescription()方法就知道點了Decaf這個種類咖啡,在呼叫cost就知道是多少錢了。

那麼要是加各種調料該怎麼辦呢,比較某個單品加上調料就是摩卡?其實是一樣,如下圖:

比如,在擴充套件drink類是,在呼叫description的時候就指明我加入那些調料,比如Espresson幾個實現指明加糖,豆漿。cost()裡面也是直接算好指明總共多少錢,當用戶呼叫Espresson加Milk加Soy時候呼叫cost就直接返回了多少錢。上面實現了三種咖啡。

但問題就是,調料擁有很多,哪些組合可以組,哪些組合不可以組,就要自己維護了。以後要要新增一個單品型別,就要新新增一個類實現Drink類。如果還要新增調料就要跟單品不同的組合,就會產生很多的類。這樣類就會爆炸,引入如果一個單品價格變了,相關的組合就要跟著改動。所以下面來使用一個好一點的方式設計。

(2)、一個好一點的設計方案

思路就是在超類中把所有的種類調料都內建到超類中。把上面說的幾個調料,內建進去,但他們都是布林型別的,在下面定義的方法用於判斷,是否擴充套件類中是否有他們,或者是要加什麼調料,同時是否要加錢。這個方案就會比上一個方案好一點,不會類爆炸,但是他也有一些問題。

(3)、問題

1)增刪調料種類,如果要引入一個調料,就要改動這個超類。也就是新功能的引入,會影響原有功能,原有程式碼。這樣就會在改動原有功能時,所有的引入都會產生bug,都有可能引入bug。還有不要豆漿也會修改超類。

2)新增多份問題,例如如果引入兩個牛奶,布林型就不能判斷。

因此這個設計會比上面好一點,但是還是有很多問題,所以下采用裝飾者模式類設計。因而這種模式也是最優的

二、裝飾者模式原理

1、裝飾者模式就像打包一個快遞 ;比如我賣了一個陶瓷,一本書,其核心就是我們賣的東西。這些東西我們不可能直接去賣出去,因此我們會在外面包裝好多保護東西,如泡沫塑料等等。

1)主體:陶瓷、衣服

2)包裝:報紙填充、塑料泡沫、紙板、木板

因此裝飾者模式也可以這麼理解。同樣他也跟上面案例一樣,分為如下幾個部分:

(1)、Component:主體(裝飾的主體是什麼)他就像上面飲料例子的超類Drink類(抽象的超類)。

(2)、ConcreteComponent:具體不同型別主題(比如具體要賣的陶瓷,衣服等等),就像上面的單品咖啡,用來被包裝的物品。

(3)、Decorator:裝飾者(比如泡沫塑料,紙板等等),就像是各種調料。

                                                            

上面就是類結構實現,如果主題比較複雜,還可以在主題和實體之間新增一個緩衝類,就是把主題公共的方法拿出來放在主題與實體之間。再新增一個抽象的類。然後在去實現具體的實體類。這個就根據具體情況來設計。這次的咖啡設計就比較簡單,就不需要在引入中間層。一般裝飾者就是在主體元件擴充套件到具體的實現類時,會引入一箇中間層,把裝飾者的公佈部分引入進來,在引入具體的實現時,只需要實現自己特定的部分就行了。公共的就放在上面,中間層中。

4、裝飾者模式定義: 動態的將新功能附加到物件上。在物件功能擴充套件方面,它比繼承更有彈性。簡單地說:由上面的例子來說,這裡的物件其實指的就是單品咖啡,附加的功能就是各種調料,是單品咖啡喝起來味道更好。也就是說裝飾者模式可以動態的將調料新增到單品咖啡上混合出一個好的咖啡。而且是通過附加的方式新增上去,不是繼承的方式新增上去的。

因此我們來重新設計這個方案:

三、新的專案設計方案

1、用裝飾者模式設計重新設計的方案

這裡主體跟實體大致不變,主要在抽象主體與調料中間添加了一箇中間層。中間層的作用就是在呼叫cost的時候,他是要進行費用的疊加的,還有一個重要的就是中間層引入了一個Drink物件,這個物件其實包含著被裝飾的物件,這個被裝飾的物件,在呼叫coat的時候我們會獲取他的費用,然後通過遞迴的放方式去獲取這一級所有費用、同時具體裝飾的名字可以通過getDescription()獲取。就是外面包裝那麼多東西,也可以獲取出來。

2、裝飾者模式下的舉例訂單:2份巧克力+一份牛奶的LongBlack

                                       

怎麼做呢?首先new一個單品物件LongBlack,然後包裝到Milk,再然後包裝到Chocolate,在包裝在Chocolate裡面,具體的費用計算是通過,代用最外層cost方法去獲取,他會自己去獲取自己裡面包裝的費用,然後一級級遞迴,去呼叫所有費用,從而獲取所有費用;通過這種方式結構,你會發現當調料很多的情況下,任意他隨意組合,我們都可以通過這種遞迴的方式獲取所有的費用。不像第一種方式一樣,每新一個組合就要擴充套件一個類,導致類爆炸。而且你引入一個調料的時候,只需要在上面說的具體的調料上去擴充套件一個出來就行了,引入新調料的時候,不會對原有的功能造成任何影響。它引入自己引入自己,不會對原有程式碼造成影響。

四、裝飾者模式示例演示

1、裝飾者模式的設計方案

抽象超類

package com.java.jikexueyuan.coffeebar;
/*
 * 
 * 首先定一個Drink超類,抽象類 ;在抽象類的基礎上擴展出兩個分支,一個是咖啡的單品
 * ,這裡咖啡可以設定箇中間層,這裡實現了中間層,Coffee中間層,這個中間層把單品的公有功能放進去了;一個是調料,
 * 裝飾者分支Decorator,這個裝飾者本身就是個中間層,在這Decorator裡面實現共有功能,具體的調料去繼承這個中間層去實現即可
 * 這樣,程式碼介面就比較清晰了
 */
public abstract class Drink {
	public String description=""; //這個描述具體擴展出是什麼單品或調料
	private float price=0f;; //這裡是具體的單品或調料的價格
	
	
	public void setDescription(String description)
	{
		this.description=description;
	}
	
	public String getDescription()
	{
		return description+"-"+this.getPrice();
	}
	public float getPrice()
	{
		return price;
	}
	public void setPrice(float price)
	{
		this.price=price;
	}
	public abstract float cost();  //這個是用抽象是因為,在單品種直接返回價格即可,
	//但在調料中,調料是不能單獨存在的,是跟著實體,具體實體一起的,價格不僅是調料還有單品的價格,調料cost方法是是要遞迴去獲取所有調料的價格
	
}
package com.java.jikexueyuan.coffeebar.coffee;

import com.java.jikexueyuan.coffeebar.Drink;
//這是個具體主題分支上面的中間層
public  class Coffee extends Drink {

	@Override
	public float cost() { //在實現是直接返回價格即可,因為單品就單品一個,實現簡單
		// TODO Auto-generated method stub
		return super.getPrice();
	}

	
}
package com.java.jikexueyuan.coffeebar.coffee;

public class Decaf extends Coffee {
	public Decaf()
	{
		super.setDescription("Decaf");
		super.setPrice(3.0f);
	}
}
package com.java.jikexueyuan.coffeebar.decorator;

import com.java.jikexueyuan.coffeebar.Drink;
//裝飾者分支 中間層
public  class Decorator extends Drink {
	private Drink Obj;//注意這裡面有個超類的物件,因為這是個裝飾者,
	//所以他包裝的是一個單品,或者是一個被包裝過的單品.所以他用個超類的型別

	public Decorator(Drink Obj){//所以實現這個裝飾者時必須帶入這個Drink物件放進去
		this.Obj=Obj;
	};
	
	//這裡的價格計算就有所跟單品不一樣了。首先他要計算自己的價格,比如,巧克力,牛奶多少錢,還有要計算的就是前面
	//帶入的主題的價格,如果是已包裝過的,就變成了遞迴了,就迭代的計算所有價格,以級最終的單品價格
	@Override
	public float cost() {
		// TODO Auto-generated method stub
		
		return super.getPrice()+Obj.cost();
	}

	@Override
	public String getDescription()
	{
		return super.description+"-"+super.getPrice()+"&&"+Obj.getDescription();
	}
	
	}

調料裝飾者

package com.java.jikexueyuan.coffeebar.decorator;

import com.java.jikexueyuan.coffeebar.Drink;

public class Chocolate extends Decorator {

	public Chocolate(Drink Obj) {		
		super(Obj);
		// TODO Auto-generated constructor stub
		super.setDescription("Chocolate");
		super.setPrice(3.0f);
	}

}

五、Java裡內建裝飾者介紹

1、Java的IO結構的裝飾者

這個其實跟上面咖啡館設計其實是一模一樣的。

FileInputStream等三個是一些主體,其中FilterInputStream是個中間層,繼承一些公共的東西,而下面就是一些裝飾者。

2、編寫自己的Java I/O裝飾者  -利用擴充套件java這個FilterInputStreamsh中間層實現自己的裝飾者物件

該例子功能是把檔案輸入流或者其他主體輸入流裡面輸入過來的字串轉換成大寫的這麼一個自定義的把小寫全部實現轉化為大寫輸入流

package com.java.jikexueyuan.myiodecorator;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
//首先實現中間層這個物件FilterInputStream
public class UpperCaseInputStream extends FilterInputStream{

	//然後在建構函式中要把這個超類帶進來,然後super進去
	protected UpperCaseInputStream(InputStream in) {
		super(in);
		// TODO Auto-generated constructor stub
	}

	//下面兩個就是類似cost方法,需要實現的,
	public int read() throws IOException//單字元的讀
	{
		int c=super.read();//這個super.read()就是呼叫上面super(in);的主題物件
		return c==-1?c:Character.toUpperCase((char)(c));
	}
	public int read(byte[] b,int offset,int len) throws IOException//多字元的讀
	{
		int result=super.read(b,offset,len);
		for(int i=0;i<result;i++)
		{
			b[i]=(byte)Character.toUpperCase((char)(b[i]));
		}
		
		return result;
	}
}
package com.java.jikexueyuan.myiodecorator;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class InputTest {
	public static void main(String[] args) {
		int c;
		try {
			InputStream in = new UpperCaseInputStream(new BufferedInputStream(
					new FileInputStream("F:\\test.txt")));
			while((c=in.read())>=0)
			{
				System.out.print((char)c);
				
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

六、裝飾者模式原則

1、開放-關閉原則的設計

意思就是:裝飾者新增新功能,比如新增一個調料,開放就是新增一個新功能,關閉就是新增一個新功能時,對原有的程式碼不輕易改變。也就是說在設計一個專案體系結構的時候,對新增新功能,新程式碼是開放的,對已經設計好的,或者測試好的程式碼是不允許修改的。

想要完整程式碼可以留下郵箱,我會及時傳送。