.Net中的設計模式——Composite模式

一、模式概述

描述Composite模式的最佳方式莫過於樹形圖。從抽象類或接口爲根節點開始,然後生枝發芽,以形成樹枝節點和葉結點。因此,Composite模式通常用來描述部分與整體之間的關係,而通過根節點對該結構的抽象,使得客戶端可以將單元素節點與複合元素節點作爲相同的對象來看待。

由於Composite模式模糊了單元素和複合元素的區別,就使得我們爲這些元素提供相關的操作時,可以有一個統一的接口。例如,我們要編寫一個字處理軟件,該軟件能夠處理文字,對文章進行排版、預覽、打印等功能。那麼,這個工具需要處理的對象,就應該包括單個的文字、以及由文字組成的段落,乃至整篇文檔。這些對象從軟件處理的角度來看,對外的接口應該是一致的,例如改變文字的字體,改變文字的位置使其居中或者右對齊,也可以顯示對象的內容,或者打印。而從內部實現來看,我們對段落或者文檔進行操作,實質上也是對文字進行操作。從結構來看,段落包含了文字,文檔又包含了段落,是一個典型的樹形結構。而其根節點正是我們可以抽象出來暴露在外的統一接口,例如接口IElement

composite1.GIF
既然文字、段落、文檔都具有這些操作,因此它們都可以實現
IElement接口:

composite2.GIF
從上圖可以看到,對象
WordParagraphDocument均實現了IElement接口,但ParagraphDocumentWord對象不同的是,這兩者處除了實現了IElement接口,它們還與IElement接口對象之間具有聚合的關係,且是一對多。也就是說ParagraphDocument對象內可以包含0個到多個IElement對象,這也是與前面對字處理軟件分析獲得的結果是一致的。

從整個結構來看,完全符合樹形結構的各個要素,接口IElement是根節點,而ParagraphDocument類爲枝節點,Word對象爲葉節點。既然作爲枝節點,它就具有帶葉節點的能力,從上圖的聚合關係中我們體現出了這一點。也就是說,ParagraphDocument類除了具有排版、打印方面的職責外,還能夠添加、刪除葉節點的操作。那麼這些操作應該放在哪裏呢?

管理對子節點的管理,Composite模式提供了兩種方式:一個是透明方式,也就是說在根節點中聲明所有用來管理子元素的方法,包括Add()Remove()等方法。這樣一來,實現根節點接口的子節點同時也具備了管理子元素的能力。這種實現策略,最大的好處就是完全消除了葉節點和枝節點對象在抽象層次的區別,它們具備完全一致的接口。而缺點則是不夠安全。由於葉節點本身不具備管理子元素的能力,因此提供的Add()Remove()方法在實現層次是無意義的。但客戶端調用時,卻看不到這一點,從而導致在運行期間有出錯的可能。

另一種策略則是安全方式。與透明方式剛好相反,它只在枝節點對象裏聲明管理子元素的方法,由於葉節點不具備這些方法,當客戶端在操作葉節點時,就不會出現前一種方式的安全錯誤。然而,這種實現方式,卻導致了葉節點和枝節點接口的不完全一致,這給客戶端操作時帶來了不便。

這兩種方式各有優缺點,我們在實現時,應根據具體的情況,作出更加合理的抉擇。在字處理軟件一例中,我選擇了安全方式來實現,因爲對於客戶端而言,在調用IElement接口時,通常是將其視爲可被排版、打印等操作的對象。至於爲ParagraphDocument對象添加、刪除子對象,往往是一種初始化的行爲,完全可以放到一個單獨的模塊中。根據單一職責原則(SRP),我們沒有必要讓IElement接口負累太重。所以,我們需要對上圖作稍許的修改,在ParagraphDocument對象中增加Add()Remove()方法:

composite3.GIF
以下是
IElement對象結構的實現代碼:

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類略;

實際上,我們在爲葉節點實現Add()Remove()方法時,還需要考慮一些異常情況。例如在Paragraph類中,添加的子元素就不能是Document對象和Paragraph對象。所以在添加IElement對象時,還需要做一些條件判斷,以確定添加行爲是否正確,如果錯誤,應拋出異常。

