MVP 依赖链接框架






4.45/5 (9投票s)
2007年10月14日
18分钟阅读

49054

451
一个MVP框架,它通过解析一组依赖规则来生成由一系列Presenter组成的拦截过滤器层。
目录
引言
Model View Presenter (MVP) 是一种将应用程序中各层之间的交互松散耦合的技术。Setter 和构造函数注入的依赖关系使得代码可以独立测试,而不会影响运行时宿主环境。MVP 架构通常由 Presenter、Service、Data 以及可能的其他层组成。MVP 中的典型模式包括一个视图初始化器,它也可以实现一个视图接口。或者,在面向组件的设计中,视图初始化器可以托管多个组件,每个组件都实现视图接口。每个视图都与一个控制视图的 Presenter 绑定。有关 MVP 的更多信息,请参阅我之前关于 DLINQ (LINQ to SQL) 的文章,其中我讨论了 MVP 架构。
何时使用此框架?
您是否曾想过一个场景,即多个 Presenter 处理同一个视图?如果视图初始化器托管了多个视图,并且 Presenter 在初始化状态之间存在依赖关系,该怎么办?还可能存在另一种变体,即多个处理多个视图的 Presenter 可能共享一个模型,部分或全部共享。
它做什么?
框架的核心是一个基于反射的工厂,它使用定义 Presenter 之间依赖关系的基于属性的语义,以两种配置之一设置 Presenter 的链表。Presenter 链实现了两种链传播方式——“责任链”和“拦截过滤器模式”。工厂从目标程序集中加载一组 Presenter,并使用依赖解析算法生成 Presenter 的链表。依赖关系在 Presenter 元数据中定义,形式为定义单向关系(Head、Tail、Middle)和双向关系(ComesAfter
属性)的依赖属性。使用依赖算法按定义的依赖顺序一次构建一个 Presenter 骨架链表。然后,使用缓存的 Presenter 骨架链的元数据为每个请求生成一个 Presenter 链。Presenter 链层的生命周期类似于单个 Presenter 的生命周期。Presenter 链以两种模式处理请求——“Break”(中断)和“Continue”(继续)。在break模式下,只要第一个节点(Presenter)能够处理请求,链就会终止处理并将控制权返回给调用者。Continue 模式下的处理类似于拦截过滤器模式,其中控制权会一直流向 Presenter 链的尾部,即使之前的节点已经处理了请求。
此框架不依赖于宿主环境。它可以在 ASP.NET、Winforms 和其他环境中使用。由于 Presenter 通常(但不强制)打包在单独的程序集中,因此可以通过将新的 Presenter 程序集注入系统或更改 Presenter 实现来动态更改页面的行为。缓存基础结构缓存 Presenter 骨架链,并将此更改拾取并加载新的实现。框架提供了适用于此上下文的 MVP 实现的契约。
用法
WebCacheItemManager<PresenterSkeletonChain> presenterCache =
new WebCacheItemManager<PresenterSkeletonChain>
(presenterChainCacheKey);
WebCacheItemManager<Dictionary<Type, ConstructorInfo>> ctorMetadataCache =
new WebCacheItemManager<Dictionary<Type, ConstructorInfo>>
(ctorMetadataCacheKey);
PresenterFactory.InitializeFactory
(ctorMetadataCache, presenterCache, assembly);
Presenter head = PresenterFactory.BuildPresenterChain(new DummyWebView(),
PresenterChainMode.Continue);
head.HandleRequest();
幕后


