說明:設計模式系列文章是讀劉偉
所著《設計模式的藝術之道(軟體開發人員內功修煉之道)》
一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的部落格https://blog.csdn.net/LoveLion/article/details/17517213
模式概述
樹形結構
在軟體中隨處可見,例如作業系統中的目錄結構、應用軟體中的選單、辦公系統中的公司組織結構等等,如何運用物件導向的方式來處理這種樹形結構是組合模式
需要解決的問題。組合模式通過一種巧妙的設計方案使得使用者可以一致性地處理整個樹形結構或者樹形結構的一部分,也可以一致性地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點)。
模式定義
組合模式(Composite Pattern)
:組合多個物件形成樹形結構以表示具有“整體—部分”關係的層次結構。組合模式對單個物件(即葉子物件)和組合物件(即容器物件)的使用具有一致性,組合模式又可以稱為“整體—部分”(Part-Whole)模式,它是一種物件結構型模式。
模式結構圖
組合模式結構圖如下所示:
在組合模式結構圖中包含如下幾個角色:
-
Component
(抽象構件):它可以是介面或抽象類,為葉子構件和容器構件物件宣告介面,在該角色中可以包含所有子類共有行為的宣告和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增加子構件、刪除子構件、獲取子構件等。 -
Leaf
(葉子構件):它在組合結構中表示葉子節點物件,葉子節點沒有子節點,它實現了在抽象構件中定義的行為。對於那些訪問及管理子構件的方法,可以通過異常等方式進行處理。 -
Composite
(容器構件):它在組合結構中表示容器節點物件,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用於儲存子節點,實現了在抽象構件中定義的行為,包括那些訪問及管理子構件的方法,在其業務方法中可以遞迴呼叫其子節點的業務方法。
組合模式的關鍵是定義了一個
抽象構件類
,它既可以代表葉子,又可以代表容器,而客戶端針對該抽象構件類進行程式設計,無須知道它到底表示的是葉子還是容器,可以對其進行統一處理。同時容器物件與抽象構件類之間還建立一個聚合關聯關係,在容器物件中既可以包含葉子,也可以包含容器,以此實現遞迴組合,形成一個樹形結構。
模式虛擬碼
對於客戶端而言,一般針對抽象構件程式設計,而無須關心其具體子類是容器構件還是葉子構件。抽象構建類典型程式碼如下:
public abstract class Component {
public abstract void add(Component c); //增加成員
public abstract void remove(Component c); //刪除成員
public abstract Component getChild(int i); //獲取成員
public abstract void operation(); //業務方法
}
如果繼承抽象構件的是葉子構件,則其典型程式碼如下所示:
public class Leaf extends Component {
@Override
public void add(Component c) {
//異常處理或錯誤提示
}
@Override
public void remove(Component c) {
//異常處理或錯誤提示
}
@Override
public Component getChild(int i) {
//異常處理或錯誤提示
return null;
}
@Override
public void operation() {
//葉子構件具體業務方法的實現
}
}
如果繼承抽象構件的是容器構件,則其典型程式碼如下所示:
public class Composite extends Component {
private List<Component> list = new ArrayList<>();
@Override
public void add(Component c) {
list.add(c);
}
@Override
public void remove(Component c) {
list.remove(c);
}
@Override
public Component getChild(int i) {
return (Component) list.get(i);
}
@Override
public void operation() {
//容器構件具體業務方法的實現
//遞迴呼叫成員構件的業務方法
for (Object obj : list) {
((Component) obj).operation();
}
}
}
客戶端對抽象構件類進行程式設計
public class Client {
public static void main(String[] args) {
Component component;
component = new Leaf();
//component = new Composite();
// 無須知道到底是葉子還是容器
// 可以對其進行統一處理
component.operation();
}
}
模式簡化
透明組合模式
透明組合模式中,抽象構件Component中宣告瞭所有用於管理成員物件的方法,包括add()、remove()以及getChild()等方法,這樣做的好處是確保所有的構件類都有相同的介面。在客戶端看來,葉子物件與容器物件所提供的方法是一致的,客戶端可以相同地對待所有的物件。透明組合模式也是組合模式的標準形式。
透明組合模式的完整結構圖如下:
也可以將葉子構件的add()
、remove()
等方法的實現程式碼移至Component
中,由Component
提供統一的預設實現,這樣子類就不必強制去實現管理子Component。程式碼如下所示:
public abstract class Component {
public void add(Component c) {
throw new RuntimeException("不支援的操作");
}
public void remove(Component c) {
throw new RuntimeException("不支援的操作");
}
public Component getChild(int i) {
throw new RuntimeException("不支援的操作");
}
public abstract void operation(); //業務方法
}
透明組合模式
的缺點是不夠安全,因為葉子物件和容器物件在本質上是有區別的。葉子物件不可能有下一個層次的物件,即不可能包含成員物件,因此為其提供add()、remove()以及getChild()等方法是沒有意義的,這在編譯階段不會出錯,但在執行階段如果呼叫這些方法可能會出錯(如果沒有提供相應的錯誤處理程式碼)。
安全組合模式
安全組合模式中,在抽象構件Component中沒有宣告任何用於管理成員物件的方法,而是在Composite類中宣告並實現這些方法。
安全組合模式的完整結構圖如下:
此時Component
就應該這樣定義了
public abstract class Component {
// 業務方法
public abstract void operation();
}
安全組合模式
的缺點是不夠透明,因為葉子構件和容器構件具有不同的方法,且容器構件中那些用於管理成員物件的方法沒有在抽象構件類中定義,因此客戶端不能完全針對抽象程式設計,必須有區別地對待葉子構件和容器構件。在實際應用中,安全組合模式的使用頻率也非常高,在Java AWT中
使用的組合模式就是安全組合模式。
模式應用
模式在JDK中的應用
Java SE
中的AWT
和Swing
包的設計就基於組合模式,在這些介面包中為使用者提供了大量的容器構件(如Container
)和成員構件(如Checkbox
、Button
和TextComponent
等),其結構如下圖所示
Component
類是抽象構件,Checkbox
、Button
和TextComponent
是葉子構件,而Container
是容器構件,在AWT
中包含的葉子構件還有很多。在一個容器構件中可以包含葉子構件,也可以繼續包含容器構件,這些葉子構件和容器構件一起組成了複雜的GUI
介面。除此以外,在XML解析
、組織結構樹處理
、檔案系統設計
等領域,組合模式都得到了廣泛應用。
模式在開源專案中的應用
Spring
中org.springframework.web.method.support.HandlerMethodArgumentResolver
使用了安全組合模式。提取關鍵程式碼如下:
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
再看下它的一個實現類org.springframework.web.method.support.HandlerMethodArgumentResolverComposite
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
/**
* Add the given {@link HandlerMethodArgumentResolver}.
*/
public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) {
this.argumentResolvers.add(resolver);
return this;
}
/**
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
*/
public HandlerMethodArgumentResolverComposite addResolvers(
@Nullable HandlerMethodArgumentResolver... resolvers) {
if (resolvers != null) {
Collections.addAll(this.argumentResolvers, resolvers);
}
return this;
}
/**
* Clear the list of configured resolvers.
*/
public void clear() {
this.argumentResolvers.clear();
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
模式總結
主要優點
-
組合模式可以清楚地定義分層次的複雜物件,表示物件的全部或部分層次,它讓客戶端忽略了層次的差異,方便對整個層次結構進行控制。
-
客戶端可以一致地使用一個組合結構或其中單個物件,不必關心處理的是單個物件還是整個組合結構,簡化了客戶端程式碼。
-
組合模式為樹形結構的物件導向實現提供了一種靈活的解決方案,通過葉子物件和容器物件的遞迴組合,可以形成複雜的樹形結構,但對樹形結構的控制卻非常簡單。
適用場景
(1) 在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待它們。
(2) 在一個使用面嚮物件語言開發的系統中需要處理一個樹形結構。
(3) 在一個系統中能夠分離出葉子物件和容器物件,而且它們的型別不固定,需要增加一些新的型別。