採用Composite模式,我們將WordParagraphDocument抽象爲IElement接口。雖然各自內部的實現並不相同,枝節點和葉節點的實質也不一樣,但對於調用者而言,是沒有區別的。例如在類WordProcessor中,包含一個GetSelectedElement()靜態方法,它能夠獲得當前選擇的對象:

public class WordProcessor

{

public static IElement GetSelectedElement(){……}

}

對於字處理軟件的UI來說,如果要改變選中對象的字體,則可以在命令按鈕cmdChangeFontClick事件中寫下如下代碼:

public void cmdChangeFont_Click(object sender, EventArgs e)

{

    WordProcessor.GetSelectedElement().ChangeFont(currentFont);

}

不管當前選中的對象是文字、段落還是整篇文檔,對於UI而言,操作都是完全一致的,根本不需要去判斷對象的類別。因此,如果在Business Layer的類庫設計時,採用Composite模式,將極大地簡化UI表示層的開發工作。此外,應用該模式也較好的支持項目的可擴展性。例如,我們爲IElement接口增加了Sentence類,對於前面的例子而言,只需要修改GetSelectedElement()方法,而cmdChangeFont命令按鈕的Click事件以及Business Layer類庫原有的設計,都不需要做任何改變。這也符合OO的開放-封閉原則(OCP),即對於擴展是開放的(Open for extension),對於更改則是封閉的(Closed for modification)。

二、.Net Framework中的Composite模式

.Net中,最能體現Composite模式的莫過於WindowsWeb的控件。在這些控件中,有的包含子控件,有的則不包含且不能包含子控件,這正好符合葉節點和枝節點的含義。所有Web控件的基類爲System.Web.UI.Contril類(如果是Windows控件,則基類爲System.Windows.Forms.Control類)。其子類包含有HtmlControlHtmlContainerControl等。按照Composite模式的結構,枝節點和葉節點屬於根節點的不同分支,同時枝節點與根節點之間應具備一個聚合關係,可以通過Add()Remove()方法添加和移除其子節點。設定HtmlControl爲葉節點,而HtmlContaiinerControl爲枝節點,那麼採用透明方式的設計方法,在.Net中控件類的結構,就應該如下圖所示:

composite4.GIF
雖然根據透明方式的
Composite模式,HtmlControl類與其父類Control之間也應具備一個聚合關係,但實質上該類並不具備管理子控件的職責,因此我在類圖中忽略了這個關係。此時,HtmlControl類中的Add()Remove()方法,應該爲空,或者拋出一個客戶端能夠捕獲的異常。

然而,從具體實現來考慮,由於HtmlControl類和HtmlContainerControl類在實現細節層次,區別僅在於前者不支持子控件,但從控件本身的功能來看,很多行爲是相同或者相近的。例如HtmlControl類的Render()方法,調用了方法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()方法,在這個方法中也調用了RenderBeginTag()方法,且RenderBeginTag方法的實現和前者完全一致:

protected override void Render(HtmlTextWriter writer)

{

      this.RenderBeginTag(writer);

      this.RenderChildren(writer);

      this.RenderEndTag(writer);

}

按照上面的結構,由於HtmlControlHtmlContainerControl之間並無繼承關係,這就要求兩個類中,都要重複實現RenderBeginTag()方法,從而導致產生重複代碼。根據OO的特點,解決的辦法,就是讓HtmlContainerControl繼承自HtmlControl類(因爲HtmlContainerControl的接口比HtmlControl寬,所以只能令HtmlContainerControl作爲子類),並讓RenderBeginTag()方法成爲HtmlControl類的protected方法,子類HtmlContainerControl可以直接調用這個方法。然而與之矛盾的是,HtmlContainerControl卻是一個可以包含子控件的枝節點,而HtmlControl則是不能包含子控件的葉節點,那麼這樣的繼承關係還成立嗎?

