使用哪个模式进行日志记录?依赖注入还是服务定位器?

40

考虑这种情况。我有一些业务逻辑,偶尔需要写入日志。

interface ILogger
{
    void Log(string stuff);
}

interface IDependency
{
    string GetInfo();
}

class MyBusinessObject
{
    private IDependency _dependency;

    public MyBusinessObject(IDependency dependency)
    {
        _dependency = dependency;
    }

    public string DoSomething(string input)
    {
        // Process input
        var info = _dependency.GetInfo();
        var intermediateResult = PerformInterestingStuff(input, info);

        if (intermediateResult== "SomethingWeNeedToLog")
        {
            // How do I get to the ILogger-interface?
        }

        var result = PerformSomethingElse(intermediateResult);

        return result;
    }
}

如何获取ILogger接口?我看到两个主要的可能性:

  1. 通过构造函数使用依赖注入传递它。
  2. 通过单例服务定位器获取它。

你更喜欢哪种方法,为什么?或者有更好的模式吗?

更新: 请注意,我不需要记录所有方法调用。我只想记录一些(罕见的)事件,这些事件可能会在我的方法中发生或不发生。


2
第三个选项是属性注入(使用属性),这可能避免了构造函数的混乱。 - Mark Heath
9个回答

18

我个人两种方式都运用:

我的规则如下:

  • 在静态上下文中 - 服务定位
  • 在实例上下文中 - 依赖注入

我觉得这样做可以达到测试的平衡。相比使用服务定位的类,我发现使用依赖注入的类更难进行测试设置,因此服务定位成为了例外而不是常规。但我在使用它时保持一致,所以记住需要编写哪种类型的测试也不难。

有人提出依赖注入会使构造函数变得混乱。我并不认为这是一个问题,但如果您有这种感觉,有许多其他的替代方案可以使用依赖注入,但避免使用构造函数参数。下面是Ninject的DI方法列表: http://ninject.codeplex.com/wikipage?title=Injection%20Patterns

您会发现大多数控制反转容器都具有与Ninject相同的功能。我选择展示Ninject,因为它们有最简明的示例。

希望这对你有帮助。

编辑:为清楚起见,我使用Unity和Common Service Locator。我有一个DI的Unity容器单例实例,我的IServiceLocator实现只是该单例Unity容器的包装器。这样,我就不必多次执行任何类型映射之类的操作。

除了跟踪之外,我也不觉得AOP特别有用。对于其清晰性,我更喜欢手动记录日志。我知道大多数AOP日志框架都能够做到这两点,但我并不经常需要前者(AOP的核心优势)。当然,这只是个人偏好。


1
谢谢,这与我的感受非常相符! - CodingInsomnia

10

日志记录器显然是您的业务逻辑依赖的服务,因此应像处理 IDependency 一样将其视为依赖项。在构造函数中注入记录器。

注意:尽管AOP被提及为注入日志的方法,但我不同意它是这种情况的解决方案。AOP非常适用于执行跟踪,但永远不会成为业务逻辑日志的解决方案。


5
“记录器显然是您的业务逻辑所依赖的服务” - 我不同意这个说法。您的业务逻辑不应该以任何方式依赖于日志记录,而且您的应用程序应该在没有它的情况下正常工作。但是,您的调试过程可能需要它... - Thorkil Holm-Jacobsen
1
在OP的情况下,它是:业务逻辑会不时需要写入日志。我同意这并非总是如此。但我不同意你的“业务逻辑绝不能依赖于日志记录”。我曾多次遇到这样的业务需求。 - Peter Lillevold

7

我的经验法则是:

  • 如果代码在类库中,使用构造函数注入或属性注入的空对象模式。

  • 如果代码在主应用程序中,使用服务定位器(或单例)。

我发现这个规则在使用log4net时非常适用。您不希望类库依赖可能不存在的内容,但在应用程序中,您知道日志记录器一定存在,而像log4net这样的库主要基于服务定位模式。

我认为记录日志足够静态,不需要使用DI。在应用程序中几乎不可能更改记录实现,特别是因为每个日志框架都非常灵活且易于扩展。在类库中更重要,因为您的库可能需要被多个已经使用不同日志记录器的应用程序使用。

当然,你的情况可能不同。DI很棒,但并不意味着所有东西都需要使用DI。


6
也许这有点离题,但为什么我们需要注入日志记录器呢?当我们可以在类的开头直接输入以下内容时:
Logger logger = LogManager.GetLogger("MyClassName");

在开发和维护过程中,记录器不会发生变化。现代记录器具有高度可定制性,因此缺少替换文本记录器为数据库的参数。

