優雅的處理樹狀結構——組合模式總結

一、前言

本模式經 遍歷「容器」的優雅方法——總結迭代器模式 引出,繼續看最後的子菜單的案例html

二、組合模式的概念

組合模式,也叫 Composite 模式……是構造型的設計模式之一。java

組合模式容許對象組合成樹形結構,來表現「總體/部分」的層次結構,使得客戶端對單個對象和組合對象的使用具備一致性。設計模式

Composite Pattern緩存

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.  安全

有一些拗口,通俗的說:組合模式是關於怎樣將對象造成樹形結構來表現總體和部分的層次結構的成熟模式。數據結構

使用組合模式,可讓用戶以一致的方式處理個體對象和組合對象,組合模式的關鍵在於不管是個體對象仍是組合對象都實現了相同的接口或都是同一個抽象類的子類。 ide

即,組合模式,它能經過遞歸來構造樹形的對象結構,並能夠經過一個對象來訪問整個對象樹。post

即,組合模式,在大多數狀況下,可讓客戶端忽略對象個體和對象組合之間的差別。學習

2.一、組合模式的角色和類圖

結合數據結構裏的樹,其實很好寫出來。無非就是葉子和非葉子節點的的組合。測試

一、須要一個類爲葉子節點和非葉子節點的共同抽象父類,如圖裏的 Component 接口(抽象類也能夠),是樹形結構的節點的抽象:

  • 爲全部的對象,包括葉子節點,定義統一的接口(公共屬性,行爲等的定義)

  • 提供管理子節點對象的接口方法

  • [可選]提供管理父節點對象的接口方法 

二、設計一個 Leaf 類表明樹的葉節點,這個要單獨拿出來區分,是 Component 的實現子類

三、設計一個 Composite 類做爲樹枝節點,即非葉節點,也是 Component 的實現子類

四、client 客戶端,它使用 Component 接口操做樹

2.二、組合(Composite)、組件(Component接口)、和樹的關係

在該模式裏熟悉一些定義,其實不必死記硬背,定義隨便起名字,只要能自洽便可。

一、組合(Composite)包含了組件(Component)

二、組件 Component 接口 = 組合Composite + 葉節點Leaf,由於組件是抽象的,葉子和枝節點(組合)是組件的具體表現,很好理解。

其實就是遞歸,獲得的是由上而下的樹形結構,根部是一個組合Composite,而組合的分支延伸展開(組合包含了組件),直至葉子節點leaf爲止。

三、基於組合模式改進迭代器模式裏的菜單系統

如菜單子系統的實現,就是典型的樹狀結構

 

須要一個抽象組件 Component,例子裏是 MenuComponent,做爲菜單節點和菜單節點項(葉子)的共同接口,可以讓客戶端使用統一的方法來操做菜單和菜單項。

以下,全部的組件(葉子+樹枝(非葉子))都必須實現這個組件接口,又由於葉子節點(即菜單項)和樹枝節點(即組合節點)分工不一樣,因此須要在抽象的組件類中實現默認的方法,由於某些方法可能只在某類節點中有意義。通常是作拋出運行時異常(自定義的異常)的處理。

/**
 * 菜單和菜單項的抽象——組件,讓菜單和菜單項能共用
 * 又由於但願這個抽象組件能提供一些默認的操做,故使用了抽象類
 */
public abstract class MenuComponent {
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public String getDescription() {
        throw new UnsupportedOperationException();
    }

    public double getPrice() {
        throw new UnsupportedOperationException();
    }

    public boolean isVegetarian() {
        throw new UnsupportedOperationException();
    }

    public void print() {
        throw new UnsupportedOperationException();
    }
}

下面編寫葉子節點——菜單的菜單項類。

這是組合模式類圖裏的葉子角色,它只負責實現組合的內部元素的行爲,所以宏觀上管理整個菜單的方法,好比 add 、remove 等,它不該該複寫,對她沒有意義。

