1. 程式人生 > >JAVA設計模式詳解(三)----------裝飾者模式

JAVA設計模式詳解(三)----------裝飾者模式

今天LZ帶給大家的是裝飾者模式,提起這個設計模式,LZ心裡一陣激動,這是LZ學習JAVA以來接觸的第一個設計模式,也許也是各位接觸的第一個設計模式。記得當初老師在講IO的時候就提到過它:“是你還有你,一切拜託你。”沒錯,這就是裝飾者模式最簡潔的定義了。下面LZ引出標準的定義(出自百度百科):裝飾模式是在不必改變原類檔案和使用繼承的情況下,動態的擴充套件一個物件的功能。它是通過建立一個包裝物件,也就是裝飾來包裹真實的物件。

ex:工廠製作上衣,白上衣是30,藍上衣40,紅上衣50,另外還有往衣服上繡的圖案,花圖案需10元,草圖案需5元。現在工廠需要計算出成本價,要求設計出這個實現程式碼。

我們初看這題有什麼思路,難道要設計出5個類嗎?這顯然不行,如果工廠老闆突然讓加一個新的圖案,豈不是“類爆炸”!這個時候,我們可能想到了用另一種實現方式,把CLothes做為基類,然後設定whiteClothes,

blueClothes ,redClothes 為三個布林型別變數,在基類裡設定cost方法,然後圖案設定為子類繼承它,各自重寫cost方法。這樣就把原本的五個類縮小成了三個。
//白上衣是30,藍上衣40,紅上衣50,另外還有往衣服上繡的圖案,花圖案需10元,草圖案需5元
public  class Clothes {
    private int cost;
    private boolean whiteClothes = false;
    private boolean blueClothes = false;
    private boolean redClothes = false;
    public  int cost(){
        if(whiteClothes){
            cost += 30;
        }
        
        if(blueClothes){
            cost += 40;
        }
        
        if(redClothes){
            cost += 50;
        }
        return cost;
    }
    
    public int getCost() {
        return cost;
    }
    
    public void setCost(int cost) {
        this.cost = cost;
    }
    
    public boolean isWhiteClothes() {
        return whiteClothes;
    }
    
    public void setWhiteClothes(boolean whiteClothes) {
        this.whiteClothes = whiteClothes;
    }
    
    public boolean isBlueClothes() {
        return blueClothes;
    }
    
    public void setBlueClothes(boolean blueClothes) {
        this.blueClothes = blueClothes;
    }
    
    public boolean isRedClothes() {
        return redClothes;
    }
    
    public void setRedClothes(boolean redClothes) {
        this.redClothes = redClothes;
    }
}

class FlowerPattern extends Clothes{
    public  int cost(){
        return 10+super.cost();
    }
}

class GrassPattern extends Clothes{
    public  int cost(){
        return 5+super.cost();
    }
}

測試方法

public class TestDemo1 {
    public static void main(String[] args) {
        FlowerPattern flowerPattern = new FlowerPattern();
        flowerPattern.setBlueClothes(true);
        System.out.println("藍上衣花圖案一共花費:"+flowerPattern.cost());
        GrassPattern grassPattern = new GrassPattern();
        grassPattern.setRedClothes(true);
        System.out.println("紅上衣草圖案一共花費:"+grassPattern.cost());
    }
}

這種寫法,相信看過LZ前兩篇文章的人一眼就看出了問題,我們雖然寫出了實現,但是由於類之間過於耦合,導致不利於維護,當我們新增一個新的顏色的上衣時,需要更改Clothes程式碼,這就違反了我們的設計原則:類應該對擴充套件開發,對修改關閉,也就是我們常說的開閉原則。在這裡,LZ帶著大家一起認識裝飾者模式,我們以上衣為主體,以圖案來“裝飾”上衣。比方說,如果商家要藍上衣花草圖案,那麼,我們要做的是:

①拿一個藍色上衣物件

②以花圖案物件裝飾它

③以草圖案物件裝飾它

④呼叫cost()方法,並依賴委託將調料的價錢加上去

下面我們畫個圖更加詳細瞭解如何以裝飾者構造衣服訂單

①以BlueClothes物件開始,別忘了它繼承自clothes,且有一個用來計算上衣價錢的cost()方法

                     

②建立一個花圖案物件,並用它將BlueClothes物件包(wrap)起來。FlowerPattern物件是一個裝飾者,它的型別"反映"了它所裝飾的物件(本例中,就是Clothes)。所謂的"反映",指的是兩者型別一致。所以FlowerPattern也有一個cost()方法。通過多型,也可以把FlowerPattern所包裹的任何clothes當成是clothes(因為FlowerPattern是clothes的子類)

