为了账号安全,请及时绑定邮箱和手机立即绑定

在C#中使用Visitor 模式对Composite对象进行验证

标签:
C#

背景

某公司打算开发一套企业管理软件,于是,企业的组织结构管理就成为了这个软件的重要功能之一。组织结构管理系统所涵盖的功能还是比较多的,而且还会与系统的其它部分产生紧密的联系,比如审批流程、成本核算等等都与企业组织结构紧密相关。为了配合本文的描述,我们尽可能地简化这部分功能,只将功能限定在组织结构的建立、维护和验证上,基本需求包括:

  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语句即可完成,比如:

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类的实现代码段,当然,这些代码段仅体现了类之间的关系,此处并没有展示与功能实现相关的其它部分。


SNAGHTMLfd00ef
接下来,我们会进一步丰富这些类,并逐渐完善应用程序的一些基本功能,比如:向用户提供一个Windows Forms的应用界面,在树形视图控件(TreeView)中对整个组织结构进行维护,并向用户提供新建、打开和保存组织结构的功能。这个过程其实还包含了很多C#/Windows Forms应用程序开发的知识点,但这些都不是本文所要讨论的内容,因此我将跳过对这些细节内容的介绍。在完成了这些基本功能的开发后,我们的应用程序大致如下图所示:

下面,我们需要向应用程序添加验证功能。为了简单起见,此处我们仅实现以下验证逻辑:

  1. 同级别中不存在同名的部门或职员

  2. 部门名称不能为空

  3. 职员的姓、名不能为空

  4. 职员的电子邮件不能为空,并应符合电子邮件地址格式

  5. 职员的电话号码不能为空,并应符合电话号码的格式

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

整个设计的完整类图如下所示:

image

图中OrganizationValidator就是一个IVisitor接口的实现,在三个Visit的重载方法中,分别完成了对Organization、Department和Employee的验证逻辑。此处就不详述其实现代码了,读者请参考本文附带的源程序代码来了解这个类的具体实现。

效果

在当前的Windows Forms应用程序中添加上基于Visitor模式实现的验证逻辑以后,就可以在任意层级的节点上,单击鼠标右键并选择“验证”菜单项来触发验证逻辑。以下是在添加了一个新的职员信息后,在整个组织结构上进行数据验证的结果,应用程序提示该职员的电子邮件地址格式不正确,以及电话号码不能为空:

image

总结

本文通过一个实际案例展示了在应用程序开发过程中实现组合(Composite)模式和访问者(Visitor)模式的方式,综上所述,Visitor模式在扩展已有对象结构的操作上,显得很有优势。这种扩展与类型继承的方式有着本质的区别。通过类型继承可以在原类型上增加新的字段和方法,从而达到行为扩展的目的;但从面向对象的角度来看,有些行为又本不应该属于这些对象,比如本案例中的验证功能,它本不应该是组织结构模型的一种行为(组织结构对象不可能自己验证自己),而是应用程序为组织结构提供的一种附加功能。Visitor模式很好地把这些行为的实现与对象结构分离,使得应用程序可以在不改变对象结构和现有行为的基础上,为之提供新的行为实现(比如,在本案例中如果还需要实现整个组织结构的某项数据统计功能,那么只需要再实现一个OrganizationCounterVisitor类型即可),有效、合理地满足了面向对象设计中的“关注点分离”和“开闭”原则。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消