【渐进】延迟加载机制的简易实现(上)
我们在软件设计中常遇到这样一种场景,一个对象的某个成员,它的加载很耗时,然而又不是总是需要,因为我们不希望它在对象初始化的时候就被加载,而是在它被显示使用时才去加载。
我们总是建议暴露属性而不是成员,作用之一便是本文的主题"延迟加载", 属性的get,set使得我们能够主动的控制成员的加载。
- public class Test
- {
- private string _property;
- public string Property
- {
- get { return this._property; }
- set { this._property = value; }
- }
- }
以上就是最基本的属性写法,.net3.0之后,给了如下的语法糖:
- public string Property { get; set; }
然而很多时候,情况远远要复杂的多,于是有了如下的实现:
- private object _property;
- public object Property
- {
- get
- {
- if (this._property == null)
- this._property = new object();
- return this._property;
- }
- }
当一个对象足够变得复杂后,上述代码更加使得代码臃肿而充斥大量重复的判断。作为程序员的我们总是有那么一股意识,把重复的抽取出来。于是我们隐约有了一个想法,是不是可以设计一个自动的延迟加载机制来代替这种写法?判断是重复的,实际的加载方法也是早已存在的,那么这个机制要做的就是自动的为对象加载某个成员的数据,而无需去重复的编码判断是不是已经加载或是未被加载。
熟悉ORM的朋友对于延迟加载一定不陌生,多数ORM框架均提供了优秀的延迟加载机制。
- [ActiveRecord("C_Article")]
- public class Article : ActiveRecordBase<Article>
- {
- [HasMany(typeof(Tag), Lazy = true)]
- public IList TagList { get; set; }
- }
- using (new SessionScope())
- {
- Article article = Article.Find(1);
- Response.Write(article.TagList.Count);
- }
上述代码利用Castel的ActiveRecord框架展示了一个延迟加载的调用方式:一个特性(HasMany),一个范围声明(SessionScope),就完成了加载,这就是本文将尝试的设计。本文将先尝试以较简易的方式实现以便它可以立刻开始工作,后续篇幅将逐步完善这个设计。
设计之前,先确定一下将要设计的机制的大致使用:不使用代理,AOP等思想对原有类做拦截,不引入特性做为标记,当用户需要使用延迟加载的属性时,需在当前代码上下文声明一个非延迟加载区域,形如SessionScope。
首先,这里所设计的机制不同于ORM框架,这里的数据并非源于数据库,所以将由用户来实现正常的数据加载接口:
- /// <summary>
- /// 加载接口
- /// </summary>
- /// <typeparam name="TOut">加载的类型</typeparam>
- /// <typeparam name="TIn">启用此加载的对载类型</typeparam>
- public interface ILoader<TOut, TIn>
- {
- /// <summary>
- /// 正常加载
- /// </summary>
- /// <param name="t"></param>
- /// <returns></returns>
- TOut Load(TIn t);
- }
用户需要实现如上接口来完成正常的数据加载。
接下来,需要设计一个非延迟加载区域 NotLazyScope:
- /// <summary>
- /// 延迟加载区域
- /// </summary>
- public class NotLazyScope : IDisposable
- {
- /// <summary>
- /// 延迟标记槽
- /// </summary>
- private LocalDataStoreSlot _slot;
- /// <summary>
- /// 延迟标记槽名称
- /// </summary>
- public static readonly string SLOT_NAME = "NotLazy";
- /// <summary>
- /// 声明延迟加载区域
- /// </summary>
- public NotLazyScope()
- {
- this._slot = Thread.GetNamedDataSlot(SLOT_NAME);
- Thread.SetData(this._slot, true);
- }
- /// <summary>
- /// 结束?延?加?区域声明
- /// </summary>
- public void End()
- {
- Thread.SetData(this._slot, false);
- }
- #region IDisposable 成员
- void IDisposable.Dispose()
- {
- this.End();
- }
- #endregion
- }
为了实现如SessionScope的语法声明方式,我们需要在当前的上下文记录一些标记,这里利用了线程栈的数据槽来实现。
接着要设计主要的加载器,作为这个机制的调用入口:
- /// <summary>
- /// 一级缓存容器
- /// </summary>
- internal class PrimaryCache : Dictionary<object, Dictionary<string, object>> { }
以上将作为一个缓存容器,后面会发掘出其他用途。
- /// <summary>
- /// 延?加?器
- /// </summary>
- public class LazyLoader
- {
- /// <summary>
- /// 一级缓存
- /// </summary>
- private static PrimaryCache _pool = new PrimaryCache();
- /// <summary>
- /// 执?延?加?
- /// ?声明了NotLazyScope?在声明有效?围内将执??延?加?
- /// </summary>
- /// <typeparam name="TIn">执?加?的对?类型</typeparam>
- /// <typeparam name="TOut">加?的类型</typeparam>
- /// <param name="loader">加?所使用的接口实例</param>
- /// <param name="t">执?加?的对?实例</param>
- /// <param name="property">延?加?的属性名称</param>
- /// <returns>?回上一次加?结果</returns>
- public static TOut Load<TIn, TOut>(ILoader<TOut, TIn> loader, TIn t, string property)
- where TOut : class, new()
- where TIn : class, new()
- {
- if (Exist<TIn>(t, property))
- return _pool[t][property] as TOut;
- lock (_pool)
- {
- if (!Exist<TIn>(t, property))
- {
- if (Lazy())
- return default(TOut);
- TOut result = loader.Load(t);
- if (!_pool.ContainsKey(t))
- _pool.Add(t, new Dictionary<string, object>());
- _pool[t].Add(property, result);
- }
- }
- return Lazy() ? default(TOut) : loader.Load(t);
- }
- /// <summary>
- /// 是否存在缓存
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="t"></param>
- /// <param name="property"></param>
- /// <returns></returns>
- private static bool Exist<TIn>(TIn t, string property)
- {
- return _pool.ContainsKey(t) && _pool[t].ContainsKey(property);
- }
- /// <summary>
- /// 是否延?
- /// </summary>
- /// <returns></returns>
- private static bool Lazy()
- {
- return !Convert.ToBoolean(Thread.GetData(Thread.GetNamedDataSlot(NotLazyScope.SLOT_NAME)));
- }
- }
完成了以上这些就足够完成本文的设计了。
要想真正使用,还需要先来实现一个ILoader接口:
- /// <summary>
- /// 好友加?器
- /// </summary>
- internal class UserFriendLoader : ILoader<List<long>, UserInfoEN>
- {
- #region ILoader<List<long>,UserInfoEN> 成员
- public List<long> Load(UserInfoEN t)
- {
- return UserBO.GetFriendUserIdList(t.UserId);
- }
- #endregion
- }
现在可以来尝试使用它了:
- return Lazy.LazyLoader.Load<UserInfoEN, List<long>>(new Lazy.UserFriendLoader(), this, "FriendUserIdList");
- using (new Lazy.NotLazyScope())
- {
- //...
- }
上面的代码完成了这个简易设计,能方便的让你在现有项目中引入这个机制而不用做大幅度改动。不过对于上述设计所提供的调用,可能会让您觉得使用起来比较麻烦,或许您更倾向于简单的使用类似特性的语法对属性进行延迟加载的声明,在接下来的篇幅将一步步尝试对设计进行改造,让它的调用更友好,设计更合理。
从延迟机制的实现衍生:您可能会注意到上述实现中,对于延迟加载的属性,当实际加载被执行一次后,便无需再次执行就直接从缓存中获得,基于此可衍生出一个缓存机制,如果您熟悉NHibernate,想必会联想到NH的多级缓存设计,对了,这里便暗合了这种设计,只不过现在这个机制不是基于数据库了,从这个缓存出发,便可扩展出延迟加载机制的一级缓存。