/**
 * 葉子節點,表明菜單裏的一項
 * 只複寫對其有意義的方法,沒有意義的方法,好比得到子節點等,就不理會便可
 */
public class MenuItem extends MenuComponent {
    private String name;
    private String description;
    private boolean vegetarian;
    private double price;

    public MenuItem(String name, String description, boolean vegetarian, double price) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public void print() {
        System.out.print("  " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }

        System.out.println(", " + getPrice());
        System.out.println("     -- " + getDescription());
    }
}

下面,編寫樹枝節點——菜單,也就是組合類。

以前的菜單項是的單個的組件類,而組合類才體現了遞歸思想,組合類聚合了組件類。一些對其沒有意義的方法,一樣不須要複寫實現。

菜單也能夠有子菜單(菜單項其實本質也能夠是子菜單),因此組合了一個 Arraylist<MenuComponent>,由於菜單和菜單項都屬於 MenuComponent,那麼使用一樣的方法,能夠兼顧二者,這正應了組合模式的意義——使用組合模式,可讓用戶以一致的方式處理個體對象和組合對象,組合模式的關鍵在於不管是個體對象仍是組合對象都實現了相同的接口或都是同一個抽象類的子類。 

/**
 * 樹枝節點,也就是組合節點——表明各個菜單
 */
public class Menu extends MenuComponent {
    private String name;
    private String description;
    /**
     * 依賴了菜單組件,遞歸的實現
     */
    private List<MenuComponent> menuComponents = new ArrayList<>();

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    /**
     * 由於菜單做爲樹枝節點,它是一個組合,包含了菜單項和其餘的子菜單,因此 print()應該打印出它包含的一切。
     */
    @Override
    public void print() {
        System.out.print("\n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");

        // 使用了迭代器(迭代器模式和組合模式的有機結合),遍歷菜單的菜單項
        Iterator iterator = menuComponents.iterator();
        while (iterator.hasNext()) { 
            // 打印這個節點包含的一切,print 能夠兼顧兩類節點,這是組合模式的特色
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print(); // 遞歸思想的應用
        }
    }
}

由於菜單是一個組合,包含了菜單項和其餘的子菜單,因此它的print()應該打印出它包含的一切,此時遞歸思想派上了用場。

下面編寫客戶端——服務員類

/**
 * 客戶端,也就是服務員類,聚合了菜單組件接口(這裏是抽象類)控制菜單,解耦合
 */
public class Waitress {
    /**
     * 聚合了菜單組件——這一抽象節點,能兼顧葉子節點和樹枝節點
     */
    private MenuComponent allMenus;

    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void printMenu() {
        allMenus.print();
    }
}

客戶端類代碼很簡單,只須要聚合一個頂層的組件接口便可。最頂層的菜單組件能夠兼顧全部菜單或者菜單項,故客戶端只須要調用一次最頂層的print方法,便可打印整個菜單系統。

總體結構以下圖:

下面建立菜單

public class MenuTestDrive {
    public static void main(String args[]) {
        // 建立全部的菜單系統,它們本質上都是組合節點——MenuComponent
        MenuComponent pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast");
        MenuComponent dinerMenu = new Menu("DINER MENU", "Lunch");
        MenuComponent cafeMenu = new Menu("CAFE MENU", "Dinner");
        MenuComponent dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
        // 建立頂級root節點——allMenus,表明整個菜單系統
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
        allMenus.add(pancakeHouseMenu); // 把每一個菜單系統,組合到root節點,當作樹枝節點
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);
        // 爲煎餅屋的菜單系統,增長菜單項
        pancakeHouseMenu.add(new MenuItem(
                "K&B's Pancake Breakfast",
                "Pancakes with scrambled eggs, and toast"));// 爲餐廳的菜單系統,增長菜單項
        dinerMenu.add(new MenuItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat"));
        dinerMenu.add(new MenuItem("BLT", "Bacon with lettuce & tomato on whole wheat"));
        // 爲餐廳的菜單系統,增長子菜單——這個其實也是菜單項,可是,是樹枝,這是一個飯後甜點子菜單
        dinerMenu.add(dessertMenu);
        // 爲飯後甜點菜單系統,增長菜單項
        dessertMenu.add(new MenuItem("Apple Pie", "Apple pie with a flakey crust, topped with vanilla icecream"));
        dessertMenu.add(new MenuItem("Cheesecake", "Creamy New York cheesecake, with a chocolate graham crust"));
        // 爲咖啡廳菜單系統,增長菜單項
        cafeMenu.add(new MenuItem(
                "Veggie Burger and Air Fries",
                "Veggie burger on a whole wheat bun, lettuce, tomato, and fries"));
        // 把整個菜單傳給客戶端
        Waitress waitress = new Waitress(allMenus);
        waitress.printMenu();
    }
}

