November 23, 2014

使用Unity實作AOP Logging

目前專案某個函式庫裡有支base class使用RestSharp來發送HTTP request,並使用Common.Logging掛上log4net來實作log機制。按照現行設計,我們是透過constructor injection的方式把RestSharp的IRestClient介面inject到base class。如
protected ApiProxyBase(IRestClient restClient)
{
    Client = restClient;
}

protected IRestClient Client { get; private set; }

protected ILog Log = LogManager.GetLogger("Request");
Common.Logging則是直接在base class使用,並未採用constructor injection,主要因為這麼做不會對unit test造成太大影響,目前團隊也未對log機制來做unit test。log的內容則是hardcode在這支base class裡,所以當log的內容需要修改時,這支base class就得修改重新發佈新版本。log的部份主要是在發送HTTP request和接收到HTTP response時使用,如
protected IRestResponse<T> GetRestResponse<T>(IRestRequest restRequest)
{
    var sb = new StringBuilder();
   
    try
    {
        AppendRequestLog(restRequest, sb);
        IRestResponse<T> response = Client.Execute<T>(restRequest);
        AppendResponseLog(response, sb);
        Log.Info(sb);
        return response;
    }
    catch (Exception ex)
    {
        Log.Error(sb, ex);
        throw new BaseClassException("Failed to get information.", ex);
    }
}
現行的log設計有幾個主要問題
  1. log內容若修改,base class就得修改重新發佈新版本函式庫
  2. log機制侷限於使用Common.Logging,函式庫打包時得多加入Common.Logging
  3. 函式庫使用者相依於Common.Logging,且需使用特定Common.Logging adapter,如log4net/NLog,或是自行實作出一個adapter
  4. log內容無法由函式庫使用者來自訂擴充或修改
要讓函式庫使用者能自訂log內容,我們可以用delegate藉由屬性或方法傳入base class,但如此一來base class就得新增屬性或方法參數,只為了傳遞log用的delegate,有點違反SRP的味道在。如果能讓函式庫使用者自訂log內容,並且把Common.Logging從函式庫抽掉,這樣的設計豈不乾淨且具彈性許多。

文章開始所提到的IRestClient介面目前是透過IoC container經由constructor injection把實體類別帶入base class裡。目前團隊使用的IoC container為Unity,如果Unity提供AOP的機制讓我們在具現化RestClient類別時,把log邏輯塞到Execute<T>方法(signature為public virtual IRestResponse Execute(IRestRequest request) where T : new();)裡,一切就搞定了。

目前參考base class函式庫的專案使用的Unity nuget版本為3.0.1304,可由https://www.nuget.org/packages/Unity/3.0.1304安裝。Unity本身預設沒有支援AOP的機制,需額外安裝Unity Interception Extension。安裝成功的話,專案會多出兩個參考,Microsoft.Practices.Unity.InterceptionMicrosoft.Practices.Unity.Interception.Configuration

接下來在宣告container時把這個extension加進來
var container = new UnityContainer();
container.AddNewExtension<Interception>();

並建立一類別實作IInterceptionBehavior介面,如
internal class LoggingInterceptionBehavior : IInterceptionBehavior
{
    protected ILog Log = LogManager.GetLogger("Base");

    public IMethodReturn Invoke(IMethodInvocation input,
                                GetNextInterceptionBehaviorDelegate getNext)
    {
        if (input.MethodBase.Name != "Execute")
        {
            return getNext()(input, getNext);
        }

        var sb = new StringBuilder();

        try
        {
            var request = input.Arguments["request"] as IRestRequest;

            if (request == null)
            {
                return getNext()(input, getNext);
            }

            AppendRequestLog(request, sb);

            IMethodReturn result = getNext()(input, getNext);

            var response = result.ReturnValue as IRestResponse;

            AppendResponseLog(response, sb);

            Log.Info(sb);

            return result;
        }
        catch (Exception ex)
        {
            Log.Error(sb, ex);
            throw;
        }
    }

    private void AppendRequestLog(IRestRequest restRequest, StringBuilder sb)
    {
        // Log request
    }

    private void AppendResponseLog(IRestResponse response, StringBuilder sb)
    {
        // Log response
    }

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public bool WillExecute
    {
        get { return true; }
    }
}

最後用Unity具現化RestClient
container.RegisterType<IRestClient, RestClient>(new Interceptor<InterfaceInterceptor>(),
    new InterceptionBehavior<LoggingInterceptionBehavior>());

到這裡我們已經把log邏輯從base class內拉出來,接下來便可以把base class裡原本使用到的log邏輯移除,base class顯得乾淨許多,函式庫也不用再相依Common.Logging。日後如果要更換logging framework或是採用自己撰寫的log元件,只需要修改LoggingInterceptionBehavior這支類別即可。

No comments: