1. 程式人生 > >java/android 設計模式學習筆記(12)---組合模式

java/android 設計模式學習筆記(12)---組合模式

  這篇我們來介紹一下組合模式(Composite Pattern),它也稱為部分整體模式(Part-Whole Pattern),結構型模式之一。組合模式比較簡單,它將一組相似的物件看作一個物件處理,並根據一個樹狀結構來組合物件,然後提供一個統一的方法去訪問相應的物件,以此忽略掉物件與物件集合之間的差別。這個最典型的例子就是資料結構中的樹了,如果一個節點有子節點,那麼它就是枝幹節點,如果沒有子節點,那麼它就是葉子節點,那麼怎麼把枝幹節點和葉子節點統一當作一種物件處理呢?這就需要用到組合模式了。
  這裡寫圖片描述
  轉載請註明出處:http://blog.csdn.net/self_study/article/details/51761709


  PS:對技術感興趣的同鞋加群544645972一起交流。

設計模式總目錄

特點

  組合模式允許你將物件組合成樹形結構來表現“整體/部分”層次結構,並且能夠讓客戶端以一致的方式處理個別對象以及組合物件。
  組合模式讓我們能用樹形方式建立物件的結構,樹裡面包含了組合構件以及葉子構件的物件,而且能夠把相同的操作應用在組合構件和葉子構件上,換句話說,在大多數情況下我們可以忽略組合物件和葉子物件之間的差別。組合模式使用的場景:

  • 表示物件的部分-整體結構層次時;
  • 從一個整體中能夠獨立出部分模組或功能的場景。

UML類圖

  組合模式在實際使用中會有兩種情況:安全的組合模式與透明的組合模式。

安全的組合模式

  我們先來看看安全組合模式的 uml 類圖:
  這裡寫圖片描述
可以看到組合模式有 4 個角色:

  • Component:抽象根節點,為組合中的物件宣告介面行為,是所有節點的抽象。在適當的情況下,實現所有類共有介面的預設行為。宣告一個介面用於訪問和管理 Component 的子節點。可在遞迴結構中定義一個介面,用於訪問一個父節點,並在合適的情況下實現它;
  • Composite:增加定義枝幹節點的行為,儲存子節點,實現 Component 介面中的有關的操作;
  • Leaf:在組合中表示葉子結點物件,葉子節點沒有子節點,實現 Component 介面中的全部操作;
  • Client:通過 Component,Composite 和 Leaf 類操縱組合節點物件。
  據此我們可以寫出安全組合模式的通用程式碼:
Component.class
public abstract class Component {
    public abstract void operation();
}

Composite.class

public class Composite extends Component{

    private ArrayList<Component> componentList = new ArrayList<>();

    @Override
    public void operation() {
        Log.e("shawn", "this is composite " + this + " -------start");
        for (Component component : componentList) {
            component.operation();
        }
        Log.e("shawn", "this is composite " + this + " -------end");
    }

    public void add(Component child) {
        componentList.add(child);
    }

    public void remove(Component child) {
        componentList.remove(child);
    }

    public Component getChild(int position) {
        return componentList.get(position);
    }
}

Leaf.class

public class Leaf extends Component{
    @Override
    public void operation() {
        Log.e("shawn", "this if leaf " + this);
    }
}

Client 測試程式碼:

Composite root = new Composite();

Leaf leaf1 = new Leaf();
Composite branch = new Composite();
root.add(leaf1);
root.add(branch);

Leaf leaf2 = new Leaf();
branch.add(leaf2);

root.operation();
break;

最後輸出結果:

com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@a37f4d8 -------start
com.android.compositepattern E/shawn: this if leaf com.android.compositepattern.composite.Leaf@1d7d4031
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@ec97316 -------start
com.android.compositepattern E/shawn: this if leaf com.android.compositepattern.composite.Leaf@5dae497
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@ec97316 -------end
com.android.compositepattern E/shawn: this is composite com.android.compositepattern.composite.Composite@a37f4d8 -------end

程式碼很簡單,結果就是一個簡單的樹形結構,但是仔細看看客戶端程式碼,就能發現它違反了 6 個設計模式原則中依賴倒置原則,客戶端不應該直接依賴於具體實現,而應該依賴於抽象,既然是面向介面程式設計,就應該把更多的焦點放在介面的設計上,於是這樣就產生了透明的組合模式。

透明的組合模式

  來看看透明的組合模式 uml 類圖:
  這裡寫圖片描述
和安全的組合模式差異就是在將 Composite 的操作放到了 Component 中,這就造成 Leaf 角色也要實現 Component 中的所有方法。實現的程式碼做出相應改變:
Component.class

public interface Component {
    void operation();

    void add(Component child);

    void remove(Component child);

    Component getChild(int position);
}

Composite.class

public class Composite implements Component{

    private ArrayList<Component> componentList = new ArrayList<>();

    @Override
    public void operation() {
        Log.e("shawn", "this is composite " + this + " -------start");
        for (Component component : componentList) {
            component.operation();
        }
        Log.e("shawn", "this is composite " + this + " -------end");
    }

    @Override
    public void add(Component child) {
        componentList.add(child);
    }

    @Override
    public void remove(Component child) {
        componentList.remove(child);
    }

    @Override
    public Component getChild(int position) {
        return componentList.get(position);
    }
}

Leaf.class

public class Leaf implements Component {
    @Override
    public void operation() {
        Log.e("shawn", "this if leaf " + this);
    }