如果我想用数据库替换文本记录器怎么办?

这个问题被忽略了。

我并不否认使用依赖注入,我只是对您的想法感到好奇。


3
完全同意。例如,log4net可以配置为记录基本上任何内容,您还可以编写自己的适配器。通过这种方式,您可以知道使用的记录器是什么,而在注入时很难弄清楚。 - Jepzen
我同意。我相信对于绝大多数应用程序来说,依赖注入日志记录是过度设计。通常,日志框架可以配置日志记录位置(等等),这为它们提供了所需的灵活性。此外,我从未见过需要模拟日志记录功能进行测试的情况。当您想要在配置中按命名空间更改日志级别时,DI 甚至会让您的生活变得更加复杂。日志框架如何知道它在哪个命名空间中?(有解决方案,但我认为它们不够优雅) - Dan
1
然而,我强烈建议在创建库时使用DI。这是DI和日志记录的一个常见用例。 - Dan

4
我们将所有的日志/跟踪切换到PostSharp(AOP框架)属性中。只需要将属性添加到方法中,就可以创建该方法的日志。
好处:
- 容易使用AOP - 明确的关注点分离 - 在编译时发生 -> 最小化性能影响 请查看this

1
很遗憾,我不能在这里使用简单的AOP方法。我不想记录所有方法调用,只想记录一些可能发生在方法内部的事件。 - CodingInsomnia

2
您可以派生另一种类型,例如LoggableBusinessObject,它在其构造函数中接受一个记录器。这意味着您只为将使用它的对象传递记录器:
public class MyBusinessObject
{
    private IDependency _dependency;

    public MyBusinessObject(IDependency dependency)   
    {   
        _dependency = dependency;   
    }   

    public virtual string DoSomething(string input)   
    {   
        // Process input   
        var info = _dependency.GetInfo();   
        var result = PerformInterestingStuff(input, info);   
        return result;   
    }   
}

public class LoggableBusinessObject : MyBusinessObject
{
    private ILogger _logger;

    public LoggableBusinessObject(ILogger logger, IDependency dependency)
        : base(dependency)
    {
        _logger = logger;
    }

    public override string DoSomething(string input)
    {
        string result = base.DoSomething(input);
        if (result == "SomethingWeNeedToLog")
        {
             _logger.Log(result);
        }
    }
}

1
这是一种有趣的解决方案,但对于一个相对简单的任务来说,它似乎需要编写过多的代码。它也只允许基于方法结果记录事物(尽管我意识到我的示例正是如此...)。 - CodingInsomnia
+1 对于一种允许我对 MyBusinessObject 进行单元测试而不必模拟 ILogger 的解决方案。 - CodingInsomnia
5
但是使用这个模式,你只能在调用基类方法之前或之后记录日志。仍然无法在方法执行过程中记录日志。 - M4N
@Martin,说得好,但是根据所提供的示例,日志记录仅基于DoSomething的结果发生,因此上述解决方案非常适合。 - James
将字段/变量_logger更改为只读,以便在LoggableBusinessObject内部无法创建新实例。 - Shiljo Paulson

2

我更喜欢单例服务。

依赖注入会使构造函数变得混乱。

如果您可以使用AOP,那将是最好的选择。


DI不等于混乱的构造函数。 - user1228
1
@Will:你能解释一下吗? - M4N
2
@Will 这个踩票有点过了。如果使用构造函数注入,ILogger接口确实会在构造函数中“混杂”一些不是核心依赖项的东西。 - CodingInsomnia
@andlju:我猜@Will指的是DI不一定需要构造函数参数(是的,他说得对)。使用属性或另一种方法进行DI同样有效。这里是Ninject DI方法列表的链接:http://ninject.codeplex.com/wikipage?title=Injection%20Patterns - Anderson Imes
3
-1,原因如下:1) 单例模式是反模式。2)混杂构造函数?请解释一下!3)AOP不能与记录业务逻辑的日志一起使用。 - Peter Lillevold
显示剩余2条评论

0

在这里使用 DI 会很好。另一个要看的东西是 AOP


0
我不建议使用这两种方法。最好使用面向切面编程(AOP)。记录日志是AOP的“hello world”。

是的,但只有在需要记录所有调用时才这样做(我已经这样做了)。但在这种情况下,我需要记录偶尔发生的事情(如果你知道我的意思,那就是事件日志记录而不是调用日志记录)。 - CodingInsomnia
如果只是偶尔使用,我认为 DI 也会过度设计。我会为 Logger 使用静态类成员,并完成它。 - duffymo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接