February 23, 2014

以Castle DynamicProxy實現AOP Caching機制

AOP (Aspect-Oriented Programming)是一種用來解決Cross-Cutting Concern混雜在業務邏輯程式碼的一種技術。常見的Cross-Cutting Concern有Logging, Caching, Validation等。以下程式碼在實作cache時相當常見
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

No comments: