概述
組合模式有時候又叫做部分-整體模式,它使我們樹型結構的問題中,模糊了簡單元素和復雜元素的概念,客戶程序可以向處理簡單元素一樣來處理復雜元素,從而使得客戶程序與復雜元素的內部結構解耦。
意圖
將對象組合成樹形結構以表示“部分-整體”的層次結構。Composite模式使得用戶對單個對象和組合對象的使用具有一致性。[GOF 《設計模式》]
結構圖
圖1 Composite模式結構圖
生活中的例子
組合模式將對象組合成樹形結構以表示"部分-整體"的層次結構。讓用戶一致地使用單個對象和組合對象。雖然例子抽象一些,但是算術表達式確實是組合的例子。算術表達式包括操作數、操作符和另一個操作數。操作數可以是數字,也可以是另一個表達式。這樣,2+3和(2+3)+(4*6)都是合法的表達式。
圖2 使用算術表達式例子的Composite模式對象圖
組合模式解說
這裡我們用繪圖這個例子來說明Composite模式,通過一些基本圖像元素(直線、圓等)以及一些復合圖像元素(由基本圖像元素組合而成)構建復雜的圖形樹。在設計中我們對每一個對象都配備一個Draw()方法,在調用時,會顯示相關的圖形。可以看到,這裡復合圖像元素它在充當對象的同時,又是那些基本圖像元素的一個容器。先看一下基本的類結構圖:
圖3
圖中橙色的區域表示的是復合圖像元素。示意性代碼:
public abstract class Graphics
{
protected string _name;
public Graphics(string name)
{
this._name = name;
}
public abstract void Draw();
}
public class Picture : Graphics
{
public Picture(string name)
: base(name)
{ }
public override void Draw()
{
//
}
public ArrayList GetChilds()
{
//返回所有的子對象
}
}
而其他作為樹枝構件,實現代碼如下:
public class Line:Graphics
{
public Line(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
}
public class Circle : Graphics
{
public Circle(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
}
public class Rectangle : Graphics
{
public Rectangle(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
}
現在我們要對該圖像元素進行處理:在客戶端程序中,需要判斷返回對象的具體類型到底是基本圖像元素,還是復合圖像元素。如果是復合圖像元素,我們將要用遞歸去處理,然而這種處理的結果卻增加了客戶端程序與復雜圖像元素內部結構之間的依賴,那麼我們如何去解耦這種關系呢?我們希望的是客戶程序可以像處理基本圖像元素一樣來處理復合圖像元素,這就要引入Composite模式了,需要把對於子對象的管理工作交給復合圖像元素,為了進行子對象的管理,它必須提供必要的Add(),Remove()等方法,類結構圖如下:
圖4
示意性代碼:
public abstract class Graphics
{
protected string _name;
public Graphics(string name)
{
this._name = name;
}
public abstract void Draw();
public abstract void Add();
public abstract void Remove();
}
public class Picture : Graphics
{
protected ArrayList picList = new ArrayList();
public Picture(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
foreach (Graphics g in picList)
{
g.Draw();
}
}
public override void Add(Graphics g)
{
picList.Add(g);
}
public override void Remove(Graphics g)
{
picList.Remove(g);
}
}
public class Line : Graphics
{
public Line(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
public override void Add(Graphics g)
{ }
public override void Remove(Graphics g)
{ }
}
public class Circle : Graphics
{
public Circle(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
public override void Add(Graphics g)
{ }
public override void Remove(Graphics g)
{ }
}
public class Rectangle : Graphics
{
public Rectangle(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
public override void Add(Graphics g)
{ }
public override void Remove(Graphics g)
{ }
}
這樣引入Composite模式後,客戶端程序不再依賴於復合圖像元素的內部實現了。然而,我們程序中仍然存在著問題,因為Line,Rectangle,Circle已經沒有了子對象,它是一個基本圖像元素,因此Add(),Remove()的方法對於它來說沒有任何意義,而且把這種錯誤不會在編譯的時候報錯,把錯誤放在了運行期,我們希望能夠捕獲到這類錯誤,並加以處理,稍微改進一下我們的程序:
public class Line : Graphics
{
public Line(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
public override void Add(Graphics g)
{
//拋出一個我們自定義的異常
}
public override void Remove(Graphics g)
{
//拋出一個我們自定義的異常
}
}
這樣改進以後,我們可以捕獲可能出現的錯誤,做進一步的處理。上面的這種實現方法屬於透明式的Composite模式,如果我們想要更安全的一種做法,就需要把管理子對象的方法聲明在樹枝構件Picture類裡面,這樣如果葉子節點Line,Rectangle,Circle使用這些方法時,在編譯期就會出錯,看一下類結構圖:
圖5
示意性代碼:
public abstract class Graphics
{
protected string _name;
public Graphics(string name)
{
this._name = name;
}
public abstract void Draw();
}
public class Picture : Graphics
{
protected ArrayList picList = new ArrayList();
public Picture(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
foreach (Graphics g in picList)
{
g.Draw();
}
}
public void Add(Graphics g)
{
picList.Add(g);
}
public void Remove(Graphics g)
{
picList.Remove(g);
}
}
public class Line : Graphics
{
public Line(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
}
public class Circle : Graphics
{
public Circle(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
}
public class Rectangle : Graphics
{
public Rectangle(string name)
: base(name)
{ }
public override void Draw()
{
Console.WriteLine("Draw a" + _name.ToString());
}
}
這種方式屬於安全式的Composite模式,在這種方式下,雖然避免了前面所討論的錯誤,但是它也使得葉子節點和樹枝構件具有不一樣的接口。這種方式和透明式的Composite各有優劣,具體使用哪一個,需要根據問題的實際情況而定。通過Composite模式,客戶程序在調用Draw()的時候不用再去判斷復雜圖像元素中的子對象到底是基本圖像元素,還是復雜圖像元素,看一下簡單的客戶端調用:
public class App
{
public static void Main()
{
Picture root = new Picture("Root");
root.Add(new Line("Line"));
root.Add(new Circle("Circle"));
Rectangle r = new Rectangle("Rectangle");
root.Add(r);
root.Draw();
}
}
.NET中的組合模式
如果有人用過Enterprise Library2.0,一定在源程序中看到了一個叫做ObjectBuilder的程序集,顧名思義,它是用來負責對象的創建工作的,而在ObjectBuilder中,有一個被稱為定位器的東西,通過定位器,可以很容易的找到對象,它的結構采用鏈表結構,每一個節點是一個鍵值對,用來標識對象的唯一性,使得對象不會被重復創建。定位器的鏈表結構采用可枚舉的接口類來實現,這樣我們可以通過一個迭代器來遍歷這個鏈表。同時多個定位器也被串成一個鏈表。具體地說就是多個定位器組成一個鏈表,表中的每一個節點是一個定位器,定位器本身又是一個鏈表,表中保存著多個由鍵值對組成的對象的節點。所以這是一個典型的Composite模式的例子,來看它的結構圖:
圖6
正如我們在圖中所看到的,IReadableLocator定義了最上層的定位器接口方法,它基本上具備了定位器的大部分功能。
部分代碼:
public interface IReadableLocator : IEnumerable<KeyValuePair<object, object>>
{
//返回定位器中節點的數量
int Count { get; }
//一個指向父節點的引用
IReadableLocator ParentLocator { get; }
//表示定位器是否只讀
bool ReadOnly { get; }
//查詢定位器中是否已經存在指定鍵值的對象
bool Contains(object key);
//查詢定位器中是否已經存在指定鍵值的對象,根據給出的搜索選項,表示是否要向上回溯繼續尋找。
bool Contains(object key, SearchMode options);
//使用謂詞操作來查找包含給定對象的定位器
IReadableLocator FindBy(Predicate<KeyValuePair<object, object>> predicate);
//根據是否回溯的選項,使用謂詞操作來查找包含對象的定位器
IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate);
//從定位器中獲取一個指定類型的對象
TItem Get<TItem>();
//從定位其中獲取一個指定鍵值的對象
TItem Get<TItem>(object key);
//根據選項條件,從定位其中獲取一個指定類型的對象
TItem Get<TItem>(object key, SearchMode options);
//給定對象鍵值獲取對象的非泛型重載方法
object Get(object key);
//給定對象鍵值帶搜索條件的非泛型重載方法
object Get(object key, SearchMode options);
}
一個抽象基類ReadableLocator用來實現這個接口的公共方法。兩個主要的方法實現代碼如下:
public abstract class ReadableLocator : IReadableLocator
{
/**//// <summary>
/// 查找定位器,最後返回一個只讀定位器的實例
/// </summary>
public IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate)
{
if (predicate == null)
throw new ArgumentNullException("predicate");
if (!Enum.IsDefined(typeof(SearchMode), options))
throw new ArgumentException(Properties.Resources.InvalidEnumerationValue, "options");
Locator results = new Locator();
IReadableLocator currentLocator = this;
while (currentLocator != null)
{
FindInLocator(predicate, results, currentLocator);
currentLocator = options == SearchMode.Local ? null : currentLocator.ParentLocator;
}
return new ReadOnlyLocator(results);
}
/**//// <summary>
/// 遍歷定位器
/// </summary>
private void FindInLocator(Predicate<KeyValuePair<object, object>> predicate, Locator results,
IReadableLocator currentLocator)
{
foreach (KeyValuePair<object, object> kvp in currentLocator)
{
if (!results.Contains(kvp.Key) && predicate(kvp))
{
results.Add(kvp.Key, kvp.Value);
}
}
}
}
可以看到,在FindBy方法裡面,循環調用了FindInLocator方法,如果查詢選項是只查找當前定位器,那麼循環終止,否則沿著定位器的父定位器繼續向上查找。FindInLocator方法就是遍歷定位器,然後把找到的對象存入一個臨時的定位器。最後返回一個只讀定位器的新的實例。
從這個抽象基類中派生出一個具體類和一個抽象類,一個具體類是只讀定位器(ReadOnlyLocator),只讀定位器實現抽象基類沒有實現的方法,它封裝了一個實現了IReadableLocator接口的定位器,然後屏蔽內部定位器的寫入接口方法。另一個繼承的是讀寫定位器抽象類ReadWriteLocator,為了實現對定位器的寫入和刪除,這裡定義了一個對IReadableLocator接口擴展的接口叫做IReadWriteLocator,在這個接口裡面提供了實現定位器的操作:
圖7
實現代碼如下:
public interface IReadWriteLocator : IReadableLocator
{
//保存對象到定位器
void Add(object key, object value);
//從定位器中刪除一個對象,如果成功返回真,否則返回假
bool Remove(object key);
}
從ReadWirteLocator派生的具體類是Locator類,Locator類必須實現一個定位器的全部功能,現在我們所看到的Locator它已經具有了管理定位器的功能,同時他還應該具有存儲的結構,這個結構是通過一個WeakRefDictionary類來實現的,這裡就不介紹了。[關於定位器的介紹參考了niwalker的Blog]
效果及實現要點
1.Composite模式采用樹形結構來實現普遍存在的對象容器,從而將“一對多”的關系轉化“一對一”的關系,使得客戶代碼可以一致地處理對象和對象容器,無需關心處理的是單個的對象,還是組合的對象容器。
2.將“客戶代碼與復雜的對象容器結構”解耦是Composite模式的核心思想,解耦之後,客戶代碼將與純粹的抽象接口——而非對象容器的復內部實現結構——發生依賴關系,從而更能“應對變化”。
3.Composite模式中,是將“Add和Remove等和對象容器相關的方法”定義在“表示抽象對象的Component類”中,還是將其定義在“表示對象容器的Composite類”中,是一個關乎“透明性”和“安全性”的兩難問題,需要仔細權衡。這裡有可能違背面向對象的“單一職責原則”,但是對於這種特殊結構,這又是必須付出的代價。ASP.NET控件的實現在這方面為我們提供了一個很好的示范。
4.Composite模式在具體實現中,可以讓父對象中的子對象反向追溯;如果父對象有頻繁的遍歷需求,可使用緩存技巧來改善效率。
適用性
以下情況下適用Composite模式:
1.你想表示對象的部分-整體層次結構
2.你希望用戶忽略組合對象與單個對象的不同,用戶將統一地使用組合結構中的所有對象。
總結
組合模式解耦了客戶程序與復雜元素內部結構,從而使客戶程序可以向處理簡單元素一樣來處理復雜元素。