[Architecture] Inversion of Logging

摘要:[Architecture] Inversion of Logging

動機

一個軟體系統的開發,Log是一個不可或缺的功能。不管是做問題的追查、或是狀態的分析,有了Log的輔助能讓開發人員有跡可循。而這些Log功能的實作模組,開發人員可以選用.NET內建的EventLog、或者是第三方的Log4net….等等來使用。有這麼多種的實作模組可以使用,簡化了開發人員的工作量,但是也帶來了另外一個問題:「系統增加了對Log實作模組的相依」。

 

假設我們現在開發一個User模組,這個模組使用了EventLog來完成Log功能。經過長時間的驗證後,確認了User模組的穩固以及強大。現在有另一個專案需要使用User模組相關的功能,而這個專案則是使用Log4net來完成Log功能。這時候就會很尷尬的發現,新專案跟User模組是採用兩套不同Log功能的實作模組。

 

 

當然在程式開發時,開發人員可以無視這個問題,反正編譯都會通過功能都是正常。但這樣就苦了後續接手的部署人員,部屬人員必須要理解兩種不同實作模組的設定方式。當然這也可以透過非程式開發的手段去處理,但當系統越來越大、Log模組越來越多,總有一天部屬人員會殺到開發人員翻桌抗議的。並且對於開發人員自身來說,這些額外的模組設定,在開發系統單元測試時,也會或多或少增加開發人員的工作量。

 

 

本篇文章介紹一個套用IoC模式的Inversion of Logging實作,這個實作定義物件之間的職責跟互動,用來反轉系統對於Log模組的相依,解決上述的問題。為自己做個紀錄,也希望能幫助到有需要的開發人員。

 

 

實作

 

範列下載

實作說明請參照範例程式內容:Inversion of Logging點此下載
(*執行範例須有系統管理員權限,才能正常執行。)

 

ILogger

 

首先為了可以抽換使用的Log模組,必須先來建立一組抽象的ILogger介面。讓系統只需要相依這層抽象模組,就可以使用Log功能。而供系統抽換用的Log模組則需要實作這個ILogger介面,提供Log功能。

 


namespace CLK.Logging
{
    public enum Level
    {
        Error,
        Warning,
        Information,
        SuccessAudit,
        FailureAudit,
    }
}

 


namespace CLK.Logging
{
    public interface ILogger
    {
        // Methods
        void Log(Level level, string message);

        void Log(Level level, string message, System.Exception exception);
    }
}

 

LogManager

 

接著著手處理生成ILogger物件的LogManager物件。這個LogManager物件套用Singleton模式,讓整個系統裡只會有唯一的LogManager物件,同系統內不同模組之間也會只有唯一的LogManager物件。這樣就可以統一由唯一LogManager物件來控管不同模組之間的ILogger物件生成。

 

而LogManager是一個抽象類別,它提供了ILogger物件生成及自己物件釋放的函式。實作LogManager類別需要實作這兩個介面,以提供管理ILogger物件生命週期的功能。另外在不同模組之間,有可能會需要不同的Log設定。所以在生成ILogger物件的生成函式上,加入了一個字串參數,讓LogManager類別的實作用來識別不同模組,以生成不同設定的ILogger物件。至於物件釋放的函式,則只是預留給實作LogManager類別的物件有統一釋放資源的入口。

 

 


namespace CLK.Logging
{
    public abstract class LogManager : IDisposable
    {
        // Singleton
        private static LogManager _current;

        public static LogManager Current
        {
            get
            {
                // Require
                if (_current == null) throw new InvalidOperationException();

                // Return
                return _current;
            }
            set
            {
                // Require
                if (_current != null) throw new InvalidOperationException();

                // Return
                _current = value;
            }
        }
        

        // Methods
        public abstract ILogger CreateLogger(string name);

        public abstract void Dispose();
    }    
}

 

EventLogManager

 

在範例程式裡,示範了實作EventLog的Log模組實作,將Log資訊寫入到Windows事件紀錄。相關的程式碼如下,有興趣的開發人員可以花點時間學習,在需要擴充Log模組的時候(例如:Log4net),就可以自行加入相關的實作。

 


