[Architecture Pattern] 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功能。
1 2 3 4 5 6 7 8 9 10 11 | namespace CLK.Logging { public enum Level { Error, Warning, Information, SuccessAudit, FailureAudit, } } |
1 2 3 4 5 6 7 8 9 10 | 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类别的对象有统一释放资源的入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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),就可以自行加入相关的实作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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; } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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)); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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对象的功能,成为静态函式让系统更方便的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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设定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | namespace InversionOfLoggingSample { class Program { static void Main( string [] args) { // Init LogManager.Current = new EventLogManager(); // Test User user = new User(); user.Test( "Clark=_=y-~" ); } } } |
期許自己~
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?