引擎部件:元数据
Enums
Position
:表示 Presenter 节点在链中的相对位置Dependency
:内部使用。此enum
表示两个 Presenter 节点之间的二元关系PresenterChainMode
:定义 Presenter 链的行为
PresenterPositionAttribute
- 定义 Presenter 在链中的相对位置(Head、Tail 或 Middle [默认])
- 可选择定义两个节点之间的二元依赖关系(
ComesAfter
属性)
PresenterSkeleton
工厂用于维护 Presenter 链的骨架。该骨架将被缓存,后续调用工厂时将使用缓存的 Presenter 骨架来创建具有视图和链式模式依赖关系的 Presenter 链。
引擎部件:接口
与 MVP 架构基础的依赖注入保持一致,工厂定义了与宿主环境无关的核心接口。
IView
:基本视图标识符。客户端可以定义继承IView
的更专业化的视图接口ICacheItemManager<T>
:定义了一个缓存契约,允许对不同类型的缓存对象进行强类型访问public interface ICacheItemManager<T> { T Get(); void Put(T item); string GetCacheKey(); event CacheInvalidatedEventHandler<T> CacheInvalidated; }
缓存契约定义了一个
CacheInvalidated
事件,类型为CacheInvalidatedEventHandler
,定义如下public delegate void CacheInvalidatedEventHandler<T> (string key, T value);
ICacheItemManager
实现预计在缓存存储无效时触发此事件。IControlStore<V>
和ISessionStore
定义了用户会话和用户界面控件级别的细粒度存储契约。
引擎部件:数据结构
PresenterSkeletonChain
.NET 1.x 数据结构如 ArrayList、Queue 和 Stack 是基于数组的实现。如果反编译 Queue 的 IL,可以明显看出它是以数组实现的。依赖链最好实现为链表。链表中的节点在内存中不必是连续的,这与数组不同。重新排序链表涉及重新排序指向单个节点的指针,而不是像基于数组的链表实现那样在连续内存位置中物理插入和删除值。所以,当我偶然发现 .NET Framework 2 的 System.Collections.Generic
命名空间中的 LinkedList<T>
时,我准备好实现自己的链表。
PresenterSkeletonChain
是与 PresenterChain
中 Presenter 节点的计算依赖顺序对应的基础设施骨架。骨架由工厂使用的依赖解析算法生成,然后缓存。有关 ICacheManager
的 Web 实现,请参阅缓存说明。PresenterSkeletonChain
是无状态的,而 Presenter
链表具有对 IView
实例、链式模式以及其他可能影响其操作的底层模型状态的依赖关系。因此,Presenter
链遵循单元工作模式。由用户定义 Presenter
链的生命周期。我倾向于认为它遵循 Web 应用程序中的请求生命周期以及 Winforms 应用程序中的表单生命周期。
表示器
这代表了从 PresenterSkeletonChain
构建的实际 Presenter 链。它本身不是一个链表,但它指向另一个 Presenter 节点,这些节点共同构成一个单向 Presenter 链表。从这个意义上说,Presenter 链的头部是一个前端控制器,客户端调用链头上的 HandleRequest
。
拦截过滤器、管道、责任链、装饰器……天哪!
基础 Presenter
类根据传递给其构造函数的 PresenterChainMode
参数的值来实现两种链传播模式。在“Break”模式下,它实现了经典的 GOF 责任链模式,这类似于 switch
case 语句的控制执行。从头部开始,链中的每个节点都会检查它是否可以处理执行上下文。如果它无法处理请求,则控制权会传递给链中的下一个节点。当第一个节点处理执行上下文时,控制权会返回给调用者。在“Continue”模式下,Presenter 链实现了一个拦截过滤器模式,其中链中的每个节点都有机会处理执行,即使之前的节点已经处理了请求。与经典的拦截过滤器相比,有一个细微的差别。在我的管道中,每个 Presenter 节点首先检查它是否可以处理执行,然后再处理请求,然后将控制权转移到链中的下一个节点。
虽然这与 ASP.NET 管道架构非常相似,但它却大不相同。在 ASP.NET 管道模型中,请求最终由实现 IHttpHandler
接口的单个端点处理。中间“过滤器”层对请求执行一些专门的功能,但处理只由一个处理器完成。
我还会声称我的设计与 GOF 装饰器模式不同。装饰器的链表可用于实现拦截过滤器模式,其中控制权从外部包装对象线性传递到内部被包装的对象。为了增加您的困惑,我将引入另一个听起来相似但又不完全相同的模式。管道和过滤器是拦截过滤器模式的另一种变体,但主要用于消息队列架构。在这种情况下,过滤器通过管道相互连接,一个过滤器节点的输出被作为输入馈送到下一个过滤器,并使用管道作为状态缓冲机制。在我的链设计中,节点之间没有显式的输入/输出状态语义,尽管有可能链中的所有 Presenter 都作用于同一个模型实例。
在下面的代码中,CanHandleRequest()
和 ProcessRequest()
是在抽象基类 Presenter
中定义的抽象方法。Presenter 链的入口点是 HandleRequest()
方法,该方法由客户端调用。
public virtual void HandleRequest()
{
if (CanHandleRequest())
{
if (ChainMode == PresenterChainMode.Break)
{
ProcessRequest();
_hasProcessedRequest = true;
return;
}
else { ProcessRequest(); _hasProcessedRequest = true; }
}
if(Next != null) Next.HandleRequest();
}
引擎本身:PresenterFactory
此类实现了依赖解析算法,并承担了设置 Presenter 链的大部分繁重工作。客户端通过其两个公共方法——InitializeFactory
和 BuildPresenterChain
——与它进行交互。与 MVP 模式保持一致,ICacheItemManager<Dictionary<Type, ConstructorInfo>>
和 ICacheItemManager<PresenterSkeletonChain>
的依赖关系在 InitializeFactory
方法中被注入到 PresenterFactory
中。Presenter 程序集的依赖关系也通过此方法传递。
InitializeFactory
方法设置 PresenterMetadataCacheManager
和 PresenterChainCacheManager
属性,这些属性在内部将 OnCacheInvalidated<T>
委托连接到 ICacheItemManager<T>
中声明的 CacheInvalidated
事件。
internal static void OnCacheInvalidated<T>(string key, T value)
{
switch (key)
{
case CACHE_KEY_PRESENTER_METADATA:
GetPresenterMetadata();
break;
case CACHE_KEY_PRESENTER_CHAIN:
GetPresenterSkeletonChain();
break;
}
}
BuildPresenterChain
方法应在视图初始化器的生命周期中的“初始化阶段”调用。它接受 IView
依赖关系以及链式模式行为,并返回一个使用缓存的 Presenter 骨架链骨架构建的新 Presenter 链实例。
PresenterFactory:缓存模式和线程安全
以下代码片段显示了在 PresenterFactory
中使用悲观锁定策略实现的线程安全。由于 PresenterSkeletonChain
和元数据已缓存,读取应快速进行,锁的持有时间应尽可能短。读取器(在 GetPresenterXXX
方法中)和写入器(ScanPresenters
方法)锁定 static _syncLock
,这可以防止脏读和竞态条件。此模式通常称为双重检查锁定。
通过扫描 Presenter 程序集来创建 PresenterSkeletonChain
和 Dictionary<Type, ConstructorInfo>
的新实例。仅当缓存查找结果为 null 时才执行此操作。ICacheItemManager<T>
定义了一个缓存失效委托,因此实现者预计在缓存项无效时触发 CacheInvalidated
事件。
internal static Dictionary<Type, ConstructorInfo> GetPresenterMetadata()
{
Dictionary<Type, ConstructorInfo> ctorInfo =
_presenterMetadataCacheMgr.Get();
// Allow only the first thread to update the cache to avoid race condition
// If a second thread is blocked over the double check null lock, it
// waits till the primary thread restores data into the cache
if (ctorInfo == null) {
lock (_syncLock)
{
if (_presenterMetadataCacheMgr.Get() == null) ScanPresenters();
ctorInfo = _presenterMetadataCacheMgr.Get();
}
}
return ctorInfo;
}
internal static PresenterSkeletonChain GetPresenterSkeletonChain()
{
PresenterSkeletonChain list = _presenterChainCacheMgr.Get();
// Allow only the first thread to update the cache to avoid race condition
// If a second thread is blocked over the double check null lock, it
// waits till the primary thread restores data into the cache
if (list == null)
{
lock (_syncLock)
{
if (_presenterChainCacheMgr.Get() == null) ScanPresenters();
list = _presenterChainCacheMgr.Get();
}
}
return list;
}
程序集MVPChainingFactory.Web.dll实现了 MVPChainingFactory
框架(MVPChainingFactory.dll)中定义的接口的 Web 版本。命名空间 MVPChainingFactory.Caching.Web
定义了 WebCacheItemManager<T>
,这是 ICacheItemManager<T>
的 Web 实现。以下代码片段显示了 Put
方法的实现,该方法将项放入缓存。Presenter 包含在单独的程序集中。创建了对 Presenter 程序集的 CacheDependency
,并将其连接到一个实现 CacheItemRemovedCallback
的匿名委托。此机制用于在包含 Presenter 的程序集发生更改时刷新缓存的 PresenterSkeletonChain
。
public void Put(T item)
{
string key = GetCacheKey();
Cache c = HttpRuntime.Cache;
Assembly a = Assembly.Load(CacheDependencyAssembly);
CacheDependency dep = a != null ? new CacheDependency(a.Location) : null;
// if cache item is already present calling Cache.Insert will invoke the
// CacheItemRemovedCallback and this will produce an infinite recursion.
// So insert only if not already present in the cache
if (c[key] == null)
{
c.Insert(key,
item,
dep,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
delegate(string k, object v, CacheItemRemovedReason reason)
{
CacheInvalidatedEventHandler<T> e = _events[_eventKey] as
CacheInvalidatedEventHandler<T>;
T value = v as T;
if (e != null)
e(k, value);
}
);
}
}
当缓存项失效时(由 Presenter 程序集修改触发),匿名委托会生成 CacheInvalidated
事件。WebCacheItemManager<T>
实现 CacheInvalidated
事件如下
public event CacheInvalidatedEventHandler<T> CacheInvalidated
{
add { _events.AddHandler(_eventKey, value); }
remove { _events.RemoveHandler(_eventKey, value); }
}
PresenterFactory:依赖推断机制
依赖关系使用 PresenterPositionAttribute
来表达。如前面“引擎部件:元数据”部分所述,依赖关系可以表示为二元关系或 Presenter 链中的位置。两个核心函数推断这两种类型的依赖关系。
InferRDependentFromPositionMetadata
返回节点的链位置元数据。
internal static Dependency InferRDependentFromPositionMetadata
(PresenterSkeleton node, bool isRNode)
{
PresenterPositionAttribute atr = GetPresenterPositionMetadata
(node.PresenterType);
Position pos = atr == null
? Position.Middle : atr.PresenterChainPosition;
if (pos == Position.Head)
{
node.PresenterPosition = Position.Head;
// if L node is the head, R is a dependent
return isRNode ? Dependency.ComesBefore : Dependency.ComesAfter;
}
else if (pos == Position.Tail)
{
node.PresenterPosition = Position.Tail;
// if L node is the tail, R comes before L
return isRNode ? Dependency.ComesAfter : Dependency.ComesBefore;
}
return Dependency.NotDefined;
}
IsRDependent
推断两个节点之间的二元关系。参数 isRAlreadyPresentInList
用作提示,用于在节点之间没有明确定义的二元关系时定义默认行为。如果结果是 Dependency.ComesAfter
(表示 R
在 L
之后),则 IsRDependent
表示从 L
节点到 R
节点的有向依赖。如果未找到明确的链位置或二元依赖定义,并且 isRAlreadyPresentInList
为 true
,则函数返回 Dependency.ComesBefore
,表示 R
在 L
之前。如果 R
尚未存在于链中且没有明确的依赖关系,则 R
以 FIFO 方式插入到 Presenter 链的末尾。
internal static Dependency IsRDependent
(PresenterSkeleton l, PresenterSkeleton r, bool isRAlreadyPresentInList)
{
// Cannot define dependencies between instances of the same type of
// Presenter
if (l.PresenterType.FullName == r.PresenterType.FullName)
return Dependency.NotDefined;
Dependency d = InferRDependentFromPositionMetadata(r, true);
if (d != Dependency.NotDefined) return d;
d = InferRDependentFromPositionMetadata(l, false);
if (d != Dependency.NotDefined) return d;
PresenterPositionAttribute rAtr =
GetPresenterPositionMetadata(r.PresenterType);
PresenterPositionAttribute lAtr =
GetPresenterPositionMetadata(l.PresenterType);
Type rComesAfter = rAtr == null ? null : rAtr.ComesAfter;
Type lComesAfter = lAtr == null ? null : lAtr.ComesAfter;
if (rComesAfter != null && rComesAfter.FullName ==
l.PresenterType.FullName)
return Dependency.ComesAfter;
if (lComesAfter != null && lComesAfter.FullName ==
r.PresenterType.FullName)
return Dependency.ComesBefore;
return isRAlreadyPresentInList ? Dependency.ComesBefore :
Dependency.ComesAfter;
}
依赖解析:算法
我提出了一个依赖解析算法,该算法部分在 PresenterFactory
中实现,其余部分在 PresenterSkeletonChain
中实现。请参阅下面的“架构”部分,以便更好地理解提供的代码是如何组织的以及我如何将其与客户端代码集成。PresenterFactory
扫描包含一组 Presenter 的目标程序集,并将基于属性的依赖定义提供给依赖解析算法。
相对于一个节点,可以定义两种类型的二元依赖关系
- 参照节点出现在另一个节点之后(Na -> Nr)
- 另一个节点出现在参照节点之后(Nr -> Na)
PresenterFactory
扫描 Presenter 程序集并提取抽象 Presenter
类的任何派生实现。新扫描但尚未链接到链中的 Presenter 首先与当前“头部”进行比较。如果头部是一个依赖项(由 ComesAfter
二元关系定义),则新节点将被附加到头部前面,成为链的新头部,并创建一个从新节点到前一个头部的有向链接。此步骤处理上述第二种类型的依赖关系。为了检查第一种类型,将扫描链的其余部分以查找新插入节点的一个潜在前驱。如果在关于剩余节点集的第一个类型的二元关系,则将该节点放置在新插入节点的前面,并重新调整链接。找到第一个匹配项后无需进一步扫描,因为关系是二元的。如果新节点与当前头部之间没有二元关系,则上述逻辑将应用于头部的后继节点,依此类推。
internal static void ScanPresenters()
{
Dictionary<type, /> metadata = new Dictionary<type, />();
PresenterSkeletonChain presenterList = new PresenterSkeletonChain();
int headCount = 0, tailCount = 0;
lock (_syncLock)
{
Assembly a = Assembly.Load(PresenterAssembly);
foreach (Type t in a.GetTypes())
{
Type baseType = t.BaseType;
if (t.IsClass)
{
while (baseType != typeof(Object) && baseType != null)
{
if (baseType == typeof(Presenter) && baseType.IsAbstract)
{
// Validate PresenterPositionAttribute definitions
Position p = GetPresenterPositionFromAttrib(t);
ValidateHeadOrTailDefinition
(ref headCount, ref tailCount, p);
LinkedListNode current =
ConstructSkeletonNode(t, metadata);
LinkedListNode head = presenterList.First;
if (head == null) presenterList.AddFirst(current);
else
{
if (IsRDependent(current, head, true) ==
Dependency.ComesAfter)
{
presenterList.AddBefore(head, current);
// if current has ComesAfter relationship
//with nodes
// which have no relationship w.r.t head
presenterList.AdjustLinks(current, head);
break;
}
else
presenterList.InsertPresenter
(head, current, false);
}
}
baseType = baseType.BaseType;
}
}
}
// Cache the results
_presenterChainCacheMgr.Put(presenterList);
_presenterMetadataCacheMgr.Put(metadata);
}
}
PresenterSkeletonChain
实现依赖解析算法的核心。
public virtual void InsertPresenter(
LinkedListNode<PresenterSkeleton> referenceNode,
LinkedListNode<PresenterSkeleton> newNode,
bool newNodeBeforeReference)
{
if(newNode.Value.PresenterPosition == Position.Head)
{
base.AddFirst(newNode);
return;
}
if (newNode.Value.PresenterPosition == Position.Tail)
{
base.AddLast(newNode);
return;
}
LinkedListNode<PresenterSkeleton> target = newNodeBeforeReference
? referenceNode.Previous : referenceNode.Next;
Dependency d = Dependency.NotDefined;
if (target != null && ((d = PresenterFactory.IsRDependent
(target, newNode, false))
== Dependency.NotDefined))
{
// ND => L and R nodes have the same presenter type.
// So move to the next
target = target.Next;
d = PresenterFactory.IsRDependent(target, newNode, false);
}
// Recurse till the correct slot is found for the new node
if (target != null && d == Dependency.ComesAfter) {
InsertPresenter(target, newNode, false);
return;
}
if (target != null && d == Dependency.ComesBefore) {
base.AddBefore(target, newNode);
AdjustLinks(newNode, target);
return;
}
// If the presenter chain boundary has been reached
// No need to check dependency for remainder next node set as
// it has already been done
if(newNodeBeforeReference) base.AddBefore(referenceNode, newNode);
else base.AddAfter(referenceNode, newNode);
}
在下面的函数中,lNode
是刚刚添加到 rNode
前面的一个新节点。此函数检查 rNode
右侧的剩余节点集,以查看是否存在关于 lNode
的二元关系。如果存在 ComesAfter
关系,则会重新调整指针。需要注意的是,当 lNode
和 rNode
之间没有定义二元关系时,函数 IsRDependent
表示一个默认关系,即 lNode
出现在 rNode
之前。因此,这种情况不会导致任何链接调整。
public void AdjustLinks(LinkedListNode<PresenterSkeleton> lNode,
LinkedListNode<PresenterSkeleton> rNode)
{
// Head or Tail definitions override binary relationships
if (lNode.Value.PresenterPosition != Position.Middle) return;
LinkedListNode<PresenterSkeleton> curr = rNode.Next;
while (curr != null)
{
// If there is no binary relationship then there is no change
// in the list
// when isRAlreadyPresentInList = true as IsRDependent returns
// ComesBefore
if (PresenterFactory.IsRDependent(curr, lNode, true) ==
Dependency.ComesAfter)
{
base.Remove(curr);
base.AddBefore(lNode, curr);
// no need to continue checking other right nodes as it
// is a binary relationship
break;
}
curr = curr.Next;
}
}
我的算法的复杂度
最佳情况是,当从程序集中扫描的一个新类型没有与 Presenter 链中的当前头部定义明确的关系,但头部的直接后继类型与新类型定义了 ComesAfter
关系时。这种情况将导致在新节点插入到头部之后,每个迭代一次。复杂度涉及每次比较两次,复杂度为 O(n),其中 n 是 Presenter 程序集中的 Presenter 类型数。最坏情况是当没有定义依赖关系时。这会导致在每次迭代中遍历整个链以插入一个节点。因此,最坏情况复杂度为 (1 + 2+ 3 +.... n-1),即 O(n^2) [更准确地说,是 n/2*(n+1)]。
可以通过使用图数据结构和“拓扑排序”算法来实现更高效的依赖解析方法。图中的有向边表示两个顶点之间的依赖关系。该算法涉及构建一个图并从没有入边的节点开始遍历它。运行时间是线性的 [O(V+E),其中 V 是顶点数,E 是边数]。可以使用 QuickGraph 来实现图数据结构和算法。QuickGraph 是流行的 Boost Graph Library 的 .NET 端口。事实上,我已经为 QuickGraph 贡献了传递闭包和压缩图算法实现。稍稍偏离主题,传递闭包是一种对软件开发有深远影响的算法。它被重构工具和静态代码分析所使用。然而,在 MVPChainingFactory
框架中,我在应用我的算法后缓存了依赖列表。另外,当 Presenter 的数量趋于无限大时,运行时会变得很重要,而这种情况在常见的 MVP 应用程序中很少见。我必须承认,我通过提出自己的算法获得了比使用 QuickGraph
更大的满足感,因为这是一个“业余”项目。
架构
下图说明了使用 MVPChainingFactory
框架实现的网站的架构。但是,需要注意的是,MVPChainingFactory
框架可以在 Web 环境以及 WinForms 等其他环境中使用。相关的 DLL(MVPChainingFactory.Web.dll)包含 MVPChainingFactory
框架(MVPChainingFactory.dll)中定义的依赖契约的 Web 实现。网站引用此 DLL。非 Web 客户端可以仅引用核心 MVPChainingFactory.dll 来访问框架。Presenters.dll 包含一组 Presenter,它们实现了 MVPChainingFactory.dll 中定义的抽象 Presenter
类。这些具体类用 PresenterPositionAttribute
标记以表示依赖关系。

