剩下来就是改造Logger类了。在我们将具体写入逻辑从该类剥离出来后,它只需要保留对ILogTarget列表的引用,当写入日志时,构造对应的LogData对象,再依次调用数组中每个写入方法即可。剩余的LogXXX系列的 API 无需改动,其本身就是对LogToTarget的调用而已。
public class Logger { public List<ILogTarget> Targets { get; set; } = new List<ILogTarget>(); public void LogToTarget(LogLevel level, string message) { var logData = new LogData(level, message); foreach (var target in Targets) { target.WriteLog(logData); } } ... }最后,我们看下这个版本的使用方式,这对需求方来讲是最重要的。这里使用三行代码来构建相对应的logger对象,通过给定的 API 来写。当然如果需求方想设计自己的写入对象(Email),他只需要实现ILogTarget接口,并将该对象添加到logger.Target对象即可。扩展性得到了大大的增强。
Logger logger = new Logger(); logger.Targets.Add(new ConsoleTarget()); logger.Targets.Add(new FileTarget("./log.txt")); logger.LogInformation("用户登录成功");另外,纵观v6版本,里面的类开始慢慢变多了,为了方便区分,我们把相同相似的功能放在一个文件夹下。如下图,Data表示保存日志事件相关数据,Target则保存类库内部提供好的若干具体写入逻辑。
版本七(再好的玩意都需要易用的设计)甲方:v6版本用起来确实还不错,扩展点也合理,不过,就是最初创建Logger对象的时候,比较麻烦。我把这个库给其他人用,他们说ConsoleTarget和FileTarget是啥,为什么FileTarget构造函数必须要带参数,为什么要将这两个玩意添加到Targets属性里,logger里面的Targets是啥?你能不能提供一种更好的创建方式?
确实,在v6版本中,我们通过三行来构建了一个可以写入控制台和文件的日志记录器,第一条语句不用说,通过new关键字创建对应对象,但后两句容易让新手不易理解,就像甲方所说的,似乎为了最基础的使用,我们需要了解类库的内部,这让新人使用起来非常有负担。至少为了使用这玩意,他们需要记住有ConsoleTarget、FileTarget这些类,以及相关的使用方法。
在修前之前,我们再次回顾下v6中的三行创建语句。本质上来讲,调用方只需要针对Logger中的Targets数组添加对应的写入模式对象即可,写入控制台就添加ConsoleTarget对象,写入文件则添加FileTarget对象。那么我们用一个新类LogBuilder来描述具体的创建过程,如下所示。
public class LogBuilder { public List<ILogTarget> Targets { get; } public LogBuilder() { Targets = new List<ILogTarget>(); } public Logger CreateLogger() { return new Logger(Targets.ToArray()); } }在LogBuilder中,我们维护了一个ILogTarget的列表,在类初始化时就给它设置默认长度的列表。之后,在CreateLogger函数中,我们将Targets通过Logger的构造函数注入到Logger类中的_targets字段中。为此,我们修改Logger中的相关代码,将原本的ILogTarget的列表转变成数组,这是因为Logger只用于记录日志,此时其写入的目的地就已经固定了,不需要再改动。
public class Logger { public ILogTarget[] _targets; public Logger(ILogTarget[] targets) { _targets = targets; } ... }我们定义好了Logger以及LogBuilder后,原本 v6 中向Logger内添加对应的ILogTarget对象则转变成如何向LogBuilder中的Targets添加对象。这里,常用的办法是通过扩展方法添加写入功能。
public static class LogBuilderExtentions { public static LogBuilder AddConsole(this LogBuilder builder) { builder.Targets.Add(new ConsoleTarget()); return builder; } public static LogBuilder AddFile(this LogBuilder builder, string path) { builder.Targets.Add(new FileTarget(path)); return builder; } }基于上述改动,我们的使用方法变得更加简单,如下所示。这将原先的三行构造语句缩减成了一行,虽然这行语句由4行构成,但和 v6 版本的调用方式相比,该版本具有更强的语义性。首先是创建LogBuilder对象,通过名字我们知道它是核心类Logger的创建器,之后通过AddXXX表示添加对应的日志写入目的地,AddConsole表明目的地是控制台,AddFile表明目的地是给定文件,最后通过CreateLogger创建出指定的Logger对象。这种方法的好处在于,它非常方便需求方以及其他调用方的使用,可以看到,它只需要调用方知道构造LogBuilder类,然后通过AddXXX系列的函数添加不同的日志写入媒介逻辑,最后通过CreateLogger方法创建出Logger对象。
Logger logger = new LogBuilder() .AddConsole() .AddFile("./log.txt") .CreateLogger(); logger.LogInformation("test...");