在C#中使用訪問者(Visitor)模式對組合(Composite)對象進行驗證


【注:本文已被收錄到MSDN,詳細地址:http://msdn.microsoft.com/zh-cn/library/dn155800.aspx

在應用程序的開發過程中合理使用設計模式,不僅能夠解決實際問題,提高開發效率,而且還能夠讓程序結構更為清晰合理,對達到“低耦合、高內聚”的設計目的有着很大的幫助。目前網上有很多介紹設計模式的文章,有的也自成體系,基本涵蓋了GoF的所有模式,但大多數類似文章都以一些較為簡單的類型設計為例(比如Animal、Cat、Dog、Fruit、Apple、Banana等),雖然淺顯易懂,但讀完之后發現離實際應用還是存在一定的距離。鑒於這樣的現狀,我也打算總結一些我在項目中碰到的模式應用案例,通過對實際問題進行分析描述,來把握模式應用的思路,從而幫助讀者朋友來了解如何在項目中合理地應用設計模式。事實上我已經總結了一些,以一種更為合理的組織方式發布在了我的.NET/DDD架構設計討論社區Apworks.ORG中,可以點擊:http://apworks.org/?page_id=311進入閱讀。習慣在博客園里閱讀的朋友也可以查看本人博客的“設計模式”部分。

背景

某公司打算開發一套企業管理軟件,於是,企業的組織結構管理就成為了這個軟件的重要功能之一。組織結構管理系統所涵蓋的功能還是比較多的,而且還會與系統的其它部分產生緊密的聯系,比如審批流程、成本核算等等都與企業組織結構緊密相關。為了配合本文的描述,我們盡可能地簡化這部分功能,只將功能限定在組織結構的建立、維護和驗證上,基本需求包括:

  1. 企業組織結構包括部門、職員兩種元素
  2. 部門可以包含多個子部門,還可以包含多個職員
  3. 所有職員必須歸屬於一個特定的部門
  4. 部門可以屬於另一個部門,也可以作為一級部門直屬於組織結構
  5. 提供一個簡單的圖形化界面,用於編輯企業組織結構
  6. 提供各個層級的驗證的功能,用以對部門或職員的數據設置進行驗證
  7. 提供保存和打開的功能,在保存之前會驗證整個組織結構的設置,如果驗證失敗,將不予以保存

接下來,我們以面向對象分析和設計的方式,來探討本案例的模型設計和實現。

領域模型

從上面的基本需求描述不難得知,模型對象包括三種:組織結構、部門和職員。組織結構和部門、部門和職員之間是組合關系,而部門和部門之間則是聚合關系,組合和聚合的差別就在於A是否必須依賴於B,這在UML的規范中是有討論的,在此也就不多作說明了。另外,熟悉DDD的朋友也時常能夠聽到“聚合”、“聚合根”的詞匯,但這里所說的“聚合”跟DDD中的並不一樣,所以需要注意區分。

事實上,在我們的領域范圍中,組織結構、部門和職員三者形成了一種樹形層次結構:部門隸屬於組織結構或另一部門,而職員又隸屬於部門,這正是組合對象(Composite)模式應用的典型場景。因此,我們可以使用Composite模式來設計領域模型。鑒於部門和職員共有着部分屬性(例如全局唯一標識“ID”)和一些相關操作,我們就把這部分內容抽象出來,以OrganizationElement抽象類對其進行表示,於是,我們就得到了下面的模型圖:

image

根據Composite模式的描述,Department類型繼承於OrganizationElement抽象類型,同時,它又聚合了OrganizationElement類型,因此,Department類型中可以聚合任何OrganizationElement的派生類型,也就是可以包含多個Employee或者Department。為了編程方便,我在Department類型中設計了兩個只讀屬性,用以分別返回所包含的所有Employee對象和Department對象。這種篩選其實很簡單,直接使用LINQ語句即可完成,比如:

// class Department

readonly List<OrganizationElement> elements = new List<OrganizationElement>();

public IEnumerable<Department> Departments
{
    get
    {
        return (from element in elements
                where element is Department
                select (element as Department)).ToList();
    }
}

有了上面設計的類圖,將其轉換成C#代碼就非常簡單了,以下是Organization、Department以及Employee類的實現代碼段,當然,這些代碼段僅體現了類之間的關系,此處並沒有展示與功能實現相關的其它部分。

public abstract class OrganizationElement
{
    readonly Guid id = Guid.NewGuid();

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj))
            return true;
        if (obj == null)
            return false;
        OrganizationElement other = obj as OrganizationElement;
        return other != null && other.ID == this.ID;
    }

    public override int GetHashCode()
    {
        return id.GetHashCode();
    }
    
    public Guid ID
    {
        get { return id; }
    }
}

public class Organization : ICollection<OrganizationElement>
{
    readonly List<OrganizationElement> elements = new List<OrganizationElement>();
    
