如果你使用过Enterprise Library 2.0 CTP或者使用过Composite UI Application Block,你一定发现它们中间包含了一个ObjectBuilder的程序集。如果你还不知道它是什么、做什么用、它的内部如何实现的话,那么现在就来听听ObjectBuilder(以下简称OB)讲述它的故事吧。
OB顾名思义就是对象创建器,它诞生于微软公司模式与实践小组,从今年5 月开始,几经蜕变,终于形成了如今这个稳定的版本。由于它的表现出色, 目前 被 P&P做为Application Blocks的支持模块,用来负责对象的创建工作。
为什么要用OB? 我们知道,一个对象的创建,最简单的方式就是new一个,而对象销毁的工作在.NET平台上,由CLR的GC负责,正常情况下,不需要我们介入。既然如此,为什么还要专门设计一个庞大的对象创建器呢?别急,听我慢慢道来。当手头的项目越来越大,对象也越来越多,对象之间的交互也越来越复杂,这时,简单的对象创建过程已经渐渐的不能适应这种情况了。比如,对象之间存在着依赖关系,那么对象的创建次序就显得很重要。一个对象被创建,需要哪些条件?在对象销毁方面,也存在的同样的问题,尽管有GC,但是GC收集对象的行为并不是可以预测的。OB就是肩负着这样的使命来到人间,下面我们看看OB究竟是如何实现的,一旦你获得了它内部实现的原理,那么你在你的程序中使用OB将更加的得心应手,哪怕你不使用OB,OB中采用的设计思路和设计模式,也会让你学到许多不可多得的知识。
在讲解OB创建对象的过程之前,我们先来看看OB是管理对象的。(注:OB的大部分设计都是可以扩展的,也就是说如果你觉得某些方面,你的想法比OB更出色,那么你可以用自己设计的部分替换OB的默认实现,要做的就是从OB提供的接口类中派生出你的类。)
定位器(Locator)
在OB中有一个称为定位器的东西,通过定位器,你可以很容易地找到需要的对象。定位器被设计为两种,一种是只读定位器,另一种是读写定位器。他们之间存在的继承的关系,也就是说读写定位器是对只读定位器的扩展。
定位器的结构采用链表结构,每一个节点是一个键值对,用来标识对象的唯一性,使得对象不会被重复创建。定位器的链表结构采用可枚举的接口类来实现,这样我们可以通过一个迭代器来遍历这个链表。同时多个定位器也被串成一个链表。具体地说就是多个定位器组成一个链表,表中的每一个节点是一个定位器,定位器本身又是一个链表,表中保存着多个由键值对组成的对象的节点。
{
//返回定位器中节点的数量
int Count { get; }
//一个指向父节点的引用
IReadableLocator ParentLocator { get; }
//表示定位器是否只读
bool ReadOnly { get; }
bool Contains(object key);
//查询定位器中是否已经存在指定键值的对象,根据给出的搜索选项,表示是否要向上回溯继续寻找。
bool Contains(object key, SearchMode options);
//使用谓词操作来查找包含给定对象的定位器
IReadableLocator FindBy(Predicate<KeyValuePair<object, object>> predicate);
//根据是否回溯的选项,使用谓词操作来查找包含对象的定位器
IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate);
//从定位器中获取一个指定类型的对象
TItem Get<TItem>();
TItem Get<TItem>(object key);
//根据选项条件,从定位其中获取一个指定类型的对象
TItem Get<TItem>(object key, SearchMode options);
//给定对象键值获取对象的非泛型重载方法
object Get(object key);
//给定对象键值带搜索条件的非泛型重载方法
object Get(object key, SearchMode options);
}
SerachMode选项的定义:
public enum SearchMode
{
Up, //向上回溯
Local //只查询当前位置
}
public IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate) { if (predicate == null) throw new ArgumentNullException("predicate"); if (!Enum.IsDefined(typeof(SearchMode), options)) throw new ArgumentException(Properties.Resources.InvalidEnumerationValue, "options"); Locator results = new Locator(); IReadableLocator currentLocator = this; while (currentLocator != null) { FindInLocator(predicate, results, currentLocator); currentLocator = options == SearchMode.Local ? null : currentLocator.ParentLocator; } return new ReadOnlyLocator(results); }
|
这段代码表达的意思清晰:如果没有指定谓词操作则抛出异常,如果给出的搜索选项没有在SearchMode 中定义,也抛出异常(这个地方值得 学习 ,按理说 SearchMode只定义了两个选项,怎么会没有定义呢?其实不然,如果你传递的是一个整型常量并Cast到SearchMode,那么就有可能超出SearchMode的定义,所以这个检测语句就有必要存在了。)之后是一个循环,调用FindInLocator的一个私有方法,如果查询选项是只查找当前定位器,那么循环终止,否则沿着定位器的父定位器继续向上查找。FindInLocator方法(代码略)就是遍历定位器,然后把找到的对象存入一个临时的定位器。最后返回一个只读定位器的新的实例,返回的实例中包含了所有满足谓词操作条件的对象。代码中的Locator和ReadOnlyLocator都是定位器的派生类,后面我们会提到它们。FindBy方法会一直查找所有满足条件的对象,如果我们只要找到一个对象就返回的话,那么使用Get方法,这从Get方法的代码可以看到。
从这个抽象基类中派生出一个具体类和一个抽象类,一个具体类是只读定位器(ReadOnlyLocator),这就是为什么我们没有在ReadableLocator中看到对象写入定位器的方法的原因。只读定位器实现抽象基类没有实现的方法,它封装了一个实现了IReadableLocator接口的定位器(不一定就是只读了)然后屏蔽内部定位器的写入接口方法。另一个继承的是读写定位器抽象类,为了实现定位器的写入,定义了一个对IReadableLocator接口扩展的接口叫做IReadWriteLocator:
public interface IReadWriteLocator : IReadableLocator
{
void Add(object key, object value); //保存对象到定位器
bool Remove(object key); //从定位器中删除一个对象,如果成功返回真,否则返回假
}
扩展的接口很简单,它提供了对定位操作的方法。有了这个接口,ReadWriteLocator就有义务实现这个扩展(这里是虚拟方法,具体方法让它的派生类去实现,这样不同的派生类就可以完成对定位器不同的写入方法),因为它从ReadableLocator派生,其他方法继承自ReadableLocator,只要把ReadOnly属性设置为False即可。
从ReadWirteLocator派生的具体类是Locator类,Locator类必须实现一个定位器的全部功能,对照ReadOnlyLocator,在那里只是对一个定位器的封装,而Locator必须有具体的存储结构,这个结构就是WeakRefDictionary类。
WeakRefDictionary
从名称看,这是一个保存弱引用对象的字典结构。弱引用对象是相对于强引用对象而言的,一个强引用对象指的是一个对象实例被检测到没有任何对象对它进行引用的时候,它就被GC回收,此时对象将不能访问。当一个对象被声明为弱引用对象,在对象死亡之后,虽然也无法阻止GC收集,但是在被收集之前,可以通过引用弱对象的目标对象从而使之可以复活。WeakRefDictionary采用对象适配器设计模式,把一个Directory泛型集合对象适配成符合存储弱引用对象的集合,这个类中一个看点就是对弱引用对象的操作方法:
public bool TryGet(TKey key, out TValue value) { value = default(TValue); WeakReference wr; if (!inner.TryGetValue(key, out wr)) return false; object result = wr.Target; if (result == null) { inner.Remove(key); return false; } value = DecodeNullObject<TValue>(result); return true; } |
TryGet方法首先从集合中获取弱引用对象,如果不存在,返回假。然后重新试图激活目标对象,如果目标对象为null, 说明已经被回收,也就是说目标对象已经死亡,那么就从集合中删除它,因为它不可能再被访问了。
private TObject DecodeNullObject<TObject>(object innerValue) { if (innerValue == typeof(NullObject)) return default(TObject); else return (TObject)innerValue; } private object EncodeNullObject(object value) { if (value == null) return typeof(NullObject); else return value; }
|
上面的两个方法,对空对象进行处理,使用一个NullObject的空类作为替换对象,以保证对象不被GC回收。
GetEnumerator方法中的代码返回的是有效的对象。同时为了得到正确的对象数目,调用Count之前,调用CleanAbandonedItems方法来清除死对象。
private void CleanAbandonedItems() { List<TKey> deadKeys = new List<TKey>(); foreach (KeyValuePair<TKey, WeakReference> kvp in inner) if (kvp.Value.Target == null) deadKeys.Add(kvp.Key); foreach (TKey key in deadKeys) inner.Remove(key); } |
CleanAbandonedItems的代码也很清楚,通过判断弱对象引用的目标对象是否为null,将废弃对象收集到一个临时的表,最后统一清除。
回到Locator,因为有了WeakRefDictionary,定位器使用它作为内部的存储结构,实现对象的缓冲机制。
至此,定位器的故事就暂告一段落了。从定位器的设计,我们可以学到很多东西,比如接口和抽象基类的使用、类的继承和组合。顺便说一句,整个定位器采用了.NET版的组合设计模式。如果你还不熟悉什么是组合设计模式,请阅读有关资料。