四、單一職責和組合模式的矛盾

這是一個很典型的折中設計問題:有時候會故意違反一些設計原則,去實現一些特殊需求。仍是那句話,學習設計模式不要死記硬背,最後仍是要遵循具體的技術條件和服務於特定的業務場景。

回顧案例發現:組合模式不但要管理整個菜單——這個樹狀層次結構,還要執行菜單的一些具體操做動做。明顯的,違反了單一職責原則,能夠這麼說:組合模式犧牲了單一職責的設計原則,換取了程序的透明性(transparency)——經過讓組件的接口同時包含一些樹枝子節點(組合節點)和葉子子節點的操做,客戶就能夠將組合節點和葉子節點一視同仁,而一個元素到底是組合節點仍是葉子節點對客戶都是透明的。

若是不讓組件接口同時具有多種類型節點的操做,雖然設計上安全,職責也分開,可是失去了透明性,即客戶端必須顯示的使用條件(通常用 instanceOf )來判斷節點類型

五、迭代器模式 + 組合模式來實現分擔部分責任

可以讓客戶端使用迭代器模式去遍歷整個菜單系統,比方說,女招待可能想要遊走整個菜單,只打印 / 挑選素食的菜單項。

想要實現一個組合模式+迭代器模型的菜單系統,能夠爲每一個組件都加上 createIterator() 方法。

import java.util.Iterator;

/**
 * 先從抽象的組件節點入手,加上迭代器
 */
public abstract class MenuComponent {
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public String getDescription() {
        throw new UnsupportedOperationException();
    }

    public double getPrice() {
        throw new UnsupportedOperationException();
    }

    public boolean isVegetarian() {
        throw new UnsupportedOperationException();
    }

    // 加上迭代器,這裏直接使用 JDK 的迭代器
    public abstract Iterator createIterator();

    public void print() {
        throw new UnsupportedOperationException();
    }
}

一樣的套路,編寫葉子節點和樹枝節點,繼承這個抽象類

public class Menu extends MenuComponent {
    private List<MenuComponent> menuComponents = new ArrayList<>();
    private String name;
    private String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public Iterator createIterator() {
        return new CompositeIterator(menuComponents.iterator());
    }

    @Override
    public void print() {
        Iterator iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
        }
    }
}
 
//////////////////////
import java.util.Iterator;

public class MenuItem extends MenuComponent {
    private String name;
    private String description;
    private boolean vegetarian;
    private double price;

    public MenuItem(String name, String description, boolean vegetarian, double price) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public Iterator createIterator() {
        return new NullIterator();
    }

    @Override
    public void print() {
        System.out.print("  " + getName());
        if (isVegetarian()) {
            System.out.print("(vegetable)");
        }
        System.out.println(", " + getPrice());
        System.out.println("     -- " + getDescription());
    }
}

發現了兩個新東西,一個是 NullIterator() 和 CompositeIterator(),尤爲是後者,使用了遞歸思想。

回憶:在寫 MenuComponent 類的 print 方法時,利用了一個迭代器遍歷組件內的每一個項,若是遇到的是菜單,就會遞歸地調度 print 方法處理它,換句話說,MenuComponent 是在「內部」自行處理遍歷——內部迭代器模式。