③接下來還要建立草圖案GrassPattern裝飾者,並用它將FlowerPattern物件包起來。別忘了,BlueClothes繼承自Clothes,且有一個cost()方法,用來計算衣服價錢

④這樣,計算總價錢通過呼叫最外圈裝飾者(GrassPattern)的cost()就可以辦得到。GrassPatternd cost()方法會先委託它裝飾的物件(也就是FlowerPattern)計算出價錢,然後再加上上衣的價錢。

現在,LZ來總結一下我們目前所指的的一切:

=。=裝飾者和被裝飾者物件有相同的基類。

=。=我們可以用一個或多個裝飾者包裝一個物件。

=。=既然裝飾者和被裝飾物件有相同的基類,所以在任何需要原始物件(被包裝的)的場合,可以用裝飾過的物件代替它。

=。=裝飾者可以在所委託被裝飾者的行為之前與/或之後,加上自己的行為,以達到特定的目的,

=。=物件可以在任何時候被裝飾,所以可以在執行時動態地、不限量地用你喜歡的裝飾者來裝飾物件

接下來我們先來看看裝飾者模式的說明:裝飾者模式動態地將責任附加到物件上。若要擴充套件功能,裝飾者提供了比繼承更有彈性的替代方案。

下面我們來看下類圖:

到這裡,大家可能會有些混淆:這裡使用的是繼承不是組合?。其實,這麼做的重點在於,裝飾者和被裝飾者必須是一樣的型別,也就是有共同的基類,這是相當關鍵的地方。在這裡我們利用繼承達到“型別匹配”,而不是利用繼承獲得“行為”。那麼這個行為又來自哪裡?當我們將裝飾者與元件組合時,就是再加入新的行為,所得到的新行為,並不是繼承自超累,而是由組合物件得來的。

分支

這裡,為了防止各位看不懂圖的結構,我們先切出一個分支來描述一下這張圖,當然如果各位已經看懂,也可以直接跳過這段分支,直接看下面LZ對一開始那個問題的分析。

 1,Component介面可以是介面也可以是抽象類,甚至是一個普通的父類(這個強烈不推薦,普通的類作為繼承體系的超級父類不易於維護)。

 2,裝飾器的抽象父類Decorator並不是必須的。

                 那麼我們將上述標準的裝飾器模式,用我們熟悉的JAVA程式碼給詮釋一下。首先是待裝飾的介面Component。

public interface Component {

    void method();
    
}

 接下來便是我們的一個具體的介面實現類,也就是俗稱的原始物件,或者說待裝飾物件。

public class ConcreteComponent implements Component{

    public void method() {
        System.out.println("原來的方法");
    }

}

下面便是我們的抽象裝飾器父類,它主要是為裝飾器定義了我們需要裝飾的目標是什麼,並對Component進行了基礎的裝飾。

public abstract class Decorator implements Component{

    protected Component component;

    public Decorator(Component component) {
        super();
        this.component = component;
    }

    public void method() {
        component.method();
    }
    
}

再來便是我們具體的裝飾器A和裝飾器B。

public class ConcreteDecoratorA extends Decorator{

    public ConcreteDecoratorA(Component component) {
        super(component);
    }
    
    public void methodA(){
        System.out.println("被裝飾器A擴充套件的功能");
    }

    public void method(){
        System.out.println("針對該方法加一層A包裝");
        super.method();
        System.out.println("A包裝結束");
    }
}
public class ConcreteDecoratorB extends Decorator{

    public ConcreteDecoratorB(Component component) {
        super(component);
    }
    
    public void methodB(){
        System.out.println("被裝飾器B擴充套件的功能");
    }

    public void method(){
        System.out.println("針對該方法加一層B包裝");
        super.method();
        System.out.println("B包裝結束");
    }
}

下面給出我們的測試類。我們針對多種情況進行包裝。

public class Main {

