大多數的人在決定將系統導入log機制時通常會先選擇幾個較為知名且開源的logging framework如Enterprise Library的Logging Application Block、log4net、NLog等,除了避免重複造輪子浪費時間外,這些framework也提供了不少強大的功能還附上原始碼。這裡不談如何去選擇一個合適的logging framework,畢竟個人偏好及政治因素可能佔上大部份,也就是那句老話,it depends。
上述framework都有各自的抽象層及實作,通常很直覺地當我們安裝好時,就直接在程式碼中使用起來,例如NLog使用factory method pattern去建立一個Logger實作類別
在不做unit test和不更換logging framework(例如將log4net換成NLog)的前提下,的確是不會有什麼問題。硬要說的話,就是違反了dependency inversion principle和program to an interface, not an implementation兩個原則,造成client code直接與logging framework相依,耦合度提高。
基於這兩個原則,我們可以利用facade pattern建立一個client code要做log時呼叫用的抽象層,例如
Logger logger = LogManager.GetCurrentClassLogger(); logger.Debug("this is a Debug level");而log4net則使用factory method pattern建立一個ILog介面
ILog log = LogManager.GetLogger(typeof(Program)); log.Debug("this is just a debug message");接下來諸如上面的程式碼片段便開始散佈在各個使用logging framework的類別中。這樣會有什麼問題嗎?
在不做unit test和不更換logging framework(例如將log4net換成NLog)的前提下,的確是不會有什麼問題。硬要說的話,就是違反了dependency inversion principle和program to an interface, not an implementation兩個原則,造成client code直接與logging framework相依,耦合度提高。
基於這兩個原則,我們可以利用facade pattern建立一個client code要做log時呼叫用的抽象層,例如
public interface ILogService { void Debug(string message; void Info(string message); void Warn(string message); void Error(string message); // 以下省略 }接下來建立一個類別來實作ILogService介面,而這個實作類別裡直接使用logging framework,等於是將實際log的功能再委託給logging framework,以下面為例是直接呼叫NLog提供的API。
public class LogService : ILogService { private static Logger _logger = LogManager.GetCurrentClassLogger(); #region ILogService Members public void Debug(string message) { if (!_logger.IsDebugEnabled) { return; } if (string.IsNullOrWhiteSpace(message)) { throw new ArgumentNullException("message"); } _logger.Debug(message); } // 以下省略 #endregion }接著在client code只要搭配IoC container就可以反轉相依性讓client code相依於前面建立的抽象層(ILogService)。
ILogService logService = ObjectFactory.GetInstance<ILogService>(); logService.Debug("This is a debug level");如此便符合上述兩項原則,但為了符合這兩項原則還得大費周章建立一個抽象層來使用,真的值得嗎?以下就兩個面向來討論
Testability
將原本相依於logging framework實作的程式反轉成相依於自行建立的抽象(ILogService)類別可以提高程式碼單元測試的可測性,讓我們可以專注在待測的類別,並使用mocking framework如Rhino Mocks透過mock或stub模擬ILogService的操作,藉以輔助我們實際要測試的功能,如此我們就不必理會ILogService實際上的實作(LogService)如何運作,因為我們可以透過mock/stub模擬出我們要它(ILogService)作的行為。例如下面unit test的程式碼片段
如果沒有建立ILogService這個抽象層的話會有什麼問題嗎?問題會出現在你可能無法預期LogService這個實作類別或是所使用的logging framework會為你帶來的意外效果。舉個例子,當LogService丟出一個ArgumentNullException出來。但以上面的程式碼片段來看,我們要測試的是當執行IUserAccountService.Register時要丟出RegisterUserAccountException而且錯誤訊息還要被記錄到。另一個意外效果則是會發生在logging framework的設定檔沒有正確地被設定,這時也會出現無預期的錯誤,導致在執行unit test之前,必須先確認logging framework的設定檔是否正確。但在這種情況下所做的測試已經不是unit test而是integration test了,因為所做的測試和系統環境設定因素(外在因素)有關。
雖說如此,以NLog來說,即便我們直接相依於它,在沒有任何設定檔的情況下,即使呼叫它所提供的API(如Trace, Debug, Warn等)也不會丟出exception。的確,我也曾經想過是否不要隔著一層抽象層(ILogService),因為只要它不會丟出exception,看起來似乎也不會影響到我的unit test。但如果你的unit test如上面片段一樣,想要在丟出exception時也檢查是否有執行到log錯誤訊息的程式碼(logService.AssertWasCalled(c => c.Error("exception occurs");),這時你還是會需要透過mock去檢查。
Flexibility
將原本相依於logging framework實作的程式反轉成相依於自行建立的抽象(ILogService)類別可以提高程式碼單元測試的可測性,讓我們可以專注在待測的類別,並使用mocking framework如Rhino Mocks透過mock或stub模擬ILogService的操作,藉以輔助我們實際要測試的功能,如此我們就不必理會ILogService實際上的實作(LogService)如何運作,因為我們可以透過mock/stub模擬出我們要它(ILogService)作的行為。例如下面unit test的程式碼片段
[ExpectedException(typeof(RegisterUserAccountException))] [TestMethod] public void Register_ExistedUser_ThrowRegisterUserAccountException() { RegisterRequest request = new RegisterRequest() { Username = "petechen", Password = "petechen" }; IUserAccountRepository userAccountRepository = MockRepository.GenerateStub<IUserAccountRepository>(); userAccountRepository.Stub(c => c.CreateUserAndAccount(request.Username, request.Password)).IgnoreArguments().Throw(new Exception()); ILogService logService = MockRepository.GenerateStub<ILogService>(); IUserAccountService userAccountService = new UserAccountService(userAccountRepository, ILogService logService); userAccountService.Register(request); logService.AssertWasCalled(c => c.Error("exception occurs"); }實際要測試的類別為UserAccountService,但因為它相依了兩個介面IUserAccountRepository及ILogService,於是我們使用mock/stub來模擬這兩個介面的行為,這樣我們就可以專注在UserAccountService的功能(Register)是否如我們預期(丟出RegisterUserAccountException)般地執行。
如果沒有建立ILogService這個抽象層的話會有什麼問題嗎?問題會出現在你可能無法預期LogService這個實作類別或是所使用的logging framework會為你帶來的意外效果。舉個例子,當LogService丟出一個ArgumentNullException出來。但以上面的程式碼片段來看,我們要測試的是當執行IUserAccountService.Register時要丟出RegisterUserAccountException而且錯誤訊息還要被記錄到。另一個意外效果則是會發生在logging framework的設定檔沒有正確地被設定,這時也會出現無預期的錯誤,導致在執行unit test之前,必須先確認logging framework的設定檔是否正確。但在這種情況下所做的測試已經不是unit test而是integration test了,因為所做的測試和系統環境設定因素(外在因素)有關。
雖說如此,以NLog來說,即便我們直接相依於它,在沒有任何設定檔的情況下,即使呼叫它所提供的API(如Trace, Debug, Warn等)也不會丟出exception。的確,我也曾經想過是否不要隔著一層抽象層(ILogService),因為只要它不會丟出exception,看起來似乎也不會影響到我的unit test。但如果你的unit test如上面片段一樣,想要在丟出exception時也檢查是否有執行到log錯誤訊息的程式碼(logService.AssertWasCalled(c => c.Error("exception occurs");),這時你還是會需要透過mock去檢查。
Flexibility
相依於抽象層另一個帶來的好處就是讓程式碼更具彈性及可抽換性。為什麼呢?因為程式碼中不再相依於特定的logging framework。當你想把log4net換成NLog時,只需要修改LogService實作類別,或是建立另一個新的LogService實作類別,並在IoC container中將ILogService的實作改指向新對應的實作類別即可一次更新整個系統,因為所有程式碼都是呼叫ILogService,所以根本不需要修改原本的程式碼。
ObjectFactory.Initialize(x => { //x.For<ILogService>().Use<LogService>(); x.For<ILogService>().Use<LogServiceForNLog>(); });
此外,修改之後也只需要佈署ILogService新的實作類別即可(也許就只有一支DLL檔),並不需要將整個系統重新佈署,大大地降低佈署的複雜度和出錯的可能性。當然你可能會質疑在開發中或系統已上線再更換logging framework的機率。的確,像log4net或NLog這種等級的logging framework已經是相對穩定許多,除非它們有很嚴重的bug或效能的問題,我想更換的機率是不高,但所謂的彈性就是要預防這種未知的變動。
上述提到的兩點不僅是針對logging framework,也可套用在其它的third-party元件。本篇文章不下任何定論。因為有的人不做unit test或是根本沒時間做,又或是直接就做integration test。有的人認為更換logging framework的事根本不會發生。如果我只做integration test又只使用固定一套logging framework,何必額外花心思和時間在這上頭。所以去除耦合度的必要性?It always depends!
參考
上述提到的兩點不僅是針對logging framework,也可套用在其它的third-party元件。本篇文章不下任何定論。因為有的人不做unit test或是根本沒時間做,又或是直接就做integration test。有的人認為更換logging framework的事根本不會發生。如果我只做integration test又只使用固定一套logging framework,何必額外花心思和時間在這上頭。所以去除耦合度的必要性?It always depends!
參考
No comments:
Post a Comment