MarvellousWorks公司有 A、 B、 C三个部门负责文件的拟稿、审批和备案,现有的流程如下:
2.
3.
4. C部门在接到 B部门传来的文件时,先再发布,然后对其归档
不过, MarvellousWorks的管理层为了加强部门间文件流转的管理,正在酝酿修改工作流程:
1、 增加 D部门专门负责归档, C部门将归档职责划入 D部门后只负责发布文件
2、 C部门发布文件后也须先在 D部门归档
3、 A、 B部门间所有往复流转的中间文件也都报给 D部门归档
请采用本章介绍的 中介者模式及其扩展处理 ,将文件的流转调度过程从 A、 B、 C、 D各对象的职责中独立出来,并用单元测试验证不同的流转过程。
文件对象的定义
class
Document
{
#region
essential fields
public
string
Subject {
get
;
set
; }
public
string
Body {
get
;
set
; }
#endregion
#region
optional fields
public
string
Comment {
get
;
set
; }
#endregion
public
override
string
ToString()
{
return
string
.Format(
"
\n[{0}]\n------------------\n{1}\n({2})\n
"
, Subject, Body, Comment);
}
}
参考答案
分析第一步
上 述A、B、C、D部门间的协作关系比较复杂,而且预期很快会变化,但协作的中间内容很简单——都是文件,所以采用事件方式,由.NET Framework自己的事件机制作为中介者相对很简单,而且类型间的依赖关系全都推给.NET Framework,为以后扩展更多参与方的协作关系提供便利。
据此,我们定义A、B、C、D时全部采用事件作为提供给外中介者协调响应关系的入口。
增加如下类型
class
DocumentEventArgs : EventArgs
{
Document document;
public
DocumentEventArgs(Document document)
{
this
.document
=
document;
}
public
Document Document{
get
{
return
document;}}
}
abstract
class
Department
{
protected
Document document
=
new
Document();
public
Document Document
{
get
{
return
document;}
protected
set
{ document
=
value;}
}
}
分析第二步
如果直接通过事件重载操作符 += 和-=建立各Colleague的响应关系,需要重复编写代码,而且不能在系统上线后将这个工作交给管理员维护。
因此,考虑参考前面的Builder模式,增加一个基于配置动态维护维护事件响应关系的对象。
实现 和单元测试验证
1、验证“分析第一步”的设想
///
<summary>
///
测试手工定义事件中介者的交互关系
///
</summary>
[TestMethod]
public
void
TestManualDefineEventMediatorInSucceedBranch()
{
//
用事件配置松散的响应关系
a1.WriteDocumentFinishedHandler
+=
b1.OnReceiveFileToReview;
b1.ReviewDocumentFailedHandler
+=
a1.OnReviewFailed;
b1.ReviewDocumentSucceedHandler
+=
c1.OnReceiveFileToPublish;
b1.ReviewDocumentSucceedHandler
+=
c1.OnReceiveFileToArchive;
//
成功的路径
a1.Write(
"
a
"
,
"
b
"
,
"
c
"
);
//
验证修订后的内容曾经流转给了B
Assert.AreEqual
<
string
>
(
"
a
"
, b1.Document.Subject);
Assert.AreEqual
<
string
>
(
"
b
"
, b1.Document.Body);
Assert.AreEqual
<
string
>
(
"
c
"
, b1.Document.Comment);
//
验证修订后的内容也曾经流转给了C
Assert.AreEqual
<
string
>
(
"
a
"
, c1.Document.Subject);
Assert.AreEqual
<
string
>
(
"
b
"
, c1.Document.Body);
Assert.AreEqual
<
string
>
(
"
c
"
, c1.Document.Comment);
}
Output窗口
------ Test started: Assembly: Mediator
.
Tests
.
dll ------
A begin write
A write finished
[a]
------------------
b
(
c
)
B received doc from A to review
B begin review
B review succeed
C received doc to publish from B
C published
C received doc to archive from B
C archived
1
passed
,
0
failed
,
0
skipped
,
took
0.50
seconds
(
MSTest
10.0
).
///
<summary>
///
测试手工定义事件中介者的交互关系
///
</summary>
[TestMethod]
public
void
TestManualDefineEventMediatorInFailedBranch()
{
//
用事件配置松散的响应关系
a1.WriteDocumentFinishedHandler
+=
b1.OnReceiveFileToReview;
b1.ReviewDocumentFailedHandler
+=
a1.OnReviewFailed;
b1.ReviewDocumentSucceedHandler
+=
c1.OnReceiveFileToPublish;
b1.ReviewDocumentSucceedHandler
+=
c1.OnReceiveFileToArchive;
//
失败的路径
a1.Write(
"
a
"
,
""
,
""
);
//
验证确实文档曾经流转给了B
Assert.AreEqual
<
string
>
(
"
a
"
, b1.Document.Subject);
Assert.AreEqual
<
string
>
(
""
, b1.Document.Body);
Assert.AreEqual
<
string
>
(
""
, b1.Document.Comment);
//
验证文档并没有流转给C
Assert.IsNull(c1.Document.Subject);
Assert.IsNull(c1.Document.Body);
Assert.IsNull(c1.Document.Comment);
//
修正错误的内容,重新执行流程
a1.Write(
"
a
"
,
"
b
"
,
"
c
"
);
//
验证修订后的内容曾经流转给了B
Assert.AreEqual
<
string
>
(
"
a
"
, b1.Document.Subject);
Assert.AreEqual
<
string
>
(
"
b
"
, b1.Document.Body);
Assert.AreEqual
<
string
>
(
"
c
"
, b1.Document.Comment);
//
验证修订后的内容也曾经流转给了C
Assert.AreEqual
<
string
>
(
"
a
"
, c1.Document.Subject);
Assert.AreEqual
<
string
>
(
"
b
"
, c1.Document.Body);
Assert.AreEqual
<
string
>
(
"
c
"
, c1.Document.Comment);
}
Output窗口
------ Test started: Assembly: Mediator
.
Tests
.
dll ------
A begin write
A write finished
[a]
------------------
()
B received doc from A to review
B begin review
B review failed
A received doc review failed from B
A begin write
A write finished
[a]
------------------
b
(
c
)
B received doc from A to review
B begin review
B review succeed
C received doc to publish from B
C published
C received doc to archive from B
C archived
1
passed
,
0
failed
,
0
skipped
,
took
3.64
seconds
(
MSTest
10.0
).
2、验证“分析第二部” 的设想
定义管理基于事件的中介关系Builder
class
EventMediatorBuilder
{
class
ConfigItem
{
public
Type SourceType {
get
;
set
; }
public
Type TargetType {
get
;
set
; }
public
string
SourceEventName {
get
;
set
; }
public
string
TargetHandlerMethodName {
get
;
set
; }
public
override
bool
Equals(
object
obj)
{
if
(obj
==
null
)
throw
new
ArgumentNullException(
"
obj
"
);
var target
=
(ConfigItem)obj;
return
SourceType
==
target.SourceType
&&
TargetType
==
target.TargetType
&&
string
.Equals(SourceEventName, target.SourceEventName)
&&
string
.Equals(TargetHandlerMethodName, target.TargetHandlerMethodName);
}
}
IList
<
ConfigItem
>
config
=
new
List
<
ConfigItem
>
();
public
EventMediatorBuilder AddConfig(Type sourceType, Type targetType,
string
sourceEventName,
string
targetHandlerMethodName)
{
if
(sourceType
==
null
)
throw
new
ArgumentNullException(
"
sourceType
"
);
if
(targetType
==
null
)
throw
new
ArgumentNullException(
"
targetType
"
);
if
(
string
.IsNullOrEmpty(sourceEventName))
throw
new
ArgumentNullException(
"
sourceEventName
"
);
if
(
string
.IsNullOrEmpty(targetHandlerMethodName))
throw
new
ArgumentNullException(
"
targetHandlerMethodName
"
);
if
(sourceType.GetEvent(sourceEventName)
==
null
)
throw
new
NotSupportedException(sourceEventName);
var item
=
new
ConfigItem()
{
SourceType
=
sourceType,
TargetType
=
targetType,
SourceEventName
=
sourceEventName,
TargetHandlerMethodName
=
targetHandlerMethodName
};
if
(
!
config.Contains(item))
config.Add(item);
return
this
;
}
public
EventMediatorBuilder BuildAUpColleagues(
params
object
[] colleagues)
{
if
(colleagues
==
null
)
throw
new
ArgumentNullException(
"
colleagues
"
);
if
(config.Count()
==
0
)
return
this
;
//
没有通信关系配置项
if
(colleagues.Count()
==
1
)
return
this
;
//
没有需要配置的关联对象组
colleagues.ToList().ForEach(x
=>
{
if
(x
==
null
)
throw
new
ArgumentNullException(); });
///
/ 限制:不支持一类对象的某个实例同时向另一类对象多个实例的通知
//
if (colleagues.GroupBy(x => x.GetType()).Count() != colleagues.Count())
//
throw new NotSupportedException();
foreach
(var item
in
config)
{
var sources
=
colleagues.Where(x
=>
x.GetType()
==
item.SourceType);
if
((sources
==
null
)
||
(sources.Count()
==
0
))
continue
;
var targets
=
colleagues.Where(x
=>
x.GetType()
==
item.TargetType);
if
((targets
==
null
)
||
(targets.Count()
==
0
))
continue
;
var eventInfo
=
item.SourceType.GetEvent(item.SourceEventName);
if
(eventInfo
==
null
)
continue
;
var methodInfo
=
item.TargetType.GetMethod(item.TargetHandlerMethodName, BindingFlags.Public
|
BindingFlags.Instance);
if
(methodInfo
==
null
)
continue
;
//
绑定事件响应关系
foreach
(var source
in
sources)
foreach
(var target
in
targets)
eventInfo.AddEventHandler(source, Delegate.CreateDelegate(eventInfo.EventHandlerType, target, methodInfo));
}
return
this
;
}
}
实现和单元测试验证
using
System;
using
System.Diagnostics;
using
System.Collections.Generic;
using
System.Linq;
using
System.Reflection;
using
Microsoft.VisualStudio.TestTools.UnitTesting;
namespace
MarvellousWorks.PracticalPattern.Mediator.Tests.Exercise
{
[TestClass]
public
class
DocumentWorkflowMediatorFixture
{
Scenario1.A a1;
Scenario1.B b1;
Scenario1.C c1;
Scenario2.A a2;
Scenario2.B b2;
Scenario2.C c2;
Scenario2.D d2;
EventMediatorBuilder scenario1Builder;
EventMediatorBuilder scenario2Builder;
///
<summary>
///
配置不同协调关系
///
实际项目中可以采用本章介绍的基于配置文件的定义方式
///
</summary>
[TestInitialize]
public
void
Initialize()
{
a1
=
new
Scenario1.A();
b1
=
new
Scenario1.B();
c1
=
new
Scenario1.C();
a2
=
new
Scenario2.A();
b2
=
new
Scenario2.B();
c2
=
new
Scenario2.C();
d2
=
new
Scenario2.D();
scenario1Builder
=
new
EventMediatorBuilder()
//
1. A部门拟稿后将文件报B部门审核
.AddConfig(
typeof
(Scenario1.A),
typeof
(Scenario1.B),
"
WriteDocumentFinishedHandler
"
,
"
OnReceiveFileToReview
"
)
//
2. B部门对于文件审核后,确认文件体例没有缺项后就通知C部门发布
.AddConfig(
typeof
(Scenario1.B),
typeof
(Scenario1.C),
"
ReviewDocumentSucceedHandler
"
,
"
OnReceiveFileToPublish
"
)
//
3. 如果B部门发现文件体例有缺项时,将文件返回给A部门重新修改
.AddConfig(
typeof
(Scenario1.B),
typeof
(Scenario1.A),
"
ReviewDocumentFailedHandler
"
,
"
OnReviewFailed
"
)
//
4. C部门在接到B部门传来的文件时,先再发布,然后对其归档
.AddConfig(
typeof
(Scenario1.C),
typeof
(Scenario1.C),
"
DocumentPublishedHandler
"
,
"
OnReceiveFileToArchive
"
);
scenario2Builder
=
new
EventMediatorBuilder()
.AddConfig(
typeof
(Scenario2.A),
typeof
(Scenario2.B),
"
WriteDocumentFinishedHandler
"
,
"
OnReceiveFileToReview
"
)
.AddConfig(
typeof
(Scenario2.A),
typeof
(Scenario2.D),
"
WriteDocumentFinishedHandler
"
,
"
OnReceiveFileToArchive
"
)
.AddConfig(
typeof
(Scenario2.B),
typeof
(Scenario2.A),
"
ReviewDocumentFailedHandler
"
,
"
OnReviewFailed
"
)
.AddConfig(
typeof
(Scenario2.B),
typeof
(Scenario2.D),
"
ReviewDocumentFailedHandler
"
,
"
OnReceiveFileToArchive
"
)
.AddConfig(
typeof
(Scenario2.B),
typeof
(Scenario2.C),
"
ReviewDocumentSucceedHandler
"
,
"
OnReceiveFileToPublish
"
)
.AddConfig(
typeof
(Scenario2.C),
typeof
(Scenario2.D),
"
DocumentPublishedHandler
"
,
"
OnReceiveFileToArchive
"
);
}
///
<summary>
///
测试通过Event Mediator Builder构造现有业务流程下的协作关系
///
</summary>
[TestMethod]
public
void
TestScenario1()
{
//
通过Event Mediator以及配置信息建立三个部门Colleague间的协作关系
//
所有协调关系统一剥离到作为Mediator的.NET事件机制上
scenario1Builder.BuildAUpColleagues(a1, b1, c1);
//
成功的路径
Trace.WriteLine(
"
Succeed path
"
);
a1.Write(
"
a
"
,
"
b
"
,
"
c
"
);
//
失败的路径
Trace.WriteLine(
"
\n\nFailed path
"
);
a1.Write(
"
a
"
,
""
,
""
);
//
修正错误的内容,重新执行流程
Trace.WriteLine(
"
Modified after review failed path
"
);
a1.Write(
"
a
"
,
"
b
"
,
"
c
"
);
}
///
<summary>
///
测试通过Event Mediator Builder构造管理层期望的未来业务流程下的协作关系
///
</summary>
[TestMethod]
public
void
TestScenario2()
{
//
通过Event Mediator以及配置信息建立三个部门Colleague间的协作关系
//
所有协调关系统一剥离到作为Mediator的.NET事件机制上
scenario2Builder.BuildAUpColleagues(a2, b2, c2, d2);
//
成功的路径
Trace.WriteLine(
"
Succeed path
"
);
a2.Write(
"
a
"
,
"
b
"
,
"
c
"
);
//
失败的路径
Trace.WriteLine(
"
\n\nFailed path
"
);
a2.Write(
"
a
"
,
""
,
""
);
//
修正错误的内容,重新执行流程
Trace.WriteLine(
"
Modified after review failed path
"
);
a2.Write(
"
a
"
,
"
b
"
,
"
c
"
);
}
}
}
备注1:现有情景下的A、B、C类型定义
namespace
Scenario1
{
class
A : Department
{
public
event
EventHandler
<
DocumentEventArgs
>
WriteDocumentFinishedHandler;
public
void
Write(
string
subject,
string
body,
string
comment)
{
if
(Document
==
null
)
throw
new
NullReferenceException(
"
Document
"
);
Trace.WriteLine(
"
A begin write
"
);
Document.Subject
=
subject;
Document.Body
=
body;
Document.Comment
=
comment;
Trace.WriteLine(
"
A write finished
"
);
Trace.WriteLine(Document);
if
(WriteDocumentFinishedHandler
!=
null
)
WriteDocumentFinishedHandler(