    public static void main(String[] args) {
        Component component =new ConcreteComponent();//原來的物件
        System.out.println("------------------------------");
        component.method();//原來的方法
        ConcreteDecoratorA concreteDecoratorA = new ConcreteDecoratorA(component);//裝飾成A
        System.out.println("------------------------------");
        concreteDecoratorA.method();//原來的方法
        concreteDecoratorA.methodA();//裝飾成A以後新增的方法
        ConcreteDecoratorB concreteDecoratorB = new ConcreteDecoratorB(component);//裝飾成B
        System.out.println("------------------------------");
        concreteDecoratorB.method();//原來的方法
        concreteDecoratorB.methodB();//裝飾成B以後新增的方法
        concreteDecoratorB = new ConcreteDecoratorB(concreteDecoratorA);//裝飾成A以後再裝飾成B
        System.out.println("------------------------------");
        concreteDecoratorB.method();//原來的方法
        concreteDecoratorB.methodB();//裝飾成B以後新增的方法
    }
}

從這個例子中,各位應該對裝飾者模式的結構有了一定的瞭解,那麼接下來我們解決之前的那個問題。

分支結束

接下來我們開始看最開始的問題程式碼:

先從clothes下手,這裡我們加一個description 用來描述的更詳細一下:

public abstract class Clothes {
    String description = "unknown Clothes";
    
    public String getDescription(){
        return description;
    }
    
    public abstract double cost();
}

接下來是裝飾者類,我們成為圖案Pattern類,也繼承自Clothes

class WhiteClothes extends Clothes{
     public WhiteClothes(){
         description = "WhiteClothes";
     }
    @Override
    public double cost() {
        
        return 30;
    }
     
 }

class BlueClothes extends Clothes{
     public BlueClothes(){
         description = "BlueClothes";
     }
    @Override
    public double cost() {
        
        return 40;
    }
     
}

class RedClothes extends Clothes{
     public RedClothes(){
         description = "RedClothes";
     }
    @Override
    public double cost() {
        
        return 50;
    }
     
}

接下來我們開始寫圖案程式碼,也就是具體裝飾者類,為了能夠跟上思路,LZ標了註釋

class FlowerPattern extends Pattern{//花圖案是一個裝飾者,讓它擴充套件自Pattern,而Pattern又擴充套件自Clothes
    //我們為了能讓FlowerPattern能夠引用clothes,用一個例項變數記錄衣服,也就是被裝飾者,然後在構造器中將其記錄在例項變數裡
    private Clothes clothes;
    public FlowerPattern(Clothes clothes){
        this.clothes = clothes;
    }
    @Override
    public String getDescription() {
        
        return clothes.getDescription() + ",FlowerPattern";//這裡我們把圖案也描述出來
    }

    @Override
    public double cost() {
        
        return 10+clothes.cost();//我們把呼叫委託給被裝飾物件,以計算價錢,然後再加上FlowerPattern的價錢,得到最終結果
    }
    
}

同理,我們寫出草圖案的具體實現:

class GrassPattern extends Pattern{//草圖案是一個裝飾者,讓它擴充套件自Pattern,而Pattern又擴充套件自Clothes
    //我們為了能讓FlowerPattern能夠引用clothes,用一個例項變數記錄衣服,也就是被裝飾者,然後在構造器中將其記錄在例項變數裡
    private Clothes clothes;
    public GrassPattern(Clothes clothes){
        this.clothes = clothes;
    }
    @Override
    public String getDescription() {
        
        return clothes.getDescription() + ",GrassPattern";//這裡我們把圖案也描述出來
    }

    @Override
    public double cost() {
        
        return 5+clothes.cost();//我們把呼叫委託給被裝飾物件,以計算價錢,然後再加上GrassPattern的價錢,得到最終結果
    }
    
}

最後,測試一下:

public class TestDemo2 {
    public static void main(String[] args) {
        Clothes clothes = new BlueClothes();
        clothes = new FlowerPattern(clothes);
        clothes = new GrassPattern(clothes);
        System.out.println(clothes.getDescription()+"$"+clothes.cost());
    }
}

看到這裡,LZ相信大家一定有些熟悉,記得我們在學習IO的時候曾學過包裝流,

其中FileInputStream是被裝飾的“元件”。

BufferedInputStream是一個具體的“裝飾者”,它加入兩種行為:利用緩衝輸入來改進效能利用一個readLine()方法(用來一次讀取一行文字輸入資料)來增強介面。

LineNumberInputStream也是一個具體的“裝飾者”。它加上了計算行數的能力

這裡也引出了裝飾者模式的一個缺點,常常造成設計中有大量的小類,數量實在太多,可能會造成使用此API程式設計師的困擾。但是如果我們已經瞭解了裝飾者的工作原理,以後當使用別人的大量裝飾者的API時,就可以很容易辨別出他們的裝飾者類是如何組織的,以方便用包裝方式去的想要的行為。

 裝飾者模式就到此結束了,感謝大家的收看。