    public IEnumerable<Department> Departments
    {
        get
        {
            return elements.Where(p => p is Department)
                .Select(p => p as Department).ToList();
        }
    }
    
    #region ICollection<OrganizationElement> Members

    public void Add(OrganizationElement item)
    {
        elements.Add(item);
    }

    public void Clear()
    {
        elements.Clear();
    }

    public bool Contains(OrganizationElement item)
    {
        return elements.Contains(item);
    }

    public void CopyTo(OrganizationElement[] array, int arrayIndex)
    {
        elements.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return elements.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(OrganizationElement item)
    {
        return elements.Remove(item);
    }

    #endregion

    #region IEnumerable<OrganizationElement> Members

    public IEnumerator<OrganizationElement> GetEnumerator()
    {
        return elements.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return elements.GetEnumerator();
    }

    #endregion    
}

public class Department : OrganizationElement, ICollection<OrganizationElement>
{
    readonly List<OrganizationElement> elements = new List<OrganizationElement>();
    
    public IEnumerable<Department> Departments
    {
        get
        {
            return (from element in elements
                    where element is Department
                    select (element as Department)).ToList();
        }
    }

    public IEnumerable<Employee> Employees
    {
        get
        {
            return (from element in elements
                    where element is Employee
                    select (element as Employee)).ToList();
        }
    }
    
    public string Name { get; set; }
    public string Description { get; set; }
    
    #region ICollection<OrganizationElement> Members

    public void Add(OrganizationElement item)
    {
        elements.Add(item);
    }

    public void Clear()
    {
        elements.Clear();
    }

    public bool Contains(OrganizationElement item)
    {
        return elements.Contains(item);
    }

    public void CopyTo(OrganizationElement[] array, int arrayIndex)
    {
        elements.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return elements.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(OrganizationElement item)
    {
        return elements.Remove(item);
    }

    #endregion

    #region IEnumerable<OrganizationElement> Members

    public IEnumerator<OrganizationElement> GetEnumerator()
    {
        return elements.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return elements.GetEnumerator();
    }

    #endregion
}
    
public class Employee : OrganizationElement
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string PhoneNumber { get; set; }
    public string Email { get; set; }
}

接下來,我們會進一步豐富這些類,並逐漸完善應用程序的一些基本功能,比如:向用戶提供一個Windows Forms的應用界面,在樹形視圖控件(TreeView)中對整個組織結構進行維護,並向用戶提供新建、打開和保存組織結構的功能。這個過程其實還包含了很多C#/Windows Forms應用程序開發的知識點,但這些都不是本文所要討論的內容,因此我將跳過對這些細節內容的介紹。在完成了這些基本功能的開發后,我們的應用程序大致如下圖所示:

SNAGHTMLfd00ef

下面,我們需要向應用程序添加驗證功能。為了簡單起見,此處我們僅實現以下驗證邏輯:

  1. 同級別中不存在同名的部門或職員
  2. 部門名稱不能為空
  3. 職員的姓、名不能為空
  4. 職員的電子郵件不能為空,並應符合電子郵件地址格式
  5. 職員的電話號碼不能為空,並應符合電話號碼的格式

在C#中,實現這些驗證的方式是多樣的,就我們目前的這個案例而言,大致可以使用以下幾種方式:

