Java設計模式學習記錄-組合模式 Java設計模式學習記錄-GoF設計模式概述

前言

今天要介紹的設計模式是組合模式,組合模式也是結構型設計模式的一種,它主要體現了總體與部分的關係,其典型的應用就是樹形結構。組合是一組對象,其中的對象可能包含一個其餘對象,也可能包含一組其餘對象。html

組合模式

組合模式定義爲:將對象組合成樹形結構以表示「總體-部分」的層次結構。組合模式是單個對象和組合對象的使用具備一致性。node

在使用組合模式的使用要注意如下兩點:設計模式

組合中既要能包含個體,也要能包含其餘組合。安全

要抽象出對象和組合的公共特性。ide

舉例說明

介紹了一些基本內容,可能會仍是不清楚組合模式究竟是什麼樣的一個模式,仍是老樣子,舉🌰說明。post

在咱們的家用PC電腦上的文件結構就是一個很好的例子,例如在個人電腦上有以下圖所示的文件目錄結構。學習

從root文件夾到具體的文件,一層一層的這種結構就是典型的樹形結構,root是硬盤中的某個文件夾,能夠理解爲根節點,這個文件下下面有兩個文件夾和一個文件,image-folder文件夾這種有分支的能夠理解爲分支節點,文件則理解爲葉子節點優化

爲了要實現這種結構,這三種節點,咱們通常的思路是,建立三個根節點、分支節點、葉子節點這三個類,可是咱們發現根節點的特性其實和分支節點同樣,能夠理解爲一類,因此咱們只須要建立兩個類就能夠。this

定義分支節點(包含根節點)的接口url

/**
 * 定義分支節點(根節點)
 */
public interface IBranch {

    /**
     * 得到分支節點信息
     * @return
     */
    public String getInfo();

    /**
     * 增長分支節點(文件夾下還可能會有文件夾)
     * @param branch
     */
    public void addBranch(IBranch branch);

    /**
     * 增長葉子節點
     * @param leaf
     */
    public void addLeaf(ILeaf leaf);

    /**
     * 得到子集
     * @return
     */
    public ArrayList getChildren();
}

具體的實現以下

/**
 * 分支節點(文件夾)
 */
public class Folder implements IBranch{

    /**
     * 節點名稱
     */
    private String name;

    /**
     * 子集
     */
    private ArrayList children = Lists.newArrayList();

    /**
     * 帶參數的構造方法
     * @param name
     */
    public Folder(String name){
        this.name = name;
    }

    /**
     * 得到分支節點信息
     *
     * @return
     */
    @Override
    public String getInfo() {

        return "名稱:" + name;
    }

    /**
     * 增長分支節點(文件夾下還可能會有文件夾)
     *
     * @param branch
     */
    @Override
    public void addBranch(IBranch branch) {

        children.add(branch);
    }

    /**
     * 增長葉子節點
     *
     * @param leaf
     */
    @Override
    public void addLeaf(ILeaf leaf) {
        children.add(leaf);
    }

    /**
     * 得到子集
     *
     * @return
     */
    @Override
    public ArrayList getChildren() {
        return children;
    }
}

定義葉子節點的接口

/**
 * 定義葉子節點
 */
public interface ILeaf {

    /**
     * 得到葉子節點的信息
     * @return
     */
    public String getInfo();

}

由於葉子節點,不會有子集因此只須要一個得到描述信息的方法便可,具體的實現以下。

/**
 * 葉子節點(文件)
 */
public class File implements ILeaf {

    private String name;

    /**
     *
     * @param name
     */
    public File(String name){
        this.name = name;
    }

    /**
     * 得到葉子節點的信息
     *
     * @return
     */
    @Override
    public String getInfo() {
        return "名稱:"+name;
    }
}

節點類已經定義完成了,因此如今能夠開始組裝數據了,而後將最終的數據打印出來看看是否是這個結構。

public class ClientTest {