关于单元测试
MVP 设计的主要关注点是单元测试。此框架不假定宿主环境。我使用了 Rhinomocks 来模拟单元测试的依赖关系。单元测试不限于公共接口方法。一种测试非公共方法的模式是使用反射从单元测试中调用框架类中的私有或受保护方法。其优点是能够精细控制框架类中的成员可访问性。缺点是重构(重命名)成员的能力,这需要进行搜索和替换操作,而不是依赖于重构工具使用的可靠对象图机制。或者,可以修改框架类的设计,对所有需要单元测试的非公共方法使用 internal
关键字。我通过将框架程序集标记为 InternalsVisibleTo
属性来采用这种方法。
我的单元测试分为两类——“Factory
API”和“Factory 加载顺序”(通过指定 CategoryAttribute
)。前者处理 Factory
API 和功能,后者测试生成的 Presenter 链是否符合单元测试中定义的依赖关系。我遵循两种测试类别中的一个通用模式,即在 setup 方法中创建模拟对象并设置通用期望。我将重点介绍一些有趣的测试。其余部分可以在 MVPChainingFactory.Tests
项目的源代码中找到。
[SetUp]
public void Setup()
{
_rep = new MockRepository();
_mockMetadataCacheManager =
_rep.CreateMock<ICacheItemManager<Dictionary<Type,
ConstructorInfo>>>();
_mockPresenterChainCacheManager =
_rep.CreateMock<ICacheItemManager<PresenterSkeletonChain>>();
// Setup initial expectations for the call to InitializeFactory()
// in [Setup]
// Note - Every [Test] must make a call to InitializeFactory
// (which is required
// to construct the PresenterFactory anyway)
_mockMetadataCacheManager.CacheDependencyAssembly = null;
LastCall.Constraints(Is.Equal(_assemblyName));
_mockMetadataCacheManager.CacheInvalidated += null;
_cmEvt = LastCall.IgnoreArguments().GetEventRaiser();
_mockPresenterChainCacheManager.CacheDependencyAssembly = null;
LastCall.Constraints(Is.Equal(_assemblyName));
_mockPresenterChainCacheManager.CacheInvalidated += null;
_pccmEvt = LastCall.IgnoreArguments().GetEventRaiser();
// Do not put InitializeFactory call here as it would repeat
// the above expectations
}
在上面的代码片段中,我获取了模拟对象上 IEventRaiser
的引用,以便模拟和测试 CacheInvalidated
事件的回调功能,如下所示:
PresenterSkeletonChain p = new PresenterSkeletonChain();
Dictionary<Type, ConstructorInfo> d = new Dictionary<Type,ConstructorInfo>();
using (_rep.Record())
{
Expect.Call(_mockPresenterChainCacheManager.Get()).Return(p);
Expect.Call(_mockMetadataCacheManager.Get()).Return(d);
}
using (_rep.Playback())
{
PresenterFactory.InitializeFactory(_mockMetadataCacheManager,
_mockPresenterChainCacheManager, _assemblyName);
_pccmEvt.Raise(PresenterFactory.CACHE_KEY_PRESENTER_CHAIN, p);
_cmEvt.Raise(PresenterFactory.CACHE_KEY_PRESENTER_METADATA, d);
}
工厂功能测试
下面的测试代码片段检查了我在框架中使用的缓存模式。Get
访问器应首先在缓存中查找,并在命中时返回缓存的实例。PresenterFactory.PresenterSkeletonChain
属性访问器应在未命中时重新扫描程序集并刷新缓存。以下代码测试调用堆栈,并在缓存查找结果为未命中时确保上述“双重检查锁定”模式。对 _mockPresenterChainCacheManager.Get()
的前两次调用模拟了缓存未命中,这会导致扫描 Presenter 程序集。PutSimulator
委托拦截对 _mockPresenterChainCacheManager.Put
的调用,并获取参数的引用。在程序集扫描之后进行的最后一次 Get
调用被模拟为返回虚拟链。
PresenterSkeletonChain dummyChain = new PresenterSkeletonChain();
dummyChain.AddLast(new LinkedListNode<PresenterSkeleton>(
new PresenterSkeleton(typeof(object))));
PresenterSkeletonChain actualHead = null;
using (_rep.Record())
{
// Return null to simulate item not cached scenario
Expect.Call
(_mockPresenterChainCacheManager.Get()).Return(null).Repeat.Twice();
_mockPresenterChainCacheManager.Put(null);
LastCall.Constraints(Is.NotNull()).Do((PutSimulator)
delegate(PresenterSkeletonChain head)
{
actualHead = head;
return;
}
);
_mockMetadataCacheManager.Put(null);
LastCall.Constraints(Is.NotNull());
// The 3rd call to cachemanager Get()
// is executed after ScanPresenters invocation.
// So return a non null value
Expect.Call(_mockPresenterChainCacheManager.Get()).Return(dummyChain);
}
PresenterSkeletonChain ret = null;
using (_rep.Playback())
{
PresenterFactory.InitializeFactory
(_mockMetadataCacheManager, _mockPresenterChainCacheManager,
_assemblyName);
ret = PresenterFactory.PresenterSkeletonChain;
}
Assert.IsNotNull(ret);
工厂加载顺序测试
我制定了一些测试用例,这些用例在表示链依赖关系的复杂性级别上有所不同。每个测试用例都打包到一个单独的 Presenter 存根程序集中,该程序集包含一组存根 Presenter 以及用于表示依赖关系的属性元数据。处理特定用例的单元测试引用相应的 Presenter 存根程序集。“架构”部分描述了逻辑层和交互。
以下是 StubPresentersUsecase1.dll 中定义的用例之一的测试。在此测试中,我将生成的 Presenter 链转换为通用的 List<Type>
,并通过比较索引来验证链中 Presenter 的预期顺序。
[Test]
public void TestPresenterLoadAndOrderingOfUsecase1()
{
/*
* INPUT =
* DEFAULT[TAIL] -> P1 -> P2[AFTER P4] -> P3[HEAD] ->
* P4[AFTER P5] -> P5
*
* EXPECTED OUTPUT =
* P3 -> P1 -> P5 -> P4 -> P2 -> DEFAULT
*/
string _assemblyName = "StubPresentersUsecase1";
int countOfPresenters = 6;
PresenterSkeletonChain p = null;
using (_rep.Record())
{
_mockPresenterChainCacheManager.Put(null);
LastCall.Constraints(Is.NotNull()).Do((PutSimulator)
delegate(PresenterSkeletonChain head)
{
p = head;
return;
}
);
_mockMetadataCacheManager.Put(null);
LastCall.Constraints(Is.NotNull());
}
using (_rep.Playback())
{
PresenterFactory.InitializeFactory
(_mockMetadataCacheManager, _mockPresenterChainCacheManager,
_assemblyName);
PresenterFactory.ScanPresenters();
}
string mesg = "Expected count={0}, actual count={1}";
Assert.IsNotNull(p);
Assert.AreEqual(countOfPresenters, p.Count,
String.Format(mesg, countOfPresenters, p.Count));
// Check the HEAD and TAIL nodes
mesg = "Expected presenter type at position {0}={1}, Actual={2}";
List<Type> orderOfPresenters = ToList(p);
Assert.AreEqual(typeof(P3), orderOfPresenters[0],
String.Format(mesg, 0, typeof(P3), orderOfPresenters[0]));
Assert.AreEqual(typeof(DefaultPresenter), orderOfPresenters[5],
String.Format(mesg, 0, typeof(DefaultPresenter),
orderOfPresenters[5]));
// Check defined dependencies
mesg = "Expected that {0} comes after {1}.
Actual index of {0} = {2} and {1} = {3}";
int l = 0, r = 0;
l = orderOfPresenters.IndexOf(typeof(P4));
r = orderOfPresenters.IndexOf(typeof(P2));
Assert.Less(l, r, String.Format(mesg, typeof(P2), typeof(P4), r, l));
l = orderOfPresenters.IndexOf(typeof(P5));
r = orderOfPresenters.IndexOf(typeof(P4));
Assert.Less(l, r, String.Format(mesg, typeof(P4), typeof(P5), r, l));
}
结论
MVPChainingFactory
框架使用依赖解析算法按定义的依赖顺序生成 Presenter 的链表。该链表形成一个 Presenter 层,其行为可以配置为责任链或拦截过滤器实现。尽管我的依赖解析基础结构的设计围绕一组 Presenter,但该思想可以通过一些代码扩展应用于其他领域。其中一个候选者可能是一个服务层,包含一个按依赖关系排序的服务链表,该服务链表在 Presenter 中使用。