.Net中的設計模式——Composite模式
一、模式概述
描述Composite模式的最佳方式莫過于樹形圖。從抽象類或接口為根節(jié)點開始,然后生枝發(fā)芽,以形成樹枝節(jié)點和葉結(jié)點。因此,Composite模式通常用來描述部分與整體之間的關系,而通過根節(jié)點對該結(jié)構(gòu)的抽象,使得客戶端可以將單元素節(jié)點與復合元素節(jié)點作為相同的對象來看待。
由于Composite模式模糊了單元素和復合元素的區(qū)別,就使得我們?yōu)檫@些元素提供相關的操作時,可以有一個統(tǒng)一的接口。例如,我們要編寫一個字處理軟件,該軟件能夠處理文字,對文章進行排版、預覽、打印等功能。那么,這個工具需要處理的對象,就應該包括單個的文字、以及由文字組成的段落,乃至整篇文檔。這些對象從軟件處理的角度來看,對外的接口應該是一致的,例如改變文字的字體,改變文字的位置使其居中或者右對齊,也可以顯示對象的內(nèi)容,或者打印。而從內(nèi)部實現(xiàn)來看,我們對段落或者文檔進行操作,實質(zhì)上也是對文字進行操作。從結(jié)構(gòu)來看,段落包含了文字,文檔又包含了段落,是一個典型的樹形結(jié)構(gòu)。而其根節(jié)點正是我們可以抽象出來暴露在外的統(tǒng)一接口,例如接口IElement:
既然文字、段落、文檔都具有這些操作,因此它們都可以實現(xiàn)IElement接口:
從上圖可以看到,對象Word、Paragraph、Document均實現(xiàn)了IElement接口,但Paragraph和Document與Word對象不同的是,這兩者處除了實現(xiàn)了IElement接口,它們還與IElement接口對象之間具有聚合的關系,且是一對多。也就是說Paragraph與Document對象內(nèi)可以包含0個到多個IElement對象,這也是與前面對字處理軟件分析獲得的結(jié)果是一致的。
從整個結(jié)構(gòu)來看,完全符合樹形結(jié)構(gòu)的各個要素,接口IElement是根節(jié)點,而Paragraph和Document類為枝節(jié)點,Word對象為葉節(jié)點。既然作為枝節(jié)點,它就具有帶葉節(jié)點的能力,從上圖的聚合關系中我們體現(xiàn)出了這一點。也就是說,Paragraph和Document類除了具有排版、打印方面的職責外,還能夠添加、刪除葉節(jié)點的操作。那么這些操作應該放在哪里呢?
管理對子節(jié)點的管理,Composite模式提供了兩種方式:一個是透明方式,也就是說在根節(jié)點中聲明所有用來管理子元素的方法,包括Add()、Remove()等方法。這樣一來,實現(xiàn)根節(jié)點接口的子節(jié)點同時也具備了管理子元素的能力。這種實現(xiàn)策略,最大的好處就是完全消除了葉節(jié)點和枝節(jié)點對象在抽象層次的區(qū)別,它們具備完全一致的接口。而缺點則是不夠安全。由于葉節(jié)點本身不具備管理子元素的能力,因此提供的Add()、Remove()方法在實現(xiàn)層次是無意義的。但客戶端調(diào)用時,卻看不到這一點,從而導致在運行期間有出錯的可能。
另一種策略則是安全方式。與透明方式剛好相反,它只在枝節(jié)點對象里聲明管理子元素的方法,由于葉節(jié)點不具備這些方法,當客戶端在操作葉節(jié)點時,就不會出現(xiàn)前一種方式的安全錯誤。然而,這種實現(xiàn)方式,卻導致了葉節(jié)點和枝節(jié)點接口的不完全一致,這給客戶端操作時帶來了不便。
這兩種方式各有優(yōu)缺點,我們在實現(xiàn)時,應根據(jù)具體的情況,作出更加合理的抉擇。在字處理軟件一例中,我選擇了安全方式來實現(xiàn),因為對于客戶端而言,在調(diào)用IElement接口時,通常是將其視為可被排版、打印等操作的對象。至于為Paragraph和Document對象添加、刪除子對象,往往是一種初始化的行為,完全可以放到一個單獨的模塊中。根據(jù)單一職責原則(SRP),我們沒有必要讓IElement接口負累太重。所以,我們需要對上圖作稍許的修改,在Paragraph和Document對象中增加Add()和Remove()方法:
以下是IElement對象結(jié)構(gòu)的實現(xiàn)代碼:
public interface IElement
{
void ChangeFont(Font font);
void Show();
//其他方法略;
}
public class Word
{
public void ChangeFont(Font font)
{
this.font = font;
}
public void Show()
{
Console.WriteLine(this.ToString());
}
//其他方法略;
}
public class Paragraph
{
private ArrayList elements = new ArrayList();
public void Add(IElement element)
{
elements.Add(element);
}
public void Remove(IElement element)
{
elements.Remove(element);
}
public void ChangeFont(Font font)
{
foreach (IElement element in elements)
{
element.ChangeFont(font);
}
}
public void Show()
{
foreach (IElement element in elements)
{
element.Show(font);
}
}
//其他方法略;
}
//Document類略;
實際上,我們在為葉節(jié)點實現(xiàn)Add(),Remove()方法時,還需要考慮一些異常情況。例如在Paragraph類中,添加的子元素就不能是Document對象和Paragraph對象。所以在添加IElement對象時,還需要做一些條件判斷,以確定添加行為是否正確,如果錯誤,應拋出異常。
采用Composite模式,我們將Word、Paragraph、Document抽象為IElement接口。雖然各自內(nèi)部的實現(xiàn)并不相同,枝節(jié)點和葉節(jié)點的實質(zhì)也不一樣,但對于調(diào)用者而言,是沒有區(qū)別的。例如在類WordProcessor中,包含一個GetSelectedElement()靜態(tài)方法,它能夠獲得當前選擇的對象:
public class WordProcessor
{
public static IElement GetSelectedElement(){……}
}
對于字處理軟件的UI來說,如果要改變選中對象的字體,則可以在命令按鈕cmdChangeFont的Click事件中寫下如下代碼:
public void cmdChangeFont_Click(object sender, EventArgs e)
{
WordProcessor.GetSelectedElement().ChangeFont(currentFont);
}
不管當前選中的對象是文字、段落還是整篇文檔,對于UI而言,操作都是完全一致的,根本不需要去判斷對象的類別。因此,如果在Business Layer的類庫設計時,采用Composite模式,將極大地簡化UI表示層的開發(fā)工作。此外,應用該模式也較好的支持項目的可擴展性。例如,我們?yōu)镮Element接口增加了Sentence類,對于前面的例子而言,只需要修改GetSelectedElement()方法,而cmdChangeFont命令按鈕的Click事件以及Business Layer類庫原有的設計,都不需要做任何改變。這也符合OO的開放-封閉原則(OCP),即對于擴展是開放的(Open for extension),對于更改則是封閉的(Closed for modification)。
二、.Net Framework中的Composite模式
在.Net中,最能體現(xiàn)Composite模式的莫過于Windows或Web的控件。在這些控件中,有的包含子控件,有的則不包含且不能包含子控件,這正好符合葉節(jié)點和枝節(jié)點的含義。所有Web控件的基類為System.Web.UI.Contril類(如果是Windows控件,則基類為System.Windows.Forms.Control類)。其子類包含有HtmlControl、HtmlContainerControl等。按照Composite模式的結(jié)構(gòu),枝節(jié)點和葉節(jié)點屬于根節(jié)點的不同分支,同時枝節(jié)點與根節(jié)點之間應具備一個聚合關系,可以通過Add()、Remove()方法添加和移除其子節(jié)點。設定HtmlControl為葉節(jié)點,而HtmlContaiinerControl為枝節(jié)點,那么采用透明方式的設計方法,在.Net中控件類的結(jié)構(gòu),就應該如下圖所示:
雖然根據(jù)透明方式的Composite模式,HtmlControl類與其父類Control之間也應具備一個聚合關系,但實質(zhì)上該類并不具備管理子控件的職責,因此我在類圖中忽略了這個關系。此時,HtmlControl類中的Add()、Remove()方法,應該為空,或者拋出一個客戶端能夠捕獲的異常。
然而,從具體實現(xiàn)來考慮,由于HtmlControl類和HtmlContainerControl類在實現(xiàn)細節(jié)層次,區(qū)別僅在于前者不支持子控件,但從控件本身的功能來看,很多行為是相同或者相近的。例如HtmlControl類的Render()方法,調(diào)用了方法RenderBeginTag()方法:
protected override void Render(HtmlTextWriter writer)
{
this.RenderBeginTag(writer);
}
protected virtual void RenderBeginTag(HtmlTextWriter writer)
{
writer.WriteBeginTag(this.TagName);
this.RenderAttributes(writer);
writer.Write(’>‘);
}
而HtmlContainerControl類也具有Render()方法,在這個方法中也調(diào)用了RenderBeginTag()方法,且RenderBeginTag方法的實現(xiàn)和前者完全一致:
protected override void Render(HtmlTextWriter writer)
{
this.RenderBeginTag(writer);
this.RenderChildren(writer);
this.RenderEndTag(writer);
}
按照上面的結(jié)構(gòu),由于HtmlControl和HtmlContainerControl之間并無繼承關系,這就要求兩個類中,都要重復實現(xiàn)RenderBeginTag()方法,從而導致產(chǎn)生重復代碼。根據(jù)OO的特點,解決的辦法,就是讓HtmlContainerControl繼承自HtmlControl類(因為HtmlContainerControl的接口比HtmlControl寬,所以只能令HtmlContainerControl作為子類),并讓RenderBeginTag()方法成為HtmlControl類的protected方法,子類HtmlContainerControl可以直接調(diào)用這個方法。然而與之矛盾的是,HtmlContainerControl卻是一個可以包含子控件的枝節(jié)點,而HtmlControl則是不能包含子控件的葉節(jié)點,那么這樣的繼承關系還成立嗎?
HtmlControl類對Add()方法和Remove()方法的重寫后,這兩個方法內(nèi)容為空。由于HtmlContainerControl類繼承HtmlControl類,但我們又要求它的Add()和Remove()方法和Control類保持一致,而父類HtmlControl已經(jīng)重寫這兩個方法,此時是無法直接繼承來自父類的方法的。以上是采用透明方式的設計。
如果采用安全方式,仍然有問題。雖然在HtmlControl類中不再有Add()和Remove()方法,但由于Control類和HtmlContainerControl類都允許添加子控件,它們包含的Add()、Remove()方法,只能分別實現(xiàn)。這樣的設計必然會導致重復代碼。這也是與我們的期望不符的。
那么在.Net中,Control類究竟是怎樣實現(xiàn)的呢?下面,我將根據(jù).Net實現(xiàn)Control控件的源代碼,來分析Control控件的真實結(jié)構(gòu),以及其具體的實現(xiàn)細節(jié)。
三、深入分析.Net中的Composite模式
首先,我們來剖析Web控件的基類Control類的內(nèi)部實現(xiàn):
public class Control : IComponent, IDisposable, IParserAccessor, IDataBindingsAccessor
{
// Events;略
// Methods
public Control()
{
if (this is INamingContainer)
{
this.flags[0×80] = true;
}
}
public virtual bool HasControls()
{
if (this._controls != null)
{
return (this._controls.Count > 0);
}
return false;
}
public virtual void DataBind()
{
this.OnDataBinding(EventArgs.Empty);
if (this._controls != null)
{
string text1 = this._controls.SetCollectionReadOnly(”Parent_collections_readonly”);
int num1 = this._controls.Count;
for (int num2 = 0; num2 < num1; num2++)
{
this._controls[num2].DataBind();
}
this._controls.SetCollectionReadOnly(text1);
}
}
protected virtual void Render(HtmlTextWriter writer)
{
this.RenderChildren(writer);
}
protected virtual ControlCollection CreateControlCollection()
{
return new ControlCollection(this);
}
// Properties
public virtual ControlCollection Controls
{
get
{
if (this._controls == null)
{
this._controls = this.CreateControlCollection();
}
return this._controls;
}
}
// Fields
private ControlCollection _controls;
}
Control基類中的屬性和方法很多,為清晰起見,我只保留了幾個與模式有關的關鍵方法與屬性。在上述的源代碼中,我們需要注意幾點:
1、Control類不是抽象類,而是具體類。這是因為在設計時,我們可能會創(chuàng)建Control類型的實例。根據(jù)這一點來看,這并不符合OOP的要求。一般而言,作為抽象出來的基類,必須定義為接口或抽象類。不過在實際的設計中,也不應拘泥于這些條條框框,而應審時度勢,根據(jù)實際的情況來抉擇最佳的設計方案。
2、公共屬性Controls為ControlCollection類型,且該屬性為virtual屬性。也就是說,這個屬性可以被它的子類override。同時,該屬性為只讀屬性,在其get訪問器中,調(diào)用了方法CreateControlCollection();這個方法為protected虛方法,默認的實現(xiàn)是返回一個ControlCollection實例。
3、方法HasControls(),功能為判斷Control對象是否有子控件。它判斷的依據(jù)是根據(jù)私有字段_controls(即公共屬性Controls)的Count值。但是需要注意的是,通過HasControls()方法的返回值,并不能決定對象本身屬于葉節(jié)點,還是枝節(jié)點。因為即使是枝節(jié)點其內(nèi)部仍然可以不包含任何子對象。
4、 方法DataBind()的實現(xiàn)中,首先調(diào)用了自身的OnDataBinding()方法,然后又遍歷了Controls中的所有控件,并調(diào)用其DataBind()方法。該方法屬于控件的共有行為,從這里可以看出不管是作為葉節(jié)點的控件,還是作為枝節(jié)點的控件,它們都實現(xiàn)統(tǒng)一的接口。對于客戶端調(diào)用而言,枝節(jié)點和葉節(jié)點是沒有區(qū)別的。
5、 Control類的完整源代碼中,并不存在Add()、Remove()等類似的方法,以提供添加和移除子控件的功能。事實上,繼承Control類的所有子類均不存在Add()、Remove()等方法。
顯然,在Control類的定義和實現(xiàn)中,值得我們重視的是公共屬性Controls的類型ControlCollection。顧名思義,該類必然是一個集合類型。是否有關子控件的操作,都是在ControlCollection類型中實現(xiàn)呢?我們來分析一下ControlCollection的代碼:
public class ControlCollection : ICollection, IEnumerable
{
// Methods
public ControlCollection(Control owner)
{
this._readOnlyErrorMsg = null;
if (owner == null)
{
throw new ArgumentNullException("owner");
}
this._owner = owner;
}
public virtual void Add(Control child)
{
if (child == null)
{
throw new ArgumentNullException("child");
}
if (this._readOnlyErrorMsg != null)
{
throw new HttpException(HttpRuntime.FormatResourceString(this._readOnlyErrorMsg));
}
if (this._controls == null)
{
this._controls = new Control[5];
}
else if (this._size >= this._controls.Length)
{
Control[] controlArray1 = new Control[this._controls.Length * 4];
Array.Copy(this._controls, controlArray1, this._controls.Length);
this._controls = controlArray1;
}
int num1 = this._size;
this._controls[num1] = child;
this._size++;
this._version++;
this._owner.AddedControl(child, num1);
}
public virtual void Remove(Control value)
{
int num1 = this.IndexOf(value);
if (num1 >= 0)
{
this.RemoveAt(num1);
}
}
// Indexer
public virtual Control this[int index]
{
get
{
if ((index < 0) || (index >= this._size))
{
throw new ArgumentOutOfRangeException(”index”);
}
return this._controls[index];
}
}
// Properties
public int Count
{
get
{
return this._size;
}
}
protected Control Owner
{
get
{
return this._owner;
}
}
protected Control Owner { get; }
// Fields
private Control[] _controls;
private const int _defaultCapacity = 5;
private const int _growthFactor = 4;
private Control _owner;
}
一目了然,正是ControlCollection的Add()、Remove()方法完成了對子控件的添加和刪除。例如:
Control parent = new Control();
Control child = new Child();
//添加子控件child;
parent.Controls.Add(child);
//移除子控件child;
parent.Controls.Remove(child);
為什么要專門提供ControlCollection類型來管理控件的子控件呢?首先,作為類庫使用者,自然希望各種類型的控件具有統(tǒng)一的接口,尤其是自定義控件的時候,不希望自己重復定義管理子控件的操作;那么采用透明方式自然是最佳方案。然而,在使用控件的時候,安全也是需要重點考慮的,如果不考慮子控件管理的合法性,一旦使用錯誤,會導致整個應用程序出現(xiàn)致命錯誤。從這樣的角度考慮,似乎又應采用安全方式。這里就存在一個抉擇。故而,.Net在實現(xiàn)Control類庫時,利用了職責分離的原則,將控件對象管理子控件的屬性與行為和控件本身分離,并交由單獨的ControlCollection類負責。同時采用聚合而非繼承的方式,以一個公共屬性Controls,存在于Control類中。這種方式,集保留了透明方式和安全方式的優(yōu)勢,又摒棄了這兩種方式固有的缺陷,因此我名其為“復合方式”。
“復合方式”的設計,其對安全的保障,不僅僅是去除了Control類關于子控件管理的統(tǒng)一接口,同時還通過異常管理的方式,在ControlCollection類的子類中實現(xiàn):
public class EmptyControlCollection : ControlCollection
{
// Methods
public EmptyControlCollection(Control owner) : base(owner)
{}
public override void Add(Control child)
{
this.ThrowNotSupportedException();
}
private void ThrowNotSupportedException()
{
throw new HttpException(HttpRuntime.FormatResourceString(”Control_does_not_allow_children”, base.Owner.GetType().ToString()));
}
}
EmptyControlCollection繼承了ControlCollection類,并重寫了Add()等添加子控件的方法,使其拋出一個異常。注意,它并沒有重寫父類的Remove()方法,這是因為ControlCollection類在實現(xiàn)Remove()方法時,對集合內(nèi)的數(shù)據(jù)進行了非空判斷。而在EmptyControlCollection類中,是不可能添加子控件的,直接調(diào)用父類的Remove()方法,是不會出現(xiàn)錯誤的。
既然管理子控件的職責由ControlCollection類型負責,且Control類中的公共屬性Controls即為ControlCollection類型。所以,對于控件而言,如果是樹形結(jié)構(gòu)中的葉節(jié)點,它不能包含子控件,它的Controls屬性就應為EmptyControlCollection類型,假如用戶調(diào)用了Controls的Add()方法,就會拋出異常。如果控件是樹形結(jié)構(gòu)中的枝節(jié)點,它支持子控件,那么Controls屬性就是ControlCollection類型。究竟是枝節(jié)點還是葉節(jié)點,決定權在于公共屬性Controls:
public virtual ControlCollection Controls
{
get
{
if (this._controls == null)
{
this._controls = this.CreateControlCollection();
}
return this._controls;
}
}
在屬性的get訪問器中,調(diào)用了protected方法CreateControlCollection(),它創(chuàng)建并返回了一個ControlCollection實例:
protected virtual ControlCollection CreateControlCollection()
{
return new ControlCollection(this);
}
很明顯,在Control基類實現(xiàn)Controls屬性時,采用了Template Method模式,它推遲了ControlCollection的創(chuàng)建,將決定權交給了CreateControlCollection()方法。
如果我們需要定義一個控件,要求它不能管理子控件,就重寫CreateControlCollection()方法,返回EmptyControlCollection對象:
protected override ControlCollection CreateControlCollection()
{
return new EmptyControlCollection(this);
}
現(xiàn)在再回過頭來看HtmlControl和HtmlContainerControl類。根據(jù)前面的分析,我們要求HtmlContainerControl繼承HtmlControl類,同時,HtmlContainerControl應為枝節(jié)點,能夠管理子控件;HtmlControl則為葉節(jié)點,不支持子控件。通過引入ControlCollection類和其子類EmptyControlCollection,以及Template Method模式后,這些類之間的關系與結(jié)構(gòu)如下所示:
HtmlContainerControl繼承了HtmlControl類,這兩個類都重寫了自己父類的protected方法CreateControlCollection()。HtmlControl類,該方法返回EmptyControlCollection對象,使其成為了不包含子控件的葉節(jié)點;HtmlContainerControl類中,該方法則返回ControlCollection對象,從而被賦予了管理子控件的能力,成為了枝節(jié)點:
public abstract class HtmlControl : Control, IAttributeAccessor
{
// Methods
protected override ControlCollection CreateControlCollection()
{
return new EmptyControlCollection(this);
}
}
public abstract class HtmlContainerControl : HtmlControl
{
// Methods
protected override ControlCollection CreateControlCollection()
{
return new ControlCollection(this);
}
}
HtmlControl和HtmlContainerControl類均為抽象類。要定義它們的子類,如果不重寫其父類的CreateControlCollection()方法,那么它們的Controls屬性,就與父類完全一致。例如HtmlImage控件繼承自HtmlControl類,該控件不能添加子控件;而HtmlForm控件則繼承自HtmlContainerControl類,顯然,HtmlForm控件是支持添加子控件的操作的。
.Net的控件設計采用Composite模式的“復合方式”,較好地將控件的透明性與安全性結(jié)合起來,它的特點是:
1、在統(tǒng)一接口中消除了Add()、Remove()等子控件的管理方法,而由ControlCollection類實現(xiàn),同時通過EmptyControlCollection類保障了控件進一步的安全;
2、控件能否管理子控件,不由繼承的層次決定;而是通過重寫CreateControlCollection()方法,由Controls屬性的真正類型來決定。
如此一來,要定義自己的控件就更加容易。我們可以任意地擴展自己的控件類。不管繼承自Control,還是HtmlControl或HtmlContainerControl,都可以輕松地定義出具有枝節(jié)點或葉節(jié)點屬性的新控件。如果有新的需求要求改變管理子控件的方式,我們還可以定義繼承自ControlCollection的類,并在控件類的方法CreateControlCollection()中創(chuàng)建并返回它的實例。