可是在以下的 CompositeIterator 中,實現的是一個「外部」的迭代器,因此有許多須要追蹤的事情。外部迭代器必須維護它在遍歷中的位置,以便外部能夠經過 hasNext 和 next 來驅動遍歷。在 CompositeIterator 中,必須維護組合遞歸結構的位置,這也是爲何在組合層次結構中上上下下時,使用堆棧 JDK 的 Stack 來維護遊標的位置。

import java.util.Iterator;
import java.util.Stack;

/**
 * 自定義組合模式的組合節點的專屬迭代器 CompositeIterator
 */
public class CompositeIterator implements Iterator {
    private Stack<Iterator> stack = new Stack<>();

    // 把要遍歷的 Menu 組合的迭代器 iterator 傳入,menuComponents.iterator() 被傳入一個 stack 中保存位置
    public CompositeIterator(Iterator iterator) {
        stack.push(iterator);
    }

    // 當客戶端須要取得下一個元素的時候,先判斷是否存在下一個元素
    @Override
    public Object next() {
        if (hasNext()) {
            Iterator iterator = stack.peek(); // 僅查看當前的棧頂元素——迭代器,不出棧
            MenuComponent component = (MenuComponent) iterator.next(); // 使用該棧頂的迭代器,取出要遍歷的組合的元素
            if (component instanceof Menu) {
                // 若是取出的元素仍然是菜單,那須要繼續遍歷它,故要記錄它的位置,把它的迭代器取出來 
                // 調用 component.createIterator() 返回 CompositeIterator,這個 CompositeIterator 仍然包含一個本身的 stack,繼續存入棧中
                stack.push(component.createIterator()); 
            }

            return component;
        } else {
            return null;
        }
    }

