在C#中使用访问者(Visitor)模式对组合(Composite)对象进行验证
在应用程序的开发过程中合理使用设计模式,不仅能够解决实际问题,提高开发效率,而且还能够让程序结构更为清晰合理,对达到“低耦合、高内聚”的设计目的有着很大的帮助。目前网上有很多介绍设计模式的文章,有的也自成体系,基本涵盖了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语句即可完成,比如:
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类的实现代码段,当然,这些代码段仅体现了类之间的关系,此处并没有展示与功能实现相关的其它部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
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三种类型的对象进行操作:
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类型即可),有效、合理地满足了面向对象设计中的“关注点分离”和“开闭”原则。
源程序代码
请至 http://sdrv.ms/11eOzEF 下载本案例的源程序代码。在Visual Studio 2012中打开解决方案文件后,运行Windows Forms主程序OrgMgmt,然后使用“系统 –> 打开”菜单来打开压缩包中的organization.org文件。