  • 在屬性的設置器(setter)中驗證數據有效性,當驗證失敗時拋出異常,Windows Forms的PropertyGrid控件會捕獲異常並防止數據寫入
  • 自定義一套基於Attribute的驗證機制,在屬性上設置Attribute,並在屬性被設置的時候,通過這套機制完成驗證
  • 使用AOP,攔截屬性的設置器行為進行驗證
  • 遍歷整個樹形結構,對每個節點進行驗證,並統計各節點的驗證結果,最后將結果報告給用戶

前三種方式其實都是在屬性被設置的時候完成數據驗證,這樣做能夠在用戶操作的每個步驟確保數據的正確性,但同時也會損失一定的用戶體驗;而第四種方式則向用戶提供了更為高效的操作體驗,開發者可以根據自己項目的實際情況進行選擇。現在,就讓我們一起了解一下第四種方式的實現方法。

使用訪問者(Visitor)模式實現驗證邏輯

鑒於我們的領域模型由於組合(Composite)模式的使用而呈現出一種特定的對象結構(此處是樹形結構),我們可以采用遍歷整個對象結構的方式,對該結構的每一個節點進行指定的操作(驗證)。此處我將在組織結構的模型上應用訪問者(Visitor)模式,實現每個節點的驗證功能。簡單地說,訪問者(Visitor)模式的重點並不在於節點的遍歷過程,它的優點在於,它能夠將遍歷過程中針對每個節點的操作,從對象結構本身分離出來,從而達到了“關注點分離”的設計目的。此外,由於定義新的操作時,無需對已有的對象結構作任何修改,因此,Visitor模式的使用,還能夠讓設計滿足“開-閉”原則(OCP)。

從Visitor模式的實現上看,主要利用了面向對象的多態性,比如,針對組織結構模型,可以定義一個IVisitor接口,所有實現了該接口的類型都能夠對Organization、Department和Employee三種類型的對象進行操作:

public interface IVisitor
{
    void Visit(Organization organization);
    void Visit(Department department);
    void Visit(Employee employee);
}

在Organization、Department和Employee中,則需要接受一個IVisitor接口的實例,並調用該實例中的相應方法,以完成對當前對象的操作。這個過程其實很簡單,比如可以在Organization、Department以及Employee中定義一個Accept方法,這個方法接受一個IVisitor的實例作為參數,而在Accept方法中,只需要調用IVisitor.Visit方法即可。就Department而言,由於它本身還聚合了其它的OrganizationElement對象,因此,在Department的Accept方法中,還需要將IVisitor實例傳遞給每個子OrganizationElement對象的Accept方法,以達到遍歷整個對象結構的目的。以下就是Department類中的Accept方法實現:

public override void Accept(IVisitor visitor)
{
    visitor.Visit(this);
    foreach (var element in this.elements)
        element.Accept(visitor);
}

現在,讓我們來優化一下這個設計。在前一部分的分析中,我們引入了OrganizationElement作為組織結構模型中所有元素的抽象類型,它包含了這些元素的共有屬性和操作。在遍歷整個組織結構對象模型的時候,每個模型元素都將被訪問一次,這也就意味着Visitor中所定義的操作會應用到每個模型元素上。由此可見,我們可以從實現上將Accept方法定義在OrganizationElement的層面上,在OrganizationElement中,提供一個Accept的抽象方法,所有繼承於OrganizationElement的類型都需要實現Accept方法以完成Visitor對其的訪問。

為了進一步統一Organization類與OrganizationElement類的行為,我們在更高的層面上引入IVisitorAcceptor接口,並讓Organization和OrganizationElement都實現這個接口,這樣做的好處是,在用戶界面部分,我們無需區分當前選中的驗證節點到底是Organization還是OrganizationElement,只需要將保存在節點中的數據轉換為IVisitorAcceptor接口的實例,即可接受Visitor來遍歷所選的對象結構。IVisitorAcceptor接口定義如下:

public interface IVisitorAcceptor
{
    void Accept(IVisitor visitor);
}

基於上面的分析,Organization和OrganizationElement的實現代碼如下(僅列出與Visitor模式相關的部分):

public class Organization : ICollection<OrganizationElement>, IVisitorAcceptor
{
    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
        foreach (var department in this.elements)
            department.Accept(visitor);
    }
}

public abstract class OrganizationElement : IVisitorAcceptor
{
    public abstract void Accept(IVisitor visitor);
}

整個設計的完整類圖如下所示:

image

圖中OrganizationValidator就是一個IVisitor接口的實現,在三個Visit的重載方法中,分別完成了對Organization、Department和Employee的驗證邏輯。此處就不詳述其實現代碼了,讀者請參考本文附帶的源程序代碼來了解這個類的具體實現。

效果

在當前的Windows Forms應用程序中添加上基於Visitor模式實現的驗證邏輯以后,就可以在任意層級的節點上,單擊鼠標右鍵並選擇“驗證”菜單項來觸發驗證邏輯。以下是在添加了一個新的職員信息后,在整個組織結構上進行數據驗證的結果,應用程序提示該職員的電子郵件地址格式不正確,以及電話號碼不能為空:

image

總結

本文通過一個實際案例展示了在應用程序開發過程中實現組合(Composite)模式和訪問者(Visitor)模式的方式,綜上所述,Visitor模式在擴展已有對象結構的操作上,顯得很有優勢。這種擴展與類型繼承的方式有着本質的區別。通過類型繼承可以在原類型上增加新的字段和方法,從而達到行為擴展的目的;但從面向對象的角度來看,有些行為又本不應該屬於這些對象,比如本案例中的驗證功能,它本不應該是組織結構模型的一種行為(組織結構對象不可能自己驗證自己),而是應用程序為組織結構提供的一種附加功能。Visitor模式很好地把這些行為的實現與對象結構分離,使得應用程序可以在不改變對象結構和現有行為的基礎上,為之提供新的行為實現(比如,在本案例中如果還需要實現整個組織結構的某項數據統計功能,那么只需要再實現一個OrganizationCounterVisitor類型即可),有效、合理地滿足了面向對象設計中的“關注點分離”和“開閉”原則。

源程序代碼

請至 http://sdrv.ms/11eOzEF 下載本案例的源程序代碼。在Visual Studio 2012中打開解決方案文件后,運行Windows Forms主程序OrgMgmt,然后使用“系統 –> 打開”菜單來打開壓縮包中的organization.org文件。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM