天天看點

組合模式

一、定義

組合模式(Composite Pattern)也稱為 整體-部分(Part-Whole)模式,它的宗旨是通過将單個對象(葉子節點)群組合對象(樹枝節點)用相同的接口進行表示,使得客戶對單個對象群組合對象的使用具有一緻性。這種類型的設計模式屬于結構型模式,它建立了對象組的樹形結構。

組合模式 一般用來描述整體與部分的關系,它将對象組織到樹形結構中,最頂層的節點稱為根節點,根節點下面可以包含 樹枝節點和葉子節點,樹枝節點下面又可以包含樹枝節點和葉子節點。如下圖所示:

組合模式

由上圖可以看出,其實根節點和樹枝節點本質上是同一種資料類型(藍色圓圈,可以作為容器使用;而葉子節點與樹枝節點在語義上不屬于同一種類型,但是在組合模式中,會把樹枝節點和葉子節點認為是同一種資料類型(用同一接口定義),讓它們具備一緻行為。這樣,在組合模式 中,整個樹形結構中的對象都是同一種類型,帶來的一個好處就是客戶無需辨識樹枝節點還是葉子節點,而是可以直接進行操作,給客戶使用帶來極大的便利。組合模式 核心:借助同一接口,使葉子節點和樹枝節點的操作具備一緻性。

 組成元素:

  1. 抽象構件角色(Composite):是組合中對象聲明接口,實作所有類共有接口的預設行為。
  2. 樹葉構件角色(Leaf):上述提到的單個對象,葉節點沒有子節點。
  3. 樹枝構件角色(Composite):定義有子部件的組合部件行為,存儲子部件,在Component接口中實作與子部件有關的操作。
  4. 用戶端(Client):使用 Component 部件的對象。

二、應用場景

當子系統與其内各個對象層次呈現樹形結構時,可以使用組合模式讓該子系統内各個對象層次的行為操作具備一緻性。用戶端使用該子系統内任意一個層次對象時,無須進行區分,直接使用通用操作即可,為用戶端的使用帶來了便捷。如果樹形結構系統不使用組合模式 進行架構,那麼按照正常的思維邏輯,對該系統進行職責分析,按上文樹形結構圖所示,該系統具備兩種對象層次類型:樹枝節點和葉子節點。那麼我們就需要構造兩種對應的類型,然後由于樹枝節點具備容器功能,是以樹枝節點類内部需維護多個集合存儲其他對象層次(eg:

List<Composite>

,

List<Leaf>

),如果目前系統對象層次更複雜時,那麼樹枝節點内就又要增加對應的層次集合,這對樹枝節點的建構帶來了巨大的複雜性,臃腫性以及不可擴充性。同時用戶端通路該系統層次時,還需進行層次區分,這樣才能使用對應的行為,給用戶端的使用也帶來了巨大的複雜性。而如果使用組合模式建構該系統,由于組合模式抽取了系統各個層次的共性行為,具體層次隻需按需實作所需行為即可,這樣子系統各個層次就都屬于同一種類型,是以樹枝節點隻需維護一個集合(

List<Component>

)即可存儲系統所有層次内容,并且用戶端也無需區分該系統各個層次對象,對内系統架構簡潔優雅,對外接口精簡易用。

組合模式在代碼具體實作上,有兩種不同的方式:

  1. 透明模式:把組合(樹節點)使用的方法放到統一行為(Component)中,讓不同層次(樹節點,葉子節點)的結構都具備一緻行為;其 UML 類圖如下所示:
組合模式

透明組合模式 把所有公共方法都定義在 Component 中,這樣做的好處是用戶端無需分辨是葉子節點(Leaf)和樹枝節點(Composite),它們具備完全一緻的接口;缺點是葉子節點(Leaf)會繼承得到一些它所不需要(管理子類操作的方法)的方法,這與設計模式接口隔離原則相違背。

透明組合模式 通用代碼如下所示:

public class Client {
    public static void main(String[] args) {
        // 來一個根節點
        Component root = new Composite("root");
        // 來一個樹枝節點
        Component branchA = new Composite("---branchA");
        Component branchB = new Composite("------branchB");
        // 來一個葉子節點
        Component leafA = new Leaf("------leafA");
        Component leafB = new Leaf("---------leafB");
        Component leafC = new Leaf("---leafC");

        root.addChild(branchA);
        root.addChild(leafC);
        branchA.addChild(leafA);
        branchA.addChild(branchB);
        branchB.addChild(leafB);

        String result = root.operation();
        System.out.println(result);}
}      
// 抽象根節點
public abstract  class Component {
    protected String name;

    public Component(String name) {
        this.name = name;
    }

    public abstract String operation();

    public boolean addChild(Component component) {
        throw new UnsupportedOperationException("addChild not supported!");
    }

    public boolean removeChild(Component component) {
        throw new UnsupportedOperationException("removeChild not supported!");
    }

    public Component getChild(int index) {
        throw new UnsupportedOperationException("getChild not supported!");
    }
}      
// 樹節點
public class Composite  extends Component{
    private List<Component> mComponents;

    public Composite(String name) {
        super(name);
        this.mComponents = new ArrayList<>();
    }

    @Override
    public String operation() {
        StringBuilder builder = new StringBuilder(this.name);
        for (Component component : this.mComponents) {

            builder.append("\n");
            builder.append(component.operation());
        }
        return builder.toString();
    }
    @Override
    public boolean addChild(Component component) {
        return this.mComponents.add(component);
    }

    @Override
    public boolean removeChild(Component component) {
        return this.mComponents.remove(component);
    }

    @Override
    public Component getChild(int index) {
        return this.mComponents.get(index);
    }
}      
//葉子節點
public class Leaf extends Component {

    public Leaf(String name) {
        super(name);
    }

    @Override
    public String operation() {
        return this.name;
    }

}      

透明組合模式 中,由于 Component 包含葉子節點所不需要的方法,違背了最少知道原則,是以,我們直接将這些方法預設抛出

UnsupportedOperationException

異常。

  1. 安全模式:統一行為(Component)隻規定系統各個層次的最基礎的一緻行為,而把組合(樹節點)本身的方法(管理子類對象的添加,删除等)放到自身當中;其 UML 類圖如下所示:
組合模式

安全組合模式 把系統各層次公有的行為定義在 Component 中,把組合(樹節點)特有的行為(管理子類增加,删除等)放到自身(Composite)中。這樣做的好處是接口定義職責清晰,符合設計模式單一職責原則 和 接口隔離原則;缺點是客戶需要區分樹枝節點(Composite)和葉子節點(Leaf),這樣才能正确處理各個層次的操作,用戶端無法依賴抽象(Component),違背了設計模式依賴倒置原則。

 安全組合模式 的通用代碼相對 透明組合模式 而言,需要進行如下修改:

  • 修改 Component 代碼:隻保留各層次公有行為:
// 抽象根節點
public abstract  class Component {
    protected String name;

    public Component(String name) {
        this.name = name;
    }

    public abstract String operation();


 
}      
  • 修改用戶端代碼:将樹枝節點類型更改為 Composite 類型,以便擷取管理子類操作的方法:
public class Client {
    public static void main(String[] args) {
        // 來一個根節點
        Composite root = new Composite("root");
        // 來一個樹枝節點
        Composite branchA = new Composite("---branchA");
        Composite branchB = new Composite("------branchB");
        // 來一個葉子節點
        Component leafA = new Leaf("------leafA");
        Component leafB = new Leaf("---------leafB");
        Component leafC = new Leaf("---leafC");

        root.addChild(branchA);
        root.addChild(leafC);
        branchA.addChild(leafA);
        branchA.addChild(branchB);
        branchB.addChild(leafB);

        String result = root.operation();
        System.out.println(result);}
}      

運作結果顯示:root 包含 branchA 和 leafC;branchA 包含 leafA 和 branchB;branchB 包含 leafB。

三、組合模式在源碼中應用

組合模式在源碼中HashMap中也有應用,他裡面有一個putAll()方法

public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }      
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }      