composite5.GIF
HtmlControl
類對Add()方法和Remove()方法的重寫後,這兩個方法內容爲空。由於HtmlContainerControl類繼承HtmlControl類,但我們又要求它的Add()Remove()方法和Control類保持一致,而父類HtmlControl已經重寫這兩個方法,此時是無法直接繼承來自父類的方法的。以上是採用透明方式的設計。

如果採用安全方式,仍然有問題。雖然在HtmlControl類中不再有Add()Remove()方法,但由於Control類和HtmlContainerControl類都允許添加子控件,它們包含的Add()Remove()方法,只能分別實現。這樣的設計必然會導致重複代碼。這也是與我們的期望不符的。

那麼在.Net中,Control類究竟是怎樣實現的呢?下面,我將根據.Net實現Control控件的源代碼,來分析Control控件的真實結構,以及其具體的實現細節。

三、深入分析.Net中的Composite模式

首先,我們來剖析Web控件的基類Control類的內部實現:

public class Control : IComponent, IDisposable, IParserAccessor, IDataBindingsAccessor

{

      // Events;    

      // Methods

public Control()

{

             if (this is INamingContainer)

             {

                    this.flags[0x80] = 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類不是抽象類,而是具體類。這是因爲在設計時,我們可能會創建Control類型的實例。根據這一點來看,這並不符合OOP的要求。一般而言,作爲抽象出來的基類,必須定義爲接口或抽象類。不過在實際的設計中,也不應拘泥於這些條條框框,而應審時度勢,根據實際的情況來抉擇最佳的設計方案。

2、 公共屬性ControlsControlCollection類型,且該屬性爲virtual屬性。也就是說,這個屬性可以被它的子類override。同時,該屬性爲只讀屬性,在其get訪問器中,調用了方法CreateControlCollection();這個方法爲protected虛方法,默認的實現是返回一個ControlCollection實例。

3、 方法HasControls(),功能爲判斷Control對象是否有子控件。它判斷的依據是根據私有字段_controls(即公共屬性Controls)的Count值。但是需要注意的是,通過HasControls()方法的返回值,並不能決定對象本身屬於葉節點,還是枝節點。因爲即使是枝節點其內部仍然可以不包含任何子對象。

4、 方法DataBind()的實現中,首先調用了自身的OnDataBinding()方法,然後又遍歷了Controls中的所有控件,並調用其DataBind()方法。該方法屬於控件的共有行爲,從這裏可以看出不管是作爲葉節點的控件,還是作爲枝節點的控件,它們都實現統一的接口。對於客戶端調用而言,枝節點和葉節點是沒有區別的。

5、 Control類的完整源代碼中,並不存在Add()Remove()等類似的方法,以提供添加和移除子控件的功能。事實上,繼承Control類的所有子類均不存在Add()Remove()等方法。



顯然,在
Control類的定義和實現中,值得我們重視的是公共屬性Controls的類型ControlCollection。顧名思義,該類必然是一個集合類型。是否有關子控件的操作,都是在ControlCollection類型中實現呢?我們來分析一下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;      

}

一目瞭然,正是ControlCollectionAdd()Remove()方法完成了對子控件的添加和刪除。例如:

Control parent = new Control();

Control child = new Child();

//添加子控件child;

parent.Controls.Add(child);

//移除子控件child;

parent.Controls.Remove(child);

爲什麼要專門提供ControlCollection類型來管理控件的子控件呢?首先,作爲類庫使用者,自然希望各種類型的控件具有統一的接口,尤其是自定義控件的時候,不希望自己重複定義管理子控件的操作;那麼採用透明方式自然是最佳方案。然而,在使用控件的時候,安全也是需要重點考慮的,如果不考慮子控件管理的合法性,一旦使用錯誤,會導致整個應用程序出現致命錯誤。從這樣的角度考慮,似乎又應採用安全方式。這裏就存在一個抉擇。故而,.Net在實現Control類庫時,利用了職責分離的原則,將控件對象管理子控件的屬性與行爲和控件本身分離,並交由單獨的ControlCollection類負責。同時採用聚合而非繼承的方式,以一個公共屬性Controls,存在於Control類中。這種方式,集保留了透明方式和安全方式的優勢,又摒棄了這兩種方式固有的缺陷,因此我名其爲“複合方式”。

“複合方式”的設計,其對安全的保障,不僅僅是去除了Control類關於子控件管理的統一接口,同時還通過異常管理的方式,在ControlCollection類的子類中實現:

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類在實現Remove()方法時,對集合內的數據進行了非空判斷。而在EmptyControlCollection類中,是不可能添加子控件的,直接調用父類的Remove()方法,是不會出現錯誤的。

既然管理子控件的職責由ControlCollection類型負責,且Control類中的公共屬性Controls即爲ControlCollection類型。所以,對於控件而言,如果是樹形結構中的葉節點,它不能包含子控件,它的Controls屬性就應爲EmptyControlCollection類型,假如用戶調用了ControlsAdd()方法,就會拋出異常。如果控件是樹形結構中的枝節點,它支持子控件,那麼Controls屬性就是ControlCollection類型。究竟是枝節點還是葉節點,決定權在於公共屬性Controls

public virtual ControlCollection Controls

{

      get

      {

             if (this._controls == null)

             {

                   this._controls = this.CreateControlCollection();

             }

             return this._controls;

      }

}

在屬性的get訪問器中,調用了protected方法CreateControlCollection(),它創建並返回了一個ControlCollection實例:

protected virtual ControlCollection CreateControlCollection()

{

     return new ControlCollection(this);

}

很明顯,在Control基類實現Controls屬性時,採用了Template Method模式,它推遲了ControlCollection的創建,將決定權交給了CreateControlCollection()方法。

如果我們需要定義一個控件,要求它不能管理子控件,就重寫CreateControlCollection()方法,返回EmptyControlCollection對象:

protected override ControlCollection CreateControlCollection()
{
            return new EmptyControlCollection(this);
}

 

現在再回過頭來看HtmlControlHtmlContainerControl類。根據前面的分析,我們要求HtmlContainerControl繼承HtmlControl類,同時,HtmlContainerControl應爲枝節點,能夠管理子控件;HtmlControl則爲葉節點,不支持子控件。通過引入ControlCollection類和其子類EmptyControlCollection,以及Template Method模式後,這些類之間的關係與結構如下所示:

composite6.GIF
HtmlContainerControl
繼承了HtmlControl類,這兩個類都重寫了自己父類的protected方法CreateControlCollection()HtmlControl類,該方法返回EmptyControlCollection對象,使其成爲了不包含子控件的葉節點;HtmlContainerControl類中,該方法則返回ControlCollection對象,從而被賦予了管理子控件的能力,成爲了枝節點:

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);
}
}

HtmlControlHtmlContainerControl類均爲抽象類。要定義它們的子類,如果不重寫其父類的CreateControlCollection()方法,那麼它們的Controls屬性,就與父類完全一致。例HtmlImage控件繼承自HtmlControl類,該控件不能添加子控件;而HtmlForm控件則繼承自HtmlContainerControl類,顯然,HtmlForm控件是支持添加子控件的操作的。

 

.Net的控件設計採用Composite模式的“複合方式”,較好地將控件的透明性與安全性結合起來,它的特點是:

1、 在統一接口中消除了Add()Remove()等子控件的管理方法,而由ControlCollection類實現,同時通過EmptyControlCollection類保障了控件進一步的安全;

2、 控件能否管理子控件,不由繼承的層次決定;而是通過重寫CreateControlCollection()方法,由Controls屬性的真正類型來決定。

如此一來,要定義自己的控件就更加容易。我們可以任意地擴展自己的控件類。不管繼承自Control,還是HtmlControlHtmlContainerControl,都可以輕鬆地定義出具有枝節點或葉節點屬性的新控件。如果有新的需求要求改變管理子控件的方式,我們還可以定義繼承自ControlCollection的類,並在控件類的方法CreateControlCollection()中創建並返回它的實例。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章