設計模式:裝飾者模式與組合模式


裝飾者模式

概念

動態地給一個對象添加一些額外的職責。與繼承的模式對比,裝飾者模式更為靈活。

類圖

img

以上共有四個角色:

抽象構件(Component)角色:給出一個抽象接口,以規范准備接收附加責任的對象。

具體構件(ConcreteComponent)角色:定義一個將要接收附加責任的類。

裝飾(Decorator)角色:持有一個構件(Component)對象的實例,並定義一個與抽象構件接口一致的接口。

具體裝飾(ConcreteDecorator)角色:負責給構件對象“貼上”附加的責任。

實例介紹

火鍋包括鍋底與配菜,有很多的組合方式,適合用裝飾者模式實現。

1.定義被裝飾對象基類GuoDi(可以是抽象類也可以是接口) 相當於Component

package com.example.demo;

public interface GuoDi {

    /**
     * 鍋底的價格
     * @return
     */
    public float cost();

    /**
     * 鍋底的名字
     * @return
     */
    public String name();
}


2.定義具體被裝飾對象(也就是各種鍋底,這里有兩種) 相當於ConcreteComponent

package com.example.demo;

public class YuanYang implements GuoDi {

    /**
     * 鍋底的價格
     *
     * @return
     */
    @Override
    public float cost() {
        return 50.0f;
    }

    /**
     * 鍋底的名字
     *
     * @return
     */
    @Override
    public String name() {
        return "鴛鴦鍋底";
    }
}
package com.example.demo;

public class XiangLa implements GuoDi {

    /**
     * 鍋底的價格
     *
     * @return
     */
    @Override
    public float cost() {
        return 40.0f;
    }

    /**
     * 鍋底的名字
     *
     * @return
     */
    @Override
    public String name() {
        return "香辣鍋底";
    }
}

3.定義裝飾者抽象類PeiCai,相當於Decorator

package com.example.demo;

public class PeiCai implements GuoDi {

    private GuoDi guodi;

    public PeiCai(GuoDi guodi) {
        super();
        this.guodi = guodi;
    }

    /**
     * 鍋底的價格
     *
     * @return
     */
    @Override
    public float cost() {
        return guodi.cost();
    }

    /**
     * 鍋底的名字
     *
     * @return
     */
    @Override
    public String name() {
        return guodi.name();
    }
}

4.定義具體的裝飾者對象(這里也是定義兩個),相當於ConcreteDecorator

package com.example.demo;

public class MaLaNiuRou extends PeiCai {

    public MaLaNiuRou(GuoDi guodi) {
        super(guodi);
    }
    @Override
    public float cost() {
        return super.cost()+60f;
    }
    @Override
    public String name() {
        return super.name()+"+麻辣牛肉";
    }
}

package com.example.demo;

public class DouFuPi extends PeiCai{

    public DouFuPi(GuoDi guodi) {
        super(guodi);
    }

    @Override
    public float cost() {
        return super.cost()+20.0f;
    }
    @Override
    public String name() {
        return super.name()+"+豆腐皮";
    }
}

5.創建一個測試類Test

package com.example.demo;

public class Test {

    public static void main(String[] args) {
        //點個香辣鍋底
        GuoDi guodi = new XiangLa ();
        //來個麻辣牛肉
        MaLaNiuRou niurou = new MaLaNiuRou(guodi);
        //再來一個豆腐皮
        DouFuPi douFuPi = new DouFuPi(niurou);
        System.out.println("您點了"+douFuPi.name());
        System.out.println("共消費"+douFuPi.cost());
    }
}

基本思想

1.編寫一個類,實現與被裝飾類相同的接口,目的是他們有相同的行為。

我們做了MaLaNiuRou這個類,實現與被裝飾類XiangLa 相同的接口GuoDi,目的是他們有相同的行為:cost(),name()。

2.定義一個實例變量,引用被裝飾的對象,目的與原來的老對象進行交接。

也就是

//點個香辣鍋底
GuoDi guodi = new XiangLa ();
//來個麻辣牛肉
MaLaNiuRou niurou = new MaLaNiuRou(guodi);

3,定義構造方法。把被裝飾的對象注入進來。

public MaLaNiuRou(GuoDi guodi) {
        super(guodi);
}

4.對於不需要改寫的方法,調用被裝飾對象的方法。

5.對於要改寫的方法,改寫即可。

@Override
public float cost() {
return super.cost()+60f;
}
@Override
public String name() {
return super.name()+"+麻辣牛肉";
}

優點

(1)裝飾模式與繼承關系的目的都是要擴展對象的功能,但是裝飾模式可以提供比繼承更多的靈活性。裝飾模式允許系統動態決定“貼上”一個需要的“裝飾”,或者除掉一個不需要的“裝飾”。繼承關系則不同,繼承關系是靜態的,它在系統運行前就決定了。
(2)通過使用不同的具體裝飾類以及這些裝飾類的排列組合,設計師可以創造出很多不同行為的組合。

缺點

被裝飾者一旦身份增加,作為裝飾類,也需要相應的擴展,這必然造成編碼的負擔。

這個個人覺得其實不算是缺點,與繼承相比,裝飾功能的細化必然會導致產生很多的裝飾對象。魚與熊掌不可兼得。

混淆點

與代理模式的區別

對於裝飾者模式來說,裝飾者與被裝飾者都實現同一個接口,對於代理模式來說,代理類與真實處理的類都實現同一個接口。而且都要把后者注入到前者。實現方式差別不大。

但是其中的關注點是不一樣的。

代理模式 偏重因自己無法完成或自己無需關心,需要他人干涉事件流程,更多的是對對象的控制。
裝飾者模式 偏重對原對象功能的擴展,擴展后的對象仍是是對象本身。

也就是思想的側重點不一樣。

在我們實際業務中裝飾模式使用的案例就是IO類,經常可以看到一個流包裝之后再包裝。而代理模式的使用就有Spring AOP和Spring 事務。不過它們使用的是動態代理。因為普通的代理例如上面的例子稱為靜態代理,只能代理某一類對象,但在我們實際業務中不可能為每個類都去寫一個代理類,所以這個時候就出現了動態代理。動態代理是使用反射的方法實現的,只用傳入類的類型就可以達到增強類的方法。

應用場景

IO類(上文提到),或者遇到需要動態添加職能的場景。

組合模式

概念

組合模式對多個對象形成樹形結構以表示具有“整體部分”關系的層次結構。組合模式對葉子對象和組合對象使用具有一致性。

類圖

說明:

Component:為葉子與容器的抽象。定義操作葉子的方法,比如增刪改,獲取葉子等另外還要定義葉子的行為。

Leaf:葉子結點,沒有子節點。實現葉子具體的行為。

Composite:容器節點對象,也就是非葉子節點。內部有集合存儲子節點。可以實現訪問子節點的方法。在組合模式中,由於容器仍然包含容器容器,所以容器需要使用遞歸方法對子元素進行處理。

示例介紹

文件夾殺毒業務:

常規實現

package demo1;//為了突出核心框架代碼,我們對殺毒過程的實現進行了大量簡化

import java.util.ArrayList;

//圖像文件類   
class ImageFile {  
    private String name;  
  
    public ImageFile(String name) {  
        this.name = name;  
    }  
  
    public void killVirus() {  
        //簡化代碼,模擬殺毒   
        System.out.println("----對圖像文件'" + name + "'進行殺毒");  
    }  
}

//文本文件類   
class TextFile {  
    private String name;  
  
    public TextFile(String name) {  
        this.name = name;  
    }  
  
    public void killVirus() {  
        //簡化代碼,模擬殺毒   
        System.out.println("----對文本文件'" + name + "'進行殺毒");  
    }  
}

//文件夾類   
class Folder {  
    private String name;  
    //定義集合folderList,用於存儲Folder類型的成員   
    private ArrayList<Folder> folderList = new ArrayList<Folder>();  
    //定義集合imageList,用於存儲ImageFile類型的成員   
    private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>();  
    //定義集合textList,用於存儲TextFile類型的成員   
    private ArrayList<TextFile> textList = new ArrayList<TextFile>();  
      
    public Folder(String name) {  
        this.name = name;  
    }  
      
    //增加新的Folder類型的成員   
    public void addFolder(Folder f) {  
        folderList.add(f);  
    }  
      
    //增加新的ImageFile類型的成員   
    public void addImageFile(ImageFile image) {  
        imageList.add(image);  
    }  
      
    //增加新的TextFile類型的成員   
    public void addTextFile(TextFile text) {  
        textList.add(text);  
    }  
          

  
    public void killVirus() {  
        System.out.println("****對文件夾'" + name + "'進行殺毒");  //模擬殺毒   
          
        //如果是Folder類型的成員,遞歸調用Folder的killVirus()方法   
        for(Object obj : folderList) {  
            ((Folder)obj).killVirus();  
        }  
          
        //如果是ImageFile類型的成員,調用ImageFile的killVirus()方法   
        for(Object obj : imageList) {  
            ((ImageFile)obj).killVirus();  
        }  
          
        //如果是TextFile類型的成員,調用TextFile的killVirus()方法   
        for(Object obj : textList) {  
            ((TextFile)obj).killVirus();  
        }  
    }   
}

測試類

class Client {  
    public static void main(String args[]) {  
        Folder folder1,folder2,folder3;  
        folder1 = new Folder("Sunny的資料");  
        folder2 = new Folder("圖像文件");  
        folder3 = new Folder("文本文件");  
          
        ImageFile image1,image2;  
        image1 = new ImageFile("小龍女.jpg");  
        image2 = new ImageFile("張無忌.gif");  
          
        TextFile text1,text2;  
        text1 = new TextFile("九陰真經.txt");  
        text2 = new TextFile("葵花寶典.doc");  
          
        folder2.addImageFile(image1);  
        folder2.addImageFile(image2);  
        folder3.addTextFile(text1);  
        folder3.addTextFile(text2);  
        folder1.addFolder(folder2);  
        folder1.addFolder(folder3);  
          
        folder1.killVirus();  
    }  
} 

該實現的弊端:

1.文件夾類Folder在進行多個集合存儲需要針對不同的成員進行增刪改方法,比較冗余。
2.客戶端無法對容器和葉子進行統一的處理,因為沒有進行抽象。

3.如果增加新的葉子類型,比如視頻文件,需要修改Folder類。

用組合模式進行解決:

import java.util.*;  
  
//抽象文件類:抽象構件   
abstract class AbstractFile {  
    public abstract void add(AbstractFile file);  
    public abstract void remove(AbstractFile file);  
    public abstract AbstractFile getChild(int i);  
    public abstract void killVirus();  
}  
  
//圖像文件類:葉子構件   
class ImageFile extends AbstractFile {  
    private String name;  
      
    public ImageFile(String name) {  
        this.name = name;  
    }  
      
    public void add(AbstractFile file) {  
       System.out.println("對不起,不支持該方法!");  
    }  
      
    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  
      
    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  
      
    public void killVirus() {  
        //模擬殺毒   
        System.out.println("----對圖像文件'" + name + "'進行殺毒");  
    }  
}  
  
//文本文件類:葉子構件   
class TextFile extends AbstractFile {  
    private String name;  
      
    public TextFile(String name) {  
        this.name = name;  
    }  
      
    public void add(AbstractFile file) {  
       System.out.println("對不起,不支持該方法!");  
    }  
      
    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  
      
    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  
      
