public class CachedRepository : IRepository { IList<string> IRepository.GetLanguages() { Debug.WriteLine("Enter IRepository.GetLanguages()..."); ObjectCache cache = MemoryCache.Default; IList<string> item = cache.Get("GetLanguages") as IList<string>; if (item == null) { Debug.WriteLine("Cache key GetLanguages does not exists"); IList<string> languages = Database.GetLanguages(); cache.Set("GetLanguages", languages, new CacheItemPolicy()); return languages; } Debug.WriteLine("Cache key GetLanguages exists"); return item; } IList<Setting> IRepository.GetSettings() { Debug.WriteLine("Enter IRepository.GetSettings()..."); ObjectCache cache = MemoryCache.Default; IList<Setting> item = cache.Get("GetSettings") as IList<Setting>; if (item == null) { Debug.WriteLine("Cache key GetSettings does not exists"); IList<Setting> settings = Database.GetSettings(); cache.Set("GetSettings", settings, new CacheItemPolicy()); return settings; } Debug.WriteLine("Cache key GetSettings exists"); return item; } }可以看到在需要cache的地方,我們就必須宣告ObjectCache物件,檢查相對應的cache是否已存在,若不存在則從資料庫取得資料並儲存在cache裡,存在的話則直接回傳cache物件。如果需要加入cache的方法越來越多,則類似的程式便需要重覆撰寫在各個方法中,造成不少code duplication。除此之外,處理cache的程式碼也會和資料存取的程式碼混合在一起而降低可讀性。AOP就是為了解決前述問題,提高程式碼可讀性並將不相關的程式邏輯切割開來,達到SoC (Separation of Concerns)的概念。
在引入AOP後,Repository類別將不再包含存取cache的程式碼,如
public class Repository : IRepository { IList<string> IRepository.GetLanguages() { Debug.WriteLine("Enter IRepository.GetLanguages()..."); return Database.GetLanguages(); } IList<Setting> IRepository.GetSettings() { Debug.WriteLine("Enter IRepository.GetSettings()..."); return Database.GetSettings(); } }這樣是不是很簡潔?但存取cache的程式碼要寫到哪呢?
在這邊要借助Castle Project裡的Castle DynamicProxy來實作AOP機制。首先先透過nuget取得Castle Project的核心函式庫。執行以下指令
Install-Package Castle.Core
DynamicProxy主要是以Proxy Pattern來實作代理類別,在run-time時將存取cache的程式碼注入至原始類別的方法中,如上述的Repository類別。當用戶端存取Repository類別時,實際上存取到的是DynamicProxy產生的代理類別,也就是加料過的Repository類別。
要讓DynamicProxy能注入程式碼至目標類別裡,只要實作它所提供的IInterceptor介面即可,而IInterceptor介面只有一個名為Intercept的方法。
public class CacheInterceptor : IInterceptor { public void Intercept(IInvocation invocation) { Debug.WriteLine("Enter interceptor..."); Debug.WriteLine("Invoke {0}.{1}", invocation.Method.DeclaringType.FullName, invocation.Method.Name); string key = string.Format("{0}.{1}", invocation.Method.DeclaringType.FullName, invocation.Method.Name); ObjectCache cache = MemoryCache.Default; object item = cache.Get(key); if (item == null) { invocation.Proceed(); Debug.WriteLine("Set cache..."); cache.Set(key, invocation.ReturnValue, new CacheItemPolicy()); return; } Debug.WriteLine("Return cache..."); invocation.ReturnValue = item; Debug.WriteLine("Leave interceptor..."); } }接下來只要宣告ProxyGenerator物件並加入CacheInterceptor類別即可。
[TestMethod] public void Cache_With_AOP() { ProxyGenerator proxyGenerator = new ProxyGenerator(); IRepository repository = new Repository(); IRepository repositoryProxy = proxyGenerator.CreateInterfaceProxyWithTarget<IRepository>(repository, new CacheInterceptor()); Debug.WriteLine("First run..."); IList<string> languages = repositoryProxy.GetLanguages(); Assert.AreEqual<string>("English", languages[0]); Assert.AreEqual<string>("Japanese", languages[1]); Assert.AreEqual<string>("Simplified Chinese", languages[2]); Assert.AreEqual<string>("Traditional Chinese", languages[3]); Debug.WriteLine("Second run..."); IList<string> cachedLanguages = repositoryProxy.GetLanguages(); Assert.AreEqual<string>("English", cachedLanguages[0]); Assert.AreEqual<string>("Japanese", cachedLanguages[1]); Assert.AreEqual<string>("Simplified Chinese", cachedLanguages[2]); Assert.AreEqual<string>("Traditional Chinese", cachedLanguages[3]); }可以看到在第一次呼叫Repository物件時,程式還會執行到GetLanguages方法,且cache不存在所以建立新cache,但第二次呼叫GetLanguages方法時,cache則直接被回傳。
到這裡已經達到以AOP的方式來實作cache存取機制,但要注意的是CacheInterceptor要記得做例外處理。若Intercept方法內出現了例外,程式不應該因此而中斷,而必須將例外攔截,並執行原本Repository類別內的GetLanguages方法。
完整程式碼可參考https://github.com/petekcchen/blog/tree/master/AOPCaching