背景
某公司打算开发一套企业管理软件,于是,企业的组织结构管理就成为了这个软件的重要功能之一。组织结构管理系统所涵盖的功能还是比较多的,而且还会与系统的其它部分产生紧密的联系,比如审批流程、成本核算等等都与企业组织结构紧密相关。为了配合本文的描述,我们尽可能地简化这部分功能,只将功能限定在组织结构的建立、维护和验证上,基本需求包括:
企业组织结构包括部门、职员两种元素
部门可以包含多个子部门,还可以包含多个职员
所有职员必须归属于一个特定的部门
部门可以属于另一个部门,也可以作为一级部门直属于组织结构
提供一个简单的图形化界面,用于编辑企业组织结构
提供各个层级的验证的功能,用以对部门或职员的数据设置进行验证
提供保存和打开的功能,在保存之前会验证整个组织结构的设置,如果验证失败,将不予以保存
接下来,我们以面向对象分析和设计的方式,来探讨本案例的模型设计和实现。
领域模型
从上面的基本需求描述不难得知,模型对象包括三种:组织结构、部门和职员。组织结构和部门、部门和职员之间是组合关系,而部门和部门之间则是聚合关系,组合和聚合的差别就在于A是否必须依赖于B,这在UML的规范中是有讨论的,在此也就不多作说明了。另外,熟悉DDD的朋友也时常能够听到“聚合”、“聚合根”的词汇,但这里所说的“聚合”跟DDD中的并不一样,所以需要注意区分。
事实上,在我们的领域范围中,组织结构、部门和职员三者形成了一种树形层次结构:部门隶属于组织结构或另一部门,而职员又隶属于部门,这正是组合对象(Composite)模式应用的典型场景。因此,我们可以使用Composite模式来设计领域模型。鉴于部门和职员共有着部分属性(例如全局唯一标识“ID”)和一些相关操作,我们就把这部分内容抽象出来,以OrganizationElement抽象类对其进行表示,于是,我们就得到了下面的模型图:
根据Composite模式的描述,Department类型继承于OrganizationElement抽象类型,同时,它又聚合了OrganizationElement类型,因此,Department类型中可以聚合任何OrganizationElement的派生类型,也就是可以包含多个Employee或者Department。为了编程方便,我在Department类型中设计了两个只读属性,用以分别返回所包含的所有Employee对象和Department对象。这种筛选其实很简单,直接使用LINQ语句即可完成,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 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类的实现代码段,当然,这些代码段仅体现了类之间的关系,此处并没有展示与功能实现相关的其它部分。
下面,我们需要向应用程序添加验证功能。为了简单起见,此处我们仅实现以下验证逻辑:
同级别中不存在同名的部门或职员
部门名称不能为空
职员的姓、名不能为空
职员的电子邮件不能为空,并应符合电子邮件地址格式
职员的电话号码不能为空,并应符合电话号码的格式
在C#中,实现这些验证的方式是多样的,就我们目前的这个案例而言,大致可以使用以下几种方式:
在属性的设置器(setter)中验证数据有效性,当验证失败时抛出异常,Windows Forms的PropertyGrid控件会捕获异常并防止数据写入
自定义一套基于Attribute的验证机制,在属性上设置Attribute,并在属性被设置的时候,通过这套机制完成验证
使用AOP,拦截属性的设置器行为进行验证
遍历整个树形结构,对每个节点进行验证,并统计各节点的验证结果,最后将结果报告给用户
前三种方式其实都是在属性被设置的时候完成数据验证,这样做能够在用户操作的每个步骤确保数据的正确性,但同时也会损失一定的用户体验;而第四种方式则向用户提供了更为高效的操作体验,开发者可以根据自己项目的实际情况进行选择。现在,就让我们一起了解一下第四种方式的实现方法。
使用访问者(Visitor)模式实现验证逻辑
鉴于我们的领域模型由于组合(Composite)模式的使用而呈现出一种特定的对象结构(此处是树形结构),我们可以采用遍历整个对象结构的方式,对该结构的每一个节点进行指定的操作(验证)。此处我将在组织结构的模型上应用访问者(Visitor)模式,实现每个节点的验证功能。简单地说,访问者(Visitor)模式的重点并不在于节点的遍历过程,它的优点在于,它能够将遍历过程中针对每个节点的操作,从对象结构本身分离出来,从而达到了“关注点分离”的设计目的。此外,由于定义新的操作时,无需对已有的对象结构作任何修改,因此,Visitor模式的使用,还能够让设计满足“开-闭”原则(OCP)。
从Visitor模式的实现上看,主要利用了面向对象的多态性,比如,针对组织结构模型,可以定义一个IVisitor接口,所有实现了该接口的类型都能够对Organization、Department和Employee三种类型的对象进行操作:
1 2 3 4 5 6 | 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方法实现:
1 2 3 4 5 6 | 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接口定义如下:
1 2 3 4 | public interface IVisitorAcceptor { void Accept(IVisitor visitor); } |
基于上面的分析,Organization和OrganizationElement的实现代码如下(仅列出与Visitor模式相关的部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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类型即可),有效、合理地满足了面向对象设计中的“关注点分离”和“开闭”原则。