    public static void main(String[] args) {

        //定義根節點
        IBranch root = new Folder("root");

        //定義二級節點的文件夾
        IBranch imageFolder = new Folder("image-folder");
        IBranch documentFolder = new Folder("document-folder");
        //定義二級節點的文件
        ILeaf systemFile = new File("system-file.bat");

        //定義三級節點的文件夾
        IBranch pngFolder = new Folder("png-folder");
        IBranch gifFolder = new Folder("gif-folder");
        //定義三級節點的文件
        ILeaf testHtml = new File("test.html");
        ILeaf testJS = new File("test.js");

        //定義四級節點的文件,兩個png文件
        ILeaf test1png = new File("test1.png");
        ILeaf test2png = new File("test2.png");
        //定義四級節點的文件3個gif文件
        ILeaf my1gif = new File("my1.gif");
        ILeaf my2gif = new File("my2.gif");
        ILeaf my3gif = new File("my3.gif");

        //填充一級文件夾
        root.addBranch(imageFolder);
        root.addBranch(documentFolder);
        root.addLeaf(systemFile);
        //填充二級圖片文件夾
        imageFolder.addBranch(pngFolder);
        imageFolder.addBranch(gifFolder);
        //填充二級文檔文件夾
        documentFolder.addLeaf(testHtml);
        documentFolder.addLeaf(testJS);

        //填充三級png圖片文件夾
        pngFolder.addLeaf(test1png);
        pngFolder.addLeaf(test2png);
        //填充三級gif圖片文件夾
        gifFolder.addLeaf(my1gif);
        gifFolder.addLeaf(my2gif);
        gifFolder.addLeaf(my3gif);

        System.out.println(root.getInfo());

        //打印出來
        getChildrenInfo(root.getChildren());

    }

    /**
     * 遞歸遍歷文件
     * @param arrayList
     */
    private static void getChildrenInfo(ArrayList arrayList){

        int length = arrayList.size();

        for(int m = 0;m<length;m++){
            Object item = arrayList.get(m);
            //若是是葉子節點就直接打印出來名稱
            if(item instanceof ILeaf){
                System.out.println(((ILeaf) item).getInfo());
            }else {
                //若是是分支節點就先打印分支節點的名稱,再遞歸遍歷子節點
                System.out.println(((IBranch)item).getInfo());
                getChildrenInfo(((IBranch)item).getChildren());
            }

        }

    }

}

最終的打印結果:

名稱:root
名稱:image-folder
名稱:png-folder
名稱:test1.png
名稱:test2.png
名稱:gif-folder
名稱:my1.gif
名稱:my2.gif
名稱:my3.gif
名稱:document-folder
名稱:test.html
名稱:test.js
名稱:system-file.bat

 這個結果確實是咱們想要的,可是仔細看看其實仍是有能夠優化的地方,Folder和File都有包含名字的構造方法,以及getInfo()方法,那麼是否是能夠抽取出來?那就改變一下吧。

新增節點公共抽象類

/**
 * 節點公共抽象類
 */
public abstract class Node {

    private String name;

    /**
     * 帶參數的構造方法
     * @param name
     */
    public Node(String name){
        this.name = name;
    }

    /**
     * 得到節點信息
     * @return
     */
    public String getInfo(){
        return "名稱:"+name;
    }
}

改造後的File類

/**
 * 葉子節點(文件)
 */
public class File extends Node {
    
    /**
     * 調用父類的構造方法
     * @param name
     */
    public File(String name) {
        super(name);
    }
}

改造後的Folder類

/**
 * 分支節點(文件夾)
 */
public class Folder extends Node{

    /**
     * 子集
     */
    private ArrayList children = Lists.newArrayList();

    /**
     * 帶參數的構造方法
     * @param name
     */
    public Folder(String name){
        super(name);
    }

    /**
     * 新增節點,有多是文件也有多是文件夾
     * @param node
     */
    public void add(Node node){
        this.children.add(node);
    }

    /**
     * 得到子集
     *
     * @return
     */
    public ArrayList getChildren() {
        return children;
    }
}

改造後的使用方式

public class ClientTest {