從源碼中可以看到putAll()方法傳入的是Map對象,Map就是一個抽象構件(同時這個構件中隻支援鍵值對的存儲格式),而HashMap是一個中間構件,HashMap中的Node節點就是一個葉子節點,說到中間構件就會有規定的存儲方式。HashMap中的存儲方式是一個靜态内部類的數組Node<K,V>[] tab,其源碼如下:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }      
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }      

四、總結

透明組合模式将公共接口封裝到抽象根節點(Component) 中, 那麼系統所有節點就具備一緻行為,是以如果當系統絕大多數層次具備相同的公共行為時,采用透明組合模式也許會更好(代價:為剩下少數層次節點引入不需要的方法);而如果當系統各個層次差異性行為較多或者樹節點層次相對穩定(健壯)時,采用安全組合模式。

優點:

  • 清楚地定義分層次的複雜對象,表示對象的全部或部分層次
  • 讓用戶端忽略了層次的差異,友善對整個層次結構進行控制
  • 簡化用戶端代碼

缺點:

  • 限制類型時會較為複雜
  • 使設計變得更加抽象

注:設計模式的出現并不是說我們要寫的代碼一定要埋設計模式所要求的方方面畫,這是不現實同時也是不可能的。設計模式的出現,其實隻是強調好的代碼所具備的一些特征(六大設計原則),這些特征對于項目開發是具備積極效應的,但不是說我們每實作一個類就一定要全部滿足設計模式的要求,如果真的存在完全滿足設計模式的要求,反而可能存在過度設計的嫌疑。同時,23種設計模式,其實都是嚴格依循設計模式六大原則進行設計,隻是不同的模式在不同的場景中會更加造用。設計模式的了解應該重于意而不是形,真正編碼時,經常使用的是某種設計模式的變形體,真正切合項目的模式才是正确的模式.

git源碼:https://gitee.com/TongHuaShuShuoWoDeJieJu/design_pattern.git

這短短的一生我們最終都會失去,不妨大膽一點,愛一個人,攀一座山,追一個夢