在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进入阅读。习惯在博客园里阅读的朋友也可以查看本人博客的“设计模式”部分。
背景
某公司打算开发一套企业管理软件,于是,企业的组织结构管理就成为了这个软件的重要功能之一。组织结构管理系统所涵盖的功能还是比较多的,而且还会与系统的其它部分产生紧密的联系,比如审批流程、成本核算等等都与企业组织结构紧密相关。为了配合本文的描述,我们尽可能地简化这部分功能,只将功能限定在组织结构的建立、维护和验证上,基本需求包括:
- 企业组织结构包括部门、职员两种元素
- 部门可以包含多个子部门,还可以包含多个职员
- 所有职员必须归属于一个特定的部门
- 部门可以属于另一个部门,也可以作为一级部门直属于组织结构
- 提供一个简单的图形化界面,用于编辑企业组织结构
- 提供各个层级的验证的功能,用以对部门或职员的数据设置进行验证
- 提供保存和打开的功能,在保存之前会验证整个组织结构的设置,如果验证失败,将不予以保存
接下来,我们以面向对象分析和设计的方式,来探讨本案例的模型设计和实现。
领域模型
从上面的基本需求描述不难得知,模型对象包括三种:组织结构、部门和职员。组织结构和部门、部门和职员之间是组合关系,而部门和部门之间则是聚合关系,组合和聚合的差别就在于A是否必须依赖于B,这在UML的规范中是有讨论的,在此也就不多作说明了。另外,熟悉DDD的朋友也时常能够听到“聚合”、“聚合根”的词汇,但这里所说的“聚合”跟DDD中的并不一样,所以需要注意区分。
事实上,在我们的领域范围中,组织结构、部门和职员三者形成了一种树形层次结构:部门隶属于组织结构或另一部门,而职员又隶属于部门,这正是组合对象(Composite)模式应用的典型场景。因此,我们可以使用Composite模式来设计领域模型。鉴于部门和职员共有着部分属性(例如全局唯一标识“ID”)和一些相关操作,我们就把这部分内容抽象出来,以OrganizationElement抽象类对其进行表示,于是,我们就得到了下面的模型图:
根据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应用程序开发的知识点,但这些都不是本文所要讨论的内容,因此我将跳过对这些细节内容的介绍。在完成了这些基本功能的开发后,我们的应用程序大致如下图所示:
下面,我们需要向应用程序添加验证功能。为了简单起见,此处我们仅实现以下验证逻辑:
- 同级别中不存在同名的部门或职员
- 部门名称不能为空
- 职员的姓、名不能为空
- 职员的电子邮件不能为空,并应符合电子邮件地址格式
- 职员的电话号码不能为空,并应符合电话号码的格式
在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); }
整个设计的完整类图如下所示:
图中OrganizationValidator就是一个IVisitor接口的实现,在三个Visit的重载方法中,分别完成了对Organization、Department和Employee的验证逻辑。此处就不详述其实现代码了,读者请参考本文附带的源程序代码来了解这个类的具体实现。
效果
在当前的Windows Forms应用程序中添加上基于Visitor模式实现的验证逻辑以后,就可以在任意层级的节点上,单击鼠标右键并选择“验证”菜单项来触发验证逻辑。以下是在添加了一个新的职员信息后,在整个组织结构上进行数据验证的结果,应用程序提示该职员的电子邮件地址格式不正确,以及电话号码不能为空:
总结
本文通过一个实际案例展示了在应用程序开发过程中实现组合(Composite)模式和访问者(Visitor)模式的方式,综上所述,Visitor模式在扩展已有对象结构的操作上,显得很有优势。这种扩展与类型继承的方式有着本质的区别。通过类型继承可以在原类型上增加新的字段和方法,从而达到行为扩展的目的;但从面向对象的角度来看,有些行为又本不应该属于这些对象,比如本案例中的验证功能,它本不应该是组织结构模型的一种行为(组织结构对象不可能自己验证自己),而是应用程序为组织结构提供的一种附加功能。Visitor模式很好地把这些行为的实现与对象结构分离,使得应用程序可以在不改变对象结构和现有行为的基础上,为之提供新的行为实现(比如,在本案例中如果还需要实现整个组织结构的某项数据统计功能,那么只需要再实现一个OrganizationCounterVisitor类型即可),有效、合理地满足了面向对象设计中的“关注点分离”和“开闭”原则。
源程序代码
请至 http://sdrv.ms/11eOzEF 下载本案例的源程序代码。在Visual Studio 2012中打开解决方案文件后,运行Windows Forms主程序OrgMgmt,然后使用“系统 –> 打开”菜单来打开压缩包中的organization.org文件。