    public static void main(String[] args) {

        //定義根節點
        Folder root = new Folder("root");

        //定義二級節點的文件夾
        Folder imageFolder = new Folder("image-folder");
        Folder documentFolder = new Folder("document-folder");
        //定義二級節點的文件
        File systemFile = new File("system-file.bat");

        //定義三級節點的文件夾
        Folder pngFolder = new Folder("png-folder");
        Folder gifFolder = new Folder("gif-folder");
        //定義三級節點的文件
        File testHtml = new File("test.html");
        File testJS = new File("test.js");

        //定義四級節點的文件,兩個png文件
        File test1png = new File("test1.png");
        File test2png = new File("test2.png");
        //定義四級節點的文件3個gif文件
        File my1gif = new File("my1.gif");
        File my2gif = new File("my2.gif");
        File my3gif = new File("my3.gif");

        //填充一級文件夾
        root.add(imageFolder);
        root.add(documentFolder);
        root.add(systemFile);
        //填充二級圖片文件夾
        imageFolder.add(pngFolder);
        imageFolder.add(gifFolder);
        //填充二級文檔文件夾
        documentFolder.add(testHtml);
        documentFolder.add(testJS);

        //填充三級png圖片文件夾
        pngFolder.add(test1png);
        pngFolder.add(test2png);
        //填充三級gif圖片文件夾
        gifFolder.add(my1gif);
        gifFolder.add(my2gif);
        gifFolder.add(my3gif);

        System.out.println(root.getInfo());

        //打印出來
        getChildrenInfo(root.getChildren());

    }

    /**
     * 遞歸遍歷文件
     * @param arrayList
     */
    private static void getChildrenInfo(ArrayList arrayList){

        int length = arrayList.size();

        for(int m = 0;m<length;m++){
            Object item = arrayList.get(m);
            //若是是葉子節點就直接打印出來名稱
            if(item instanceof File){
                System.out.println(((File) item).getInfo());
            }else {
                //若是是分支節點就先打印分支節點的名稱,再遞歸遍歷子節點
                System.out.println(((Folder)item).getInfo());
                getChildrenInfo(((Folder)item).getChildren());
            }

        }

    }

}

這樣實現起來的各個節點的代碼變的更簡潔了,可是組裝數據的的代碼是沒變的。由於放數據要麼本身造要麼從某個地方查詢出來,這麼個步驟是不能簡化的。

分析

如今咱們的這個實現過程就是使用的了組合模式,下面咱們來分析一下組合模式都有哪些部分組成。先來看一下根據上面這個例子畫出來的類圖。

組合模式主要有這麼幾個角色:

抽象構件角色

(Node類)這是一個抽象角色,它給參加組合的對象規定一個接口或抽象類,給出組合中對象的默認行爲。

葉子構件角色

(File類)表明參加組合的葉子節點對象,沒有子集,而且要定義出參加組合的原始對象行爲。

樹枝構件角色

(Folder類)表明參加組合的含義子對象的對象,而且也要給出參加組合的原始對象行爲以及遍歷子集的行爲。

組合模式的兩種形式

透明方式

透明方式來實現組合模式是指,按照上面舉得例子來講,File和Folder的方法和和屬性都同樣,就是說File也包含children屬性和getChildren方法二者在類上沒有什麼區別,只不過File的children爲null,getChildren()得到的也永遠是空。這樣葉子節點對象和樹枝節點對象的區別在抽象層次上就消失了,客戶端能夠同等對待全部對象。

這種方式的缺點是不夠安全,由於葉子節點和樹枝節點在本質上是有區別的,葉子節點的getChildren()方法和children的存在沒有意義,雖然在編譯時不會出錯,可是若是在運行時以前沒有作過處理是很容易拋出異常的。

安全方式(非透明)

安全方式實現的組合模式,就是上面的例子介紹的那樣,這種實現方式把葉子和樹枝完全的區分開來處理,並作到互不干擾,樹枝有單獨本身處理子類的方法,保證運行期不會出錯。

通常在以下狀況下應當考慮使用組合模式:

  1. 須要描述對象的部分和總體的等級結構。
  2. 須要客戶端忽略掉個體構件和組合構件的區別,客戶端平等對待因此構件。

其實在咱們平常的業務當中有不少場景其實都是可使用組合模式的,例如,某公司的人員組織結構,從CEO到小職員,一級一級的人員關係就可使用組合模式,還有就是在網上商城購物時,選擇地址,從省道區再到縣也是可使用組合模式的。

 

 

 

 

 

 

 

 

 

想了解更多的設計模式請查看Java設計模式學習記錄-GoF設計模式概述

相關文章
相關標籤/搜索