【漸進】延遲載入機制的簡易實現(上)

iDotNetSpace發表於2009-10-26

我們在軟體設計中常遇到這樣一種場景,一個物件的某個成員,它的載入很耗時,然而又不是總是需要,因為我們不希望它在物件初始化的時候就被載入,而是在它被顯示使用時才去載入。

      我們總是建議暴露屬性而不是成員,作用之一便是本文的主題"延遲載入", 屬性的get,set使得我們能夠主動的控制成員的載入。

      
  1. public class Test
  2. {
  3.     private string _property;
  4.  
  5.     public string Property
  6.     {
  7.         get { return this._property; }
  8.         set { this._property = value; }
  9.     }
  10. }

      以上就是最基本的屬性寫法,.net3.0之後,給瞭如下的語法糖:

  1. public string Property { get; set; }

      然而很多時候,情況遠遠要複雜的多,於是有了如下的實現:

  1. private object _property;
  2.  
  3.         public object Property
  4.         {
  5.             get
  6.             {
  7.                 if (this._property == null)
  8.                     this._property = new object();
  9.                 return this._property;
  10.             }
  11.         }

      當一個物件足夠變得複雜後,上述程式碼更加使得程式碼臃腫而充斥大量重複的判斷。作為程式設計師的我們總是有那麼一股意識,把重複的抽取出來。於是我們隱約有了一個想法,是不是可以設計一個自動的延遲載入機制來代替這種寫法?判斷是重複的,實際的載入方法也是早已存在的,那麼這個機制要做的就是自動的為物件載入某個成員的資料,而無需去重複的編碼判斷是不是已經載入或是未被載入。

      熟悉ORM的朋友對於延遲載入一定不陌生,多數ORM框架均提供了優秀的延遲載入機制。

      
  1. [ActiveRecord("C_Article")]
  2. public class Article : ActiveRecordBase<Article>
  3. {
  4.     [HasMany(typeof(Tag), Lazy = true)]
  5.     public IList TagList { get; set; }
  6. }

     

  1. using (new SessionScope())
  2. {
  3.     Article article = Article.Find(1);
  4.     Response.Write(article.TagList.Count);
  5. }

 

     上述程式碼利用Castel的ActiveRecord框架展示了一個延遲載入的呼叫方式:一個特性(HasMany),一個範圍宣告(SessionScope),就完成了載入,這就是本文將嘗試的設計。本文將先嚐試以較簡易的方式實現以便它可以立刻開始工作,後續篇幅將逐步完善這個設計。

      設計之前,先確定一下將要設計的機制的大致使用:不使用代理,AOP等思想對原有類做攔截,不引入特性做為標記,當使用者需要使用延遲載入的屬性時,需在當前程式碼上下文宣告一個非延遲載入區域,形如SessionScope。

      首先,這裡所設計的機制不同於ORM框架,這裡的資料並非源於資料庫,所以將由使用者來實現正常的資料載入介面:

      
  1. ///
  2. /// 載入介面
  3. ///
  4. /// 的型別
  5. /// 啟用此載入的對載型別
  6. public interface ILoader
  7. {
  8.     ///
  9.     /// 正常載入
  10.     ///
  11.     ///
  12.     ///
  13.     TOut Load(TIn t);
  14. }

      使用者需要實現如上介面來完成正常的資料載入。

      接下來,需要設計一個非延遲載入區域 NotLazyScope:

      
  1. ///
  2. /// 延遲載入區域
  3. ///
  4. public class NotLazyScope : IDisposable
  5. {
  6.     ///
  7.     /// 延遲標記槽
  8.     ///
  9.     private LocalDataStoreSlot _slot;
  10.     ///
  11.     /// 延遲標記槽名稱
  12.     ///
  13.     public static readonly string SLOT_NAME = "NotLazy";
  14.     ///
  15.     /// 宣告延遲載入區域
  16.     ///
  17.     public NotLazyScope()
  18.     {
  19.         this._slot = Thread.GetNamedDataSlot(SLOT_NAME);
  20.         Thread.SetData(this._slot, true);
  21.     }
  22.     ///
  23.     /// 結束?延?加?區域宣告
  24.     ///
  25.     public void End()
  26.     {
  27.         Thread.SetData(this._slot, false);
  28.     }
  29.  
  30.     #region IDisposable 成員
  31.  
  32.     void IDisposable.Dispose()
  33.     {
  34.         this.End();
  35.     }
  36.  
  37.     #endregion
  38. }

      為了實現如SessionScope的語法宣告方式,我們需要在當前的上下文記錄一些標記,這裡利用了執行緒棧的資料槽來實現。

      接著要設計主要的載入器,作為這個機制的呼叫入口:

     

  1. ///
  2. /// 一級快取容器
  3. ///
  4. internal class PrimaryCache : Dictionary<object, Dictionary<string, object>> { }

 

      以上將作為一個快取容器,後面會發掘出其他用途。

      
  1. ///
  2. /// 延?加?器
  3. ///
  4. public class LazyLoader
  5. {
  6.     ///
  7.     /// 一級快取
  8.     ///
  9.     private static PrimaryCache _pool = new PrimaryCache();
  10.     ///
  11.     /// 執?延?加?
  12.     /// ?宣告瞭NotLazyScope?在宣告有效?圍內將執??延?加?
  13.     ///
  14.     /// ?加?的對?型別
  15.     /// ?的型別
  16.     /// ?所使用的介面例項
  17.     /// ?加?的對?例項
  18.     /// ?加?的屬性名稱
  19.     /// ?回上一次加?結果
  20.     public static TOut Load(ILoader loader, TIn t, string property)
  21.         where TOut : class, new()
  22.         where TIn : class, new()
  23.     {
  24.         if (Exist(t, property))
  25.             return _pool[t][property] as TOut;
  26.  
  27.         lock (_pool)
  28.         {
  29.             if (!Exist(t, property))
  30.             {
  31.                 if (Lazy())
  32.                     return default(TOut);
  33.  
  34.                 TOut result = loader.Load(t);
  35.  
  36.                 if (!_pool.ContainsKey(t))
  37.                     _pool.Add(t, new Dictionary<string, object>());
  38.                 _pool[t].Add(property, result);
  39.             }
  40.         }
  41.  
  42.         return Lazy() ? default(TOut) : loader.Load(t);
  43.     }
  44.     ///
  45.     /// 是否存在快取
  46.     ///
  47.     ///
  48.     ///
  49.     ///
  50.     ///
  51.     private static bool Exist(TIn t, string property)
  52.     {
  53.         return _pool.ContainsKey(t) && _pool[t].ContainsKey(property);
  54.     }
  55.     ///
  56.     /// 是否延?
  57.     ///
  58.     ///
  59.     private static bool Lazy()
  60.     {
  61.         return !Convert.ToBoolean(Thread.GetData(Thread.GetNamedDataSlot(NotLazyScope.SLOT_NAME)));
  62.     }
  63. }

      完成了以上這些就足夠完成本文的設計了。

      要想真正使用,還需要先來實現一個ILoader介面:

      
  1. ///
  2. /// 好友加?器
  3. ///
  4. internal class UserFriendLoader : ILoader<List<long>, UserInfoEN>
  5. {
  6.  
  7.     #region ILoader,UserInfoEN> 成員
  8.  
  9.     public List<long> Load(UserInfoEN t)
  10.     {
  11.         return UserBO.GetFriendUserIdList(t.UserId);
  12.     }
  13.  
  14.     #endregion
  15. }

      現在可以來嘗試使用它了:     

     

  1. return Lazy.LazyLoader.Load<UserInfoEN, List<long>>(new Lazy.UserFriendLoader(), this, "FriendUserIdList");

 

     

  1. using (new Lazy.NotLazyScope())
  2. {
  3.     //...
  4. }

 

    

     上面的程式碼完成了這個簡易設計,能方便的讓你在現有專案中引入這個機制而不用做大幅度改動。不過對於上述設計所提供的呼叫,可能會讓您覺得使用起來比較麻煩,或許您更傾向於簡單的使用類似特性的語法對屬性進行延遲載入的宣告,在接下來的篇幅將一步步嘗試對設計進行改造,讓它的呼叫更友好,設計更合理。

     

      從延遲機制的實現衍生:您可能會注意到上述實現中,對於延遲載入的屬性,當實際載入被執行一次後,便無需再次執行就直接從快取中獲得,基於此可衍生出一個快取機制,如果您熟悉NHibernate,想必會聯想到NH的多級快取設計,對了,這裡便暗合了這種設計,只不過現在這個機制不是基於資料庫了,從這個快取出發,便可擴充套件出延遲載入機制的一級快取。

 

作者:wsky (huangxu)
出處:http://wsky.cnblogs.com/
以上文字若無註明轉載字樣則為個人原創,轉載請保留簽名  

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-617463/,如需轉載,請註明出處,否則將追究法律責任。

相關文章