方法中的日志记录以何种方式容易出错且具有侵入性?

In what way is logging within a method error-prone and intrusive?

我正在读这本书 Adaptive Code via C#,作者 Gary McLean Hall 说:

[Logging, and Transactional Code,] is laborious and error-prone, and it instantly pollutes every method with irrelevant boilerplate code, increasing the noise-to-signal ratio. Instead, you can factor out such cross-cutting concerns into encapsulated functionality and apply them to the code in a much less invasive fashion. The most common way of adding functionality non-invasively is through aspect-oriented programming.

对于此代码:

public void OpenNewAccount(Guid ownerID, string accountName, decimal openingBalance)
{
    log.WriteInfo("Creating new account for owner {0} with name '{1}' and an opening balance of {2}", ownerID, accountName, openingBalance");

    using(var transaction = session.BeginTransaction())
    {
        var user = userRepository.GetByID(ownerID);
        user.CreateAccount(accountName);
        var account = user.FindAccount(accountName);
        account.SetBalance(opening Balance);

        transaction.Commit();
    }
}

我知道它很费力,而且它做了方法名没有指明的事情,比如日志记录,但我不明白为什么它容易出错。可能是因为你经常重复自己?

他的建议是改为使用此代码:

[Logged]
[Transactional]
public void OpenNewAccount(Guid ownerID, string accountName, decimal openingBalance)
{
    var user = userRepository.GetByID(ownerID);
    user.CreateAccount(accountName);
    var account = user.FindAccount(accountName);
    account.SetBalance(opening Balance);
}

但是如果你只是简单地寻找一个通用的记录器,那么你可以调用像_log.Log()这样的方法,这是相对相同的工作量。除非属性具有检查方法实现的能力,否则在这种情况下我看不出它们有多大价值,只是它们提供了稍微更具描述性的代码。

我同意作者的观点,它很容易出错。一个非常重要的原因是简单。仅通过观察您在此处提供的两个示例,很明显第二种实现更容易维护和理解。想象一下向该方法添加一个新参数:(我假设 Logged 属性也负责记录参数),如果您要使用代码在方法中记录它,您必须在脑海中解析格式字符串,添加参数等所做的任何更改都容易出错。另外,当你在多个方法中编写这段代码时,很容易有人遗漏一些参数and/or 参数传递顺序错误

任何时候你需要你的代码为一个操作执行多个动作,如果你忘记了一个或搞砸了参数,它就是潜在错误的来源。如果您添加的代码重复但不完全相同,则尤其如此。在十几个地方做,一切看起来都一样。当您 copy/pasted 来自其他地方的代码时,您将需要很长时间才能确定您忘记更改参数。

与属性一起使用的声明性方法消除了所有这些错误来源。直到您花几个小时追查一个错误才发现您在 copy/paste 之后忘记编辑,您才会意识到这些好处。这是一种体验,很大程度上取决于您正在使用的代码的大小。对于小代码没有区别,添加显式代码更容易理解,尤其是在您学习的时候。在大型代码库中,每行都需要大量维护,因此可能会引入错误,因为附近代码的每次更改都可能需要更改样板代码。

至于污染代码,也是如此。您必须浏览这些以专注于代码的主要目的。样板代码只是妨碍。同样,大小取决于代码库的大小和复杂性。

我建议你做对你有用的事情,当你开始觉得必须有更好的方法时,然后重新阅读旧的建议,看看它们现在是否更有意义。试图把它理解为一个假设性的问题几乎是不可能的,而且在你得到战斗伤疤之前是没有意义的,让它变得个人化和相关化。

以日志为例cross-cutting concern。这被认为是横切,因为日志记录策略会影响记录系统的每个部分(至少它必须考虑到这一点)。在这方面,系统的每个部分都需要考虑一些事情,这意味着更改日志记录意味着您 可能 必须更改系统记录的每个部分,(对于例如添加更多相关信息)。 (此外,如果您忘记记录重要的内容,您可以称之为容易出错)。在实践中,日志记录通常是良性的,因此它是一个乏善可陈的例子,并且可以称其为容易出错。

我认为一个更好的例子是检查安全性。我不认为看到到处剪切和粘贴安全代码很容易出错,并且更新该代码可能变得难以正确。

如果您需要更改 security/permissions 的实施方式(在非 Aspect 世界中),您(可以想象)需要重新访问每个进行安全检查的地方,因为安全检查是分散的在整个程序中。从 design/maintenance 的角度来看,复杂的设计很难解释所有可能的交互。如果您有一个 [CheckSecurity] 属性,那么纠缠就会最少,并且永远不需要重新访问需要检查安全性的每个函数。

这里的关键是 Aspects 会在编译 time/run 时根据检查更改代码。因此,如果您需要更改检查安全凭证的方式,您只需转到代码中的一个位置。这与仅使用单个 method/function 进行安全检查有很大不同,因为函数无法检查其堆栈并根据调用者更改其行为。 c#中的代码注入可以。

这是一个 PostSharp example1 example2 and a podcast