    public void killVirus() {  
        //模擬殺毒   
        System.out.println("----對文本文件'" + name + "'進行殺毒");  
    }  
}  
  
//視頻文件類:葉子構件   
class VideoFile extends AbstractFile {  
    private String name;  
      
    public VideoFile(String name) {  
        this.name = name;  
    }  
      
    public void add(AbstractFile file) {  
       System.out.println("對不起,不支持該方法!");  
    }  
      
    public void remove(AbstractFile file) {  
        System.out.println("對不起,不支持該方法!");  
    }  
      
    public AbstractFile getChild(int i) {  
        System.out.println("對不起,不支持該方法!");  
        return null;  
    }  
      
    public void killVirus() {  
        //模擬殺毒   
        System.out.println("----對視頻文件'" + name + "'進行殺毒");  
    }  
}  
  
//文件夾類:容器構件   
class Folder extends AbstractFile {  
    //定義集合fileList,用於存儲AbstractFile類型的成員   
    private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>();  
    private String name;  
          
    public Folder(String name) {  
        this.name = name;  
    }  
      
    public void add(AbstractFile file) {  
       fileList.add(file);    
    }  
      
    public void remove(AbstractFile file) {  
        fileList.remove(file);  
    }  
      
    public AbstractFile getChild(int i) {  
        return (AbstractFile)fileList.get(i);  
    }  
      
    public void killVirus() {  
        System.out.println("****對文件夾'" + name + "'進行殺毒");  //模擬殺毒   
          
        //遞歸調用成員構件的killVirus()方法   
        for(Object obj : fileList) {  
            ((AbstractFile)obj).killVirus();  
        }  
    }  
}

客戶端測試方法:

class Client {  
    public static void main(String args[]) {  
        //針對抽象構件編程   
        AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4;  
          
        folder1 = new Folder("Sunny的資料");  
        folder2 = new Folder("圖像文件");  
        folder3 = new Folder("文本文件");  
        folder4 = new Folder("視頻文件");  
          
        file1 = new ImageFile("小龍女.jpg");  
        file2 = new ImageFile("張無忌.gif");  
        file3 = new TextFile("九陰真經.txt");  
        file4 = new TextFile("葵花寶典.doc");  
        file5 = new VideoFile("笑傲江湖.rmvb");  
  
        folder2.add(file1);  
        folder2.add(file2);  
        folder3.add(file3);  
        folder3.add(file4);  
        folder4.add(file5);  
        folder1.add(folder2);  
        folder1.add(folder3);  
        folder1.add(folder4);  
          
        //從“Sunny的資料”節點開始進行殺毒操作   
        folder1.killVirus();  
    }  
} 

改進的效果

由於1557889791317

進行了抽象,所以能夠針對不同的成員進行增刪改方法。

而且如果增加新的葉子類型,比如視頻文件,不需要修改Folder類,只需要寫一個實現AbstractFile接口的視頻文件類就行了。而且各個容器相對獨立,符合開閉原則,也方便管理。

實現的基本思想

1.定義抽象類或者接口。定義操作葉子的方法,比如增刪改,獲取葉子等另外還要定義具體的業務行為。

2.分別實現葉子類與容器類,葉子類主要實現業務的行為。容器類內部維護一個抽象類的集合,並且通過這個集合實現增刪改葉子。然后通過遞歸的方式便利集合實現業務行為。

優點

1.組合模式可以無差別的使用容器對象與葉子對象。

2.新增容器與葉子都很友好。

3.與常規的操作樹的方式比,組合模式通過遞歸的方式,可以形成復雜的樹形結構,但對樹形結構的控制卻非常簡單。

缺點

新增構件只要實現抽象就能進入父構件中,不能進行約束,如果非要約束的話,需要在運行中進行類型檢查來實現。

應用場景

業務場景中具有樹形結構或者層次結構中,比如文件夾,企業的組織構成業務。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM