作为装饰器登录与依赖注入——如果我需要在 class 中登录怎么办?

Logging as a decorator vs. Dependency Injection - what if I need to log inside the class?

(我最初在 this comment 中提出这个问题,但 Mark Seemann 让我创建一个新问题。)

我正在启动一个新应用程序(.NET Core,如果这很重要),现在我正在尝试决定如何进行日志记录。

普遍的共识似乎是日志记录是一个横切关注点,因此不应将记录器直接注入到应该记录的 class 中。

经常有像下面这样的例子class如何去做:

public class BadExample : IExample
{
    private readonly ILogger logger;

    public BadExample(ILogger logger)
    {
        this.logger = logger;
    }

    public void DoStuff()
    {
        try
        {
            // do the important stuff here
        }
        catch (Exception e)
        {
            this.logger.Error(e.ToString());
        }
    }
}

相反,具有业务逻辑的 class 不应该知道记录器 (SRP),应该有一个单独的 class 来进行记录:

public class BetterExample : IExample
{
    public void DoStuff()
    {
        // do the important stuff here
    }
}

public class LoggingBetterExample : IExample
{
    private readonly IExample betterExample;
    private readonly ILogger logger;

    public LoggingBetterExample(IExample betterExample, ILogger logger)
    {
        this.betterExample = betterExample;
        this.logger = logger;
    }

    public void DoStuff()
    {
        try
        {
            this.betterExample.DoStuff();
        }
        catch (Exception e)
        {
            this.logger.Error(e.ToString());
        }
    }
}

每当需要 IExample 时,DI 容器 returns 一个 LoggingBetterExample 的实例,它在底层使用 BetterExample(包含实际的业务逻辑) .

这种方法的一些来源:

博客 posts Mark Seemann:

博客 post 和 Steven 的回答:


我的问题:

显然,LoggingBetterExample 方法仅适用于可以在实际 class.
之外进行日志记录的情况 (如上例所示:从外部捕获 BetterExample 抛出的任何异常)

我的问题是我想在实际 class.
中记录其他内容 Mark Seemann suspected here 如果有人需要这样做,可能是有问题的方法做得太多了。

正如我之前所说,我正处于新应用程序的规划阶段,所以我没有太多代码可以展示,但我现在考虑的用例是这样的:

我的应用程序将有一个包含一些可选值的配置文件。
用户可能决定省略可选值,但这样做是一个重要的决定。
所以我想在缺少某些可选值时记录警告,以防万一它是错误发生的。
(虽然省略值是完全可以的,所以我不能抛出异常并停止)

这意味着我将有一个 class 读取配置值并需要做这样的事情(伪代码):

var config = ReadConfigValues("path/to/config.file");

if (config.OptionalValue == null)
{
    logger.Warn("Optional value not set!");
}

无论 ReadConfigValues 是在这个 class 还是另一个,我都不认为这个 class 会违反 SRP。

当我无法使用装饰器在实际 class 之外登录时,是否有比注入记录器更好的解决方案?

我知道我可以读取内部 class 中的配置文件,但在装饰器中检查值(并记录警告)。但是 IMO 检查值是业务逻辑而不是基础结构,所以对我来说它属于读取配置文件的同一个 class。

checking the value is business logic and not intfastructure, so to me it belongs in the same class where the config file is read.

显然,我对您的域的了解还不足以质疑该断言的真实性,但是 logging 是域模型的一部分对我来说听起来很奇怪。无论如何,为了争论,让我们假设是这种情况。

但是,应该是这样的,读取配置文件是域逻辑。从文件中读取和操作 数据 很容易成为领域逻辑,而读取文件则是 I/O.

most common approach to Inversion of Control in application architecture is to employ the Ports & Adapters architecture。这种架构的全部意义在于将领域模型与 I/O 以及 non-determinism 的其他来源分离。海报示例是为了展示如何将域模型与其数据库访问分离,但文件访问也完全属于该类别。

在这种特殊情况下,这应该意味着您无论如何都需要一些 IConfigurationReader 接口。这意味着您可以应用装饰器:

public class ValidatingConfigurationReader : IConfigurationReader
{
    private readonly IConfigurationReader reader;
    private readonly ILogger logger;

    public ValidatingConfigurationReader(IConfigurationReader reader, ILogger logger)
    {
        this.reader = reader;
        this.logger = logger;
    }

    public MyConfiguration ReadConfigValues(string filePath)
    {
        var config = this.reader.ReadConfigValues(filePath);

        if (config.OptionalValue == null)
        {
            this.logger.Warn("Optional value not set!");
        }

        return config;
    }
}

这个ValidatingConfigurationReaderclass可以在领域模型中实现,即使底层,file-readingIConfigurationReader实现属于某个I/O层。

不要把 SRP 看得太重,否则你最终会得到函数式编程。如果您担心将日志语句放入其中会使 class 变得混乱,那么您有两个选择。您已经提到的第一个使用装饰器 class 但您不能 access/log 私有内容。第二个选项是使用部分 classes 并将日志记录语句放在单独的 class.