    @Override
    public void add(Component child) {
        throw new UnsupportedOperationException("leaf can't add child");
    }

    @Override
    public void remove(Component child) {
        throw new UnsupportedOperationException("leaf can't remove child");
    }

    @Override
    public Component getChild(int position) {
        throw new UnsupportedOperationException("leaf doesn't have any child");
    }
}

Client 測試程式碼

Component root = new Composite();

Component leaf1 = new Leaf();
Component branch = new Composite();
root.add(leaf1);
root.add(branch);

Component leaf2 = new Leaf();
branch.add(leaf2);

root.operation();

最後產生的結果是一樣的,由於是在 Component 類中定義了所有的行為,所以客戶端就不用直接依賴於具體 Composite 和 Leaf 類的實現,遵循了依賴倒置原則——依賴抽象,而不依賴具體實現。但是也違反了單一職責原則介面隔離原則,讓 Leaf 類繼承了它本不應該有的方法,並且不太優雅的丟擲了 UnsupportedOperationException ,這樣做的目的就是為了客戶端可以透明的去呼叫對應元件的方法,將枝幹節點和子節點一視同仁。
  另外,將 Component 寫成一個虛基類,並且實現所有的 Composite 方法,而且預設都丟擲異常,只讓 Composite 去覆蓋重寫父類的方法,而 Leaf 類就不需要去實現 Composite 的相關方法,這麼去實現當然也是可以的。

對比

  安全的組合模式將責任區分開來放在不同的介面中,這樣一來,設計上就比較安全,也遵循了單一職責原則介面隔離原則,但是也讓客戶端必須依賴於具體的實現;透明的組合模式,以單一職責原則介面隔離原則原則換取透明性,遵循依賴倒置原則,客戶端就直接依賴於 Component 抽象即可,將 Composite 和 Leaf 一視同仁,也就是說,一個元素究竟是枝幹節點還是葉子節點,對客戶端是透明的。
  所以這是一個很典型的折衷案例,儘管我們受到設計原則的指導,但是我們總是需要觀察某原則對我們的設計所造成的影響。有時候這個需要去根據實際案例去分析,畢竟有些時候 6 種設計模式原則在實際使用過程中是會衝突的,是讓客戶端每次使用的時候都去先檢查型別還是賦予子節點不應該有的行為,這都取決於設計者的觀點,總體而言,這兩種方案都是可行的。

示例與原始碼

  組合模式在實際生活過程中的例子就數不勝數了,比如選單、資料夾等等。我們這就以 Android 中非常經典的實現為例來分析一下。View 和 ViewGroup 想必應該都非常熟悉,其實他們用到的就是組合模式,我們先來看看他們之間的 uml 類圖:
  這裡寫圖片描述
ViewManager 這個類在java/android 設計模式學習筆記(8)—橋接模式中提到過,WindowManager 也繼承了該類:

/** Interface to let you add and remove child views to an Activity. To get an instance
  * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
  */
public interface ViewManager
{
    /**
     * Assign the passed LayoutParams to the passed View and add the view to the window.
     * <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
     * errors, such as adding a second view to a window without removing the first view.
     * <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
     * secondary {@link Display} and the specified display can't be found
     * (see {@link android.app.Presentation}).
     * @param view The view to be added to this window.
     * @param params The LayoutParams to assign to view.
     */
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

只定義了關於 View 操作的三個方法。ViewParent 類是用來定義一個 父 View 角色所具有的職責,在 Android 中,一般能成為父 View 的也只有 ViewGroup:

/**
 * Defines the responsibilities for a class that will be a parent of a View.
 * This is the API that a view sees when it wants to interact with its parent.
 * 
 */
public interface ViewParent {
    /**
     * Called when something has changed which has invalidated the layout of a
     * child of this view parent. This will schedule a layout pass of the view
     * tree.
     */
    public void requestLayout();

    /**
     * Indicates whether layout was requested on this view parent.
     *
     * @return true if layout was requested, false otherwise
     */
    public boolean isLayoutRequested();
   ....
}

從 uml 類圖中可以注意到一點,ViewGroup 和 View 使用的安全的組合模式,而不是透明的組合模式,怪不得有時候使用前需要將 View 強轉成 ViewGroup 。

總結

  使用組合模式,我們能把相同的操作應用在組合和個別物件上,換句話說,在大多數情況下,我們可以忽略物件組合和個別物件之間的差別。組合模式適用於一些介面 UI 的結構設計上,典型的例子就是Android,iOS 和 Java 等都提供了相應的 UI 框架。
  組合模式的優點:

  • 組合模式可以清楚地定義分層次的複雜物件,表示物件的全部或部分層次,它讓高層模組忽略了層次的差異,方便對整個層次結構進行控制;
  • 高層模組可以一致地使用一個組合結構或其中單個物件,不必關心處理的是單個物件還是組合結構,簡化了高層模組的程式碼。
  • 在組合模式中增加新的枝幹構件和葉子構件都很方便,無需對現有類庫進行任何修改,符合“開閉原則”;
  • 組合模式為樹形結構的面向物件實現提供了一種靈活的解決方案,通過葉子物件和枝幹物件的遞迴組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單。
  組合模式的缺點:在新增構件時不好對枝幹中的構建型別進行限制,不能依賴型別系統來施加這些約束,因為在大多數情況下,他們都來自於相同的抽象層,此時,必須進行型別檢查來實現,這個實現過程較為複雜。

原始碼下載

引用