    @Override
    public boolean hasNext() {
        if (stack.empty()) { // 若是棧是空,直接返回 false
            return false;
        } else {
            Iterator iterator = stack.peek(); // 僅查看當前的棧頂元素——迭代器,不出棧
            // 判斷當前的頂層元素是否還有下一個元素,若是棧空了,就說明當前頂層元素沒有下一個元素,返回 false,此處判斷爲 true
            if (!iterator.hasNext()) {
                stack.pop(); // 若是當前棧頂元素,沒有下一個元素了,就把當前棧頂元素出棧,遞歸的繼續判斷下一個元素
                return hasNext();
            } else { // 不然表示還有下一個元素,直接返回 true
                return true;
            }
        }
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

參考 java.util.Stack類中的peek()方法

經過測試,來觀察上述代碼的執行過程:

public class TestCompositeStack {
    public static void main(String[] args) {
        MenuComponent pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast");
        // 建立頂級root節點——allMenus,表明整個菜單系統
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
        allMenus.add(pancakeHouseMenu); // 把菜單系統,組合到root節點,當作樹枝節點

        // 爲煎餅小屋的菜單系統,增長菜單項
        pancakeHouseMenu.add(new MenuItem(
                "K&B's Pancake Breakfast",
                "Pancakes with scrambled eggs, and toast"));
        pancakeHouseMenu.add(new MenuItem(
                "Regular Pancake Breakfast",
                "Pancakes with fried eggs, sausage"));
        testStack(allMenus);
    }

    public static void testStack(MenuComponent menuComponent) {
        CompositeIterator compositeIterator = new CompositeIterator(menuComponent.createIterator());
        while (compositeIterator.hasNext()) {
            MenuComponent menuComponent1 = (MenuComponent) compositeIterator.next();
        }
    }
}

5.一、空迭代器

 若是菜單項沒什麼能夠遍歷的,好比葉子節點,那麼通常要給其遍歷方法:

 一、返回 null。可讓 createIterator() 方法返回 null,可是若是這麼作,客戶端的代碼就須要條件語句來判斷返回值是否爲 null,不太好;

 二、返回一個迭代器,而這個迭代器的 hasNext() 永遠返回 false。這個是更好的方案,客戶端不用再擔憂返回值是否爲 null。等於建立了一個迭代器,其做用是「沒做用」。

import java.util.Iterator;

/**
 * 自定義組合模式的葉子節點的專屬迭代器
 */
public class NullIterator implements Iterator {

    @Override
    public Object next() {
        return null;
    }

    @Override
    public boolean hasNext() {
        return false;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

客戶端代碼:

import java.util.Iterator;

public class Waitress {
    private MenuComponent allMenus;

    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void printMenu() {
        allMenus.print();
    }

    public void printVegetarianMenu() {
        Iterator iterator = allMenus.createIterator();while (iterator.hasNext()) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            try {
                if (menuComponent.isVegetarian()) {
                    menuComponent.print();
                }
            } catch (UnsupportedOperationException ignored) {
            }
        }
    }
}

六、組合模式和緩存

有時候,若是組合的結構很是複雜,或者遍歷的代價很大,那麼能夠爲組合節點實現一個緩存,若是業務需求是須要不斷的遍歷一個組合結構,那麼能夠把遍歷的節點存入緩存,省去每次都遞歸遍歷的開支。

七、組合模式的優勢

組合模式包含有個體對象和組合對象,並造成樹形結構,使用戶能夠方便地處理個體對象和組合對象。

一、組合對象和個體對象實現了相同的接口,用戶通常不需區分個體對象和組合對象。

二、當增長新的Composite節點和Leaf節點時,用戶的重要代碼不須要做出修改。

八、其餘案例——文件系統也是典型的樹狀結構系統

下面使用接口來基於組合模式,實現簡單的文件系統

import java.util.List;

/*
 * 文件節點抽象(是文件和目錄的父類)
 */
public interface IFile {
    //顯示文件或者文件夾的名稱
    public void display();
    public boolean add(IFile file);
    public boolean remove(IFile file);
    //得到子節點
    public List<IFile> getChild();
}
 
/////////////////////////// 文件節點
import java.util.List;

public class File implements IFile {
    private String name;
    
    public File(String name) {
        this.name = name;
    }
    
    public void display() {
        System.out.println(name);
    }

    public List<IFile> getChild() {
        return null;
    }

    public boolean add(IFile file) {
        return false;
    }

    public boolean remove(IFile file) {
        return false;
    }
}

//////////////////// 目錄節點
import java.util.ArrayList;
import java.util.List;

public class Folder implements IFile{
    private String name;
    private List<IFile> children; // 聚合了文件抽象節點
    
    public Folder(String name) {
        this.name = name;
        children = new ArrayList<IFile>();
    }
    
    public void display() {
        System.out.println(name);
    }

    public List<IFile> getChild() {
        return children;
    }


    public boolean add(IFile file) {
        return children.add(file);
    }


    public boolean remove(IFile file) {
        return children.remove(file);
    }
}

////////////////////客戶端
import java.util.List;

public class MainClass {
    public static void main(String[] args) {
        IFile rootFolder = new Folder("C:");
        IFile dashuaiFolder = new Folder("dashuai");
        IFile dashuaiFile = new File("dashuai.txt");
        rootFolder.add(dashuaiFolder);
        rootFolder.add(dashuaiFile);
        
        IFile aFolder = new Folder("aFolder");
        IFile aFile = new File("aFile.txt");
        dashuaiFolder.add(aFolder);
        dashuaiFolder.add(aFile);
        
        displayTree(rootFolder, 0);
    }
    
    // 層序遍歷樹
    private static void displayTree(IFile rootFolder, int deep) {
        for(int i = 0; i < deep; i++) {
            System.out.print("--");
        }
        //顯示自身的名稱
        rootFolder.display();
        //得到子樹
        List<IFile> children = rootFolder.getChild();
        //遍歷子樹
        for(IFile file : children) {
            if(file instanceof File) {
                for(int i = 0; i <= deep; i++) {
                    System.out.print("--");
                }
                file.display();
            } else {
                displayTree(file, deep + 1);
            }
        }
    }
}
相關文章
相關標籤/搜索