尽管我喜欢使用NLog,但它与目标,规则和记录器一起工作的方式也会变得如此。
所以我保留了记录器的思想,并在
LogManager
静态类中集中记录日志消息的格式–该类提供了整个客户端API;客户端代码不会直接处理ILogger
的实现,而是调用LogManager.Log
方法,该方法会将日志消息“分发”给所有相关的记录器。LogManager类模块
Option Explicit
Public Enum LogLevel
TraceLevel = 0
DebugLevel
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
End Enum
Private Type TLogManager
Formatter As ILogMessageFormatter
Loggers As New Dictionary
End Type
Private this As TLogManager
Public Property Get Formatter() As ILogMessageFormatter
Set Formatter = this.Formatter
End Property
Public Property Set Formatter(ByVal value As ILogMessageFormatter)
Set this.Formatter = value
End Property
Public Sub Register(ByVal logger As ILogger)
If Not this.Loggers.Exists(logger.Name) Then
this.Loggers.Add logger.Name, logger
Else
Err.Raise vbObjectError + 1098, "LogManager.Register", "There is already a logger registered with name '" & logger.Name & "'."
End If
End Sub
Public Function IsEnabled(ByVal level As LogLevel) As Boolean
Dim logger As ILogger
Dim item As Variant
For Each item In this.Loggers.Items
Set logger = item
If level >= logger.MinLevel Then
IsEnabled = True
Exit Function
End If
Next
End Function
Public Sub Log(ByVal level As LogLevel, ByVal message As String, Optional ByVal loggerName As String)
Dim logger As ILogger
If loggerName = vbNullString Then
Dim item As Variant
For Each item In this.Loggers.Items
Set logger = item
LogWith logger, level, message
Next
ElseIf this.Loggers.Exists(loggerName) Then
LogWith this.Loggers(loggerName), level, message
Else
Err.Raise vbObjectError + 1099, "LogManager.Log", "There is no registered logger named '" & loggerName & "'."
End If
End Sub
Private Sub LogWith(ByVal logger As ILogger, ByVal level As LogLevel, ByVal message As String)
If level >= logger.MinLevel Then
logger.Log FormatMessage(level, logger.Name, message)
End If
End Sub
Friend Function FormatMessage(ByVal level As LogLevel, ByVal loggerName As String, ByVal message As String)
FormatMessage = this.Formatter.FormatMessage(level, loggerName, message)
End Function
Private Sub Class_Initialize()
Set this.Formatter = New DefaultLogMessageFormatter
End Sub
我不喜欢
*Manager
的名称,因此建议不胜枚举... OTOH NLog确实具有LogManager
类。ILogMessageFormatter类模块(接口)
Option Explicit
Public Function FormatMessage(ByVal level As LogLevel, ByVal loggerName As String, ByVal message As String) As String
End Function
此接口与
LogManager.Formatter
属性一起,允许配置日志消息的输出方式。这是默认的实现:DefaultLogMessageFormatter类模块
Option Explicit
Implements ILogMessageFormatter
Private Function ILogMessageFormatter_FormatMessage(ByVal level As LogLevel, ByVal loggerName As String, ByVal message As String) As String
ILogMessageFormatter_FormatMessage = Framework.Strings.Format("{0:s}\t{1}\t[{2}]\t{3}", Now, loggerName, FormatLogLevel(level), message)
End Function
Private Function FormatLogLevel(ByVal level As LogLevel) As String
Select Case level
Case LogLevel.DebugLevel
FormatLogLevel = "DEBUG"
Case LogLevel.ErrorLevel
FormatLogLevel = "ERROR"
Case LogLevel.FatalLevel
FormatLogLevel = "FATAL"
Case LogLevel.InfoLevel
FormatLogLevel = "INFO"
Case LogLevel.TraceLevel
FormatLogLevel = "TRACE"
Case LogLevel.WarnLevel
FormatLogLevel = "WARNING"
End Select
End Function
ILogger
接口仅规定记录器具有一个名称和最小级别属性,以及一个接受格式化日志输出的Log
方法:ILogger类模块(接口)
Option Explicit <我已经为
ILogger
接口编写了2种实现,都在带有默认实例PublicNotCreatable
的类中,并且公开了Create
方法以返回实例。一个写入立即窗格,并称为DebugLogger
:DebugLogger类模块Public Sub Log(ByVal output As String)
End Sub
Public Property Get Name() As String
End Property
Public Property Get MinLevel() As LogLevel
End Property
另一个写入指定的文本文件,并称为
FileLogger
:FileLogger类模块
Option Explicit
Private Type TDebugLogger
Name As String
MinLevel As LogLevel
End Type
Private this As TDebugLogger
Implements ILogger
Public Function Create(ByVal loggerName As String, ByVal loggerMinLevel As LogLevel) As ILogger
Dim result As New DebugLogger
result.Name = loggerName
result.MinLevel = loggerMinLevel
Set Create = result
End Function
Friend Property Get Name() As String
Name = this.Name
End Property
Friend Property Let Name(ByVal value As String)
this.Name = value
End Property
Friend Property Get MinLevel() As LogLevel
MinLevel = this.MinLevel
End Property
Friend Property Let MinLevel(ByVal value As LogLevel)
this.MinLevel = value
End Property
Private Sub ILogger_Log(ByVal output As String)
Debug.Print output
End Sub
Private Property Get ILogger_MinLevel() As LogLevel
ILogger_MinLevel = this.MinLevel
End Property
Private Property Get ILogger_Name() As String
ILogger_Name = this.Name
End Property
该
FileLogger
使用一个处理打开,写入和关闭文件的TextWriter
对象-超出了本文的范围,但可以在此处进行评论。客户代码
客户端代码可以以不同的方式实现
ILogger
,并使该代码将日志条目写入例如数据库中。提供FileLogger
和DebugLogger
是为了方便起见,但没有禁止在客户端上实现和注册自定义ILogger
的信息。 Option Explicit
Private Type TFileLogger
Name As String
MinLevel As LogLevel
LogFile As String
Writer As TextWriter
End Type
Private this As TFileLogger
Implements ILogger
Public Function Create(ByVal loggerName As String, ByVal loggerMinLevel As LogLevel, ByVal path As String) As ILogger
Dim result As New FileLogger
result.Name = loggerName
result.MinLevel = loggerMinLevel
Set result.Writer = TextWriter.Create
result.LogFile = path
Set Create = result
End Function
Friend Property Get Name() As String
Name = this.Name
End Property
Friend Property Let Name(ByVal value As String)
this.Name = value
End Property
Friend Property Get MinLevel() As LogLevel
MinLevel = this.MinLevel
End Property
Friend Property Let MinLevel(ByVal value As LogLevel)
this.MinLevel = value
End Property
Friend Property Get LogFile() As String
LogFile = this.LogFile
End Property
Friend Property Let LogFile(ByVal value As String)
this.LogFile = value
End Property
Friend Property Get Writer() As TextWriter
Set Writer = this.Writer
End Property
Friend Property Set Writer(ByVal value As TextWriter)
Set this.Writer = value
End Property
Private Sub ILogger_Log(ByVal output As String)
If this.Writer.OpenFile(this.LogFile) Then
this.Writer.WriteLine output
this.Writer.CloseFile
Else
Err.Raise vbObjectError + 1092, "ILogger.Log", "FileLogger.LogFile could not be opened."
End If
End Sub
Private Property Get ILogger_MinLevel() As LogLevel
ILogger_MinLevel = this.MinLevel
End Property
Private Property Get ILogger_Name() As String
ILogger_Name = this.Name
End Property
这段代码在
Logging.xlam
中生成以下输出: C:\Dev\VBA\log.txt
以及即时窗格中的以下输出:
2014-09-28 19:42:44 TestLogger [ERROR] Division by zero
因为在
TestLogger
2014-09-28 19:42:44 MyLogger [INFO] it works!
False
2014-09-28 19:42:44 MyLogger [ERROR] Division by zero
处没有MinLevel
的记录器,所有TraceLevel
日志条目都将被忽略;客户端代码可以使用TraceLevel
函数检查当前是否启用了给定的日志级别,并有条件地注册记录器。#1 楼
真是挑剔,但除非您不想从零开始,否则没有理由声明枚举的起点。
Public Enum LogLevel
TraceLevel = 0
DebugLevel
'.....
您还在这里重复很多。几乎是匈牙利人。它们都是
LogLevel
。我认为您可以将它们缩短为Trace
,Debug
,Info
等。当然,vba对于何时可以调用和不能调用带有LogLevel.Trace
这样的完整引用的枚举成员可能会有些奇怪,因此您可以或可能不想这样做。我本人对此不感兴趣。但是您不能这样做,因为Debug
是保留关键字...。我真的建议为您的自定义错误编号使用Enum。如果它们都放在一个地方,维护起来会容易得多。
Err.Raise vbObjectError + 1098, "LogManager.Register", "There is already a logger registered with name '" & logger.Name & "'."
'.......
Err.Raise vbObjectError + 1099, "LogManager.Log", "There is no registered logger named '" & loggerName & "'."
使用枚举看起来像这样的枚举,它们变得更加简单。
Public Enum LogMangerError
SomeError = vbObjectError + 1098
SomeOtherError
End Enum
'......
Err.Raise SomeError, "LogManager.Register", "There is already a logger registered with name '" & logger.Name & "'."
这是阅读的绝对乐趣。大量使用空白。
Select Case level
Case LogLevel.DebugLevel
FormatLogLevel = "DEBUG"
Case LogLevel.ErrorLevel
FormatLogLevel = "ERROR"
Case LogLevel.FatalLevel
FormatLogLevel = "FATAL"
Case LogLevel.InfoLevel
FormatLogLevel = "INFO"
Case LogLevel.TraceLevel
FormatLogLevel = "TRACE"
Case LogLevel.WarnLevel
FormatLogLevel = "WARNING"
End Select
也许我累了,或者也许很好。我目前无法确定,但是我认为您这里有一些非常可靠的代码。良好的命名和格式,以及非常好的抽象和使用接口。
评论
太棒了有一个小项,但没有代码:我不知道在发布后它是否被更改,但是在提供的链接中找不到FileLogger类中引用的TextWriter类。也许FileWriter曾经被称为TextWriter?无论哪种方式,我都假定当时使用的类具有将Create,Open,Close和WriteLine转换为带有loggerFile名称的文本文件的方法。这有点超出了您在此处编写代码的范围,但是我不知道还有什么可以联系到您的。我一直在阅读您的大量代码审查,以尝试通过使用vba获得更多的OOP。当您编写这样的组件(记录器,文件编写器等)时,通常是否将它们保存在自己的.xlam文件中,然后像对待库一样引用它们?
@ArcherBird,直到有人提出针对VBA代码的软件包管理器解决方案(例如nuget,npm等),这才是使这些组件保持在附近的好方法。另一种方法是在\ Dev \ VBA文件夹中包含所有模块(在git源代码控制下),可以从其中轻松地将文件导入到任何新的VBA项目中。
@ArcherBird(如果您使用的是Rubberduck),则可以在模块顶部使用@PredeclaredId注释轻松添加为类提供默认实例的VB_PredeclaredId = True隐藏属性,并且可以将“ predeclared class”模板添加到您的代码浏览器工具窗口中的项目,以便您免费获得该属性。该记录器代码使用默认实例之外的工厂方法(并且早于Rubberduck!)。实际上,我发现工厂方法(与显式接口结合)非常棒,尤其是在外接程序设置中:您可以真正构建公共API。
@ArcherBird这是一个很难发现的功能。请参见存储库Wiki上的VB_Attribute注释和“使用文件夹”注释。