namespace CLK.Logging.Implementation
{
    public static class EventLogLevelConvert
    {
        public static EventLogEntryType ToEventLogEntryType(Level level)
        {
            switch (level)
            {
                case Level.Error: return EventLogEntryType.Error;
                case Level.Warning: return EventLogEntryType.Warning;
                case Level.Information: return EventLogEntryType.Information;
                case Level.SuccessAudit: return EventLogEntryType.SuccessAudit;
                case Level.FailureAudit: return EventLogEntryType.FailureAudit;
                default: return EventLogEntryType.Error;
            }
        }
    }
}

 


namespace CLK.Logging.Implementation
{
    public class EventLogLogger : ILogger
    {
        // Fields
        private readonly string _sourceName = null;


        // Constructors
        public EventLogLogger(string sourceName)
        {
            #region Contracts

            if (string.IsNullOrEmpty(sourceName) == true) throw new ArgumentException();

            #endregion
            _sourceName = sourceName;
        }


        // Methods
        public void Log(Level level, string message)
        {
            this.Log(level, message, null);
        }

        public void Log(Level level, string message, Exception exception)
        {
            #region Contracts

            if (string.IsNullOrEmpty(message) == true) throw new ArgumentException();

            #endregion

            if (EventLog.SourceExists(_sourceName)==false)
            {
                EventLog.CreateEventSource(_sourceName, null);
            }

            EventLog eventLog = new EventLog();
            eventLog.Source = _sourceName;
            eventLog.WriteEntry(string.Format("Message={0}, Exception={1}", message, exception == null ? string.Empty : exception.Message), EventLogLevelConvert.ToEventLogEntryType(level));
        }
    }    
}

 


namespace CLK.Logging.Implementation
{
    public class EventLogManager : LogManager
    {
        // Methods
        public override ILogger CreateLogger(string name)
        {
            return new EventLogLogger(name);
        }

        public override void Dispose()
        {

        }
    }
}

 

使用

 

UserModule.Logging.Logger

接著撰寫一個虛擬的UserModule來示範如何套用Inversion of Logging。首先在UserModule內加入命名空間Logging,UserModule模組內需要使用Log功能就需要引用這個命名空間。另外在Logging命名間內建立Logger物件,將下列的程式碼複製貼入Logger物件內,就完成了Logger物件的撰寫。

 

這個Logger物件使用組件名稱當作識別,並且藉由調用唯一的LogManager物件生成實作Log功能的ILogger物件。另外也包裝這個ILogger物件的功能,成為靜態函式讓系統更方便的使用。

 

 


namespace UserModule.Logging
{
    internal static class Logger
    {
        // Singleton 
        private static ILogger _currentLogger;

        private static ILogger CurrentLogger
        {
            get
            {
                if (_currentLogger == null)
                {
                    _currentLogger = LogManager.Current.CreateLogger(typeof(Logger).Assembly.GetName().Name);
                }
                return _currentLogger;
            }
        }


        // static Methods
        public static void Log(Level level, string message)
        {
            CurrentLogger.Log(level, message);
        }

        public static void Log(Level level, string message, System.Exception exception)
        {
            CurrentLogger.Log(level, message);
        }
    }
}

 

UserModule.User

 

完成上面這些程式的撰寫之後,在UserModule內使用Log功能就只需要使用抽象建立的ILogger實作,不用去相依於目前正在使用的Log模組。

 


namespace UserModule
{
    public class User
    {
        public void Test(string message)
        {
            // Do

            // Log
            Logger.Log(CLK.Logging.Level.Information, "OK");
        }
    }
}

 

執行

 

最後建立使用UserModule的系統InversionOfLoggingSample,在InversionOfLoggingSample內透過注入LogManager.Current來決定系統使用哪個Log模組,只要是在同一個系統下的Log資訊,都會透過這個Log模組來完成Log功能。

 

開發人員可以採用DI框架來生成LogManager並且注入LogManager.Current,就可以完成抽換Log模組的功能。而在單元測式的場合,也可以注入範例檔案中提供的EmptyLogger,來簡化單元測試時相關的Log設定。

 

 


namespace InversionOfLoggingSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // Init
            LogManager.Current = new EventLogManager();

            // Test
            User user = new User();
            user.Test("Clark=_=y-~");
        }
    }
}

 

 

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。