条件记录具有最小的圈复杂度

时间:2008-09-19 21:38:37

标签: language-agnostic logging coding-style cyclomatic-complexity

在阅读“What’s your/a good limit for cyclomatic complexity?”之后,我发现很多同事对我们项目中的新QA政策感到非常恼火:每项功能不再增加10 cyclomatic complexity

含义:不超过10'if','else','try','catch'和其他代码工作流程分支语句。对。正如我在'Do you test private method?'中解释的那样,这样的政策有很多好的副作用。

但是:在我们(200人 - 7年)的项目开始时,我们很高兴地记录(不,我们不能轻易地将其委托给某种'Aspect-oriented programming'方法来记录日志)。

myLogger.info("A String");
myLogger.fine("A more complicated String");
...

当我们系统的第一个版本上线时,我们遇到了巨大的内存问题,不是因为日志记录(一次关闭),而是因为日志参数(字符串) ),总是计算,然后传递给'info()'或'fine()'函数,只发现日志记录级别为'OFF',并且没有记录日期!

所以QA回来了,并敦促我们的程序员进行条件记录。总是

if(myLogger.isLoggable(Level.INFO) { myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE) { myLogger.fine("A more complicated String");
...

但是现在,由于每个功能限制的“无法移动”10个圈复杂度级别,他们认为他们在其功能中放入的各种日志被视为负担,因为每个“if(isLoggable() )“被视为+1圈复杂度!

因此,如果一个函数有8'if','else'等等,在一个紧密耦合的不容易共享的算法中,以及3个关键日志操作......它们违反了限制,即使条件日志可能不是真正该功能的复杂性的一部分......

您如何解决这种情况? 在我的项目中,我看到了一些有趣的编码演变(由于'冲突'),但我只是想先了解你的想法。


感谢您的所有答案 我必须坚持认为问题不是“格式化”相关,而是“论证评估”相关(评估可能成本很高,就在调用一个什么都不做的方法之前) 所以当写一个上面的“A String”时,我实际上是指aFunction(),aFunction()返回一个String,并且是一个复杂方法的调用,收集和计算记录器显示的所有类型的日志数据......与否(因此问题,义务使用条件记录,因此人为增加'圈复杂度'的实际问题......)

我现在得到你们中的一些人提出的“variadic功能”(谢谢约翰) 注意:java6中的快速测试表明我的varargs function在被调用之前会对其参数进行求值,所以它不能用于函数调用,而是用于“Log Retriever object”(或“function wrapper”),只有在需要时才会调用toString()。明白了。

我现在已经发表了关于这个主题的经验 我将把它留在那里直到下周二进行投票,然后我会选择你的一个答案 再次感谢您提出的所有建议:)

12 个答案:

答案 0 :(得分:53)

使用当前的日志框架,问题是没有意义的

在大多数情况下,当前的日志框架(如slf4j或log4j 2)不需要保护语句。它们使用参数化日志语句,以便可以无条件地记录事件,但只有在启用事件时才会发生消息格式化。消息构造由记录器根据需要执行,而不是由应用程序预先执行。

如果您必须使用古董日志库,您可以继续阅读以获取更多背景信息以及使用参数化消息改进旧库的方法。

守卫声明是否真的增加了复杂性?

考虑从圈复杂度计算中排除伐木保护语句。

可以说,由于其可预测的形式,条件记录检查确实不会导致代码的复杂性。

不灵活的指标可以让一个优秀的程序员变得糟糕。小心!

假设您的计算复杂性的工具无法适应这种程度,以下方法可能会提供解决方法。

需要条件记录

我认为你的警卫声明是因为你有这样的代码而引入的:

private static final Logger log = Logger.getLogger(MyClass.class);

Connection connect(Widget w, Dongle d, Dongle alt) 
  throws ConnectionException
{
  log.debug("Attempting connection of dongle " + d + " to widget " + w);
  Connection c;
  try {
    c = w.connect(d);
  } catch(ConnectionException ex) {
    log.warn("Connection failed; attempting alternate dongle " + d, ex);
    c = w.connect(alt);
  }
  log.debug("Connection succeeded: " + c);
  return c;
}

在Java中,每个日志语句都会创建一个新的StringBuilder,并在连接到该字符串的每个对象上调用toString()方法。反过来,这些toString()方法可能会创建自己的StringBuilder个实例,并在潜在的大对象图中调用其成员的toString()方法,依此类推。 (在Java 5之前,由于使用了StringBuffer,所以它甚至更加昂贵,并且所有操作都是同步的。)

这可能相对昂贵,特别是如果日志语句位于某些执行严重的代码路径中。并且,如上所述,即使记录器因为日志级别太高而必然会丢弃结果,也会发生昂贵的消息格式化。

这导致引入以下形式的保护声明:

  if (log.isDebugEnabled())
    log.debug("Attempting connection of dongle " + d + " to widget " + w);

使用此保护,仅在必要时才执行参数dw的评估以及字符串连接。

简单,高效记录的解决方案

但是,如果记录器(或您在所选日志包中编写的包装器)采用格式化程序的格式化程序和参数,则可以延迟消息构造,直到确定它将被使用,同时消除保护陈述及其圈复杂度。

public final class FormatLogger
{

  private final Logger log;

  public FormatLogger(Logger log)
  {
    this.log = log;
  }

  public void debug(String formatter, Object... args)
  {
    log(Level.DEBUG, formatter, args);
  }

  … &c. for info, warn; also add overloads to log an exception …

  public void log(Level level, String formatter, Object... args)
  {
    if (log.isEnabled(level)) {
      /* 
       * Only now is the message constructed, and each "arg"
       * evaluated by having its toString() method invoked.
       */
      log.log(level, String.format(formatter, args));
    }
  }

}

class MyClass 
{

  private static final FormatLogger log = 
     new FormatLogger(Logger.getLogger(MyClass.class));

  Connection connect(Widget w, Dongle d, Dongle alt) 
    throws ConnectionException
  {
    log.debug("Attempting connection of dongle %s to widget %s.", d, w);
    Connection c;
    try {
      c = w.connect(d);
    } catch(ConnectionException ex) {
      log.warn("Connection failed; attempting alternate dongle %s.", d);
      c = w.connect(alt);
    }
    log.debug("Connection succeeded: %s", c);
    return c;
  }

}

现在,除非必要,否则不会发生任何带缓冲区分配的级联toString()调用!这有效地消除了导致保护声明的性能损失。在Java中,一个小的惩罚就是自动装入传递给记录器的任何原始类型参数。

执行日志记录的代码可以说比以前更清晰,因为不整齐的字符串连接已经消失。如果格式字符串被外部化(使用ResourceBundle),它甚至可以更清晰,这也可以帮助维护或本地化软件。

进一步改进

另请注意,在Java中,可以使用MessageFormat对象代替“格式”String,这为您提供了更多功能,例如选择格式,以便更整齐地处理基数。另一种方法是实现自己的格式化功能,调用您为“评估”定义的某个接口,而不是基本的toString()方法。

答案 1 :(得分:30)

在Python中,您将格式化的值作为参数传递给日志记录功能。仅在启用日志记录时才应用字符串格式。仍然存在函数调用的开销,但与格式化相比,这是微不足道的。

log.info ("a = %s, b = %s", a, b)

对于任何具有可变参数的语言(C / C ++,C#/ Java等),您都可以这样做。


这并不适用于难以检索参数的情况,但是在将它们格式化为字符串时非常昂贵。例如,如果您的代码中已包含数字列表,则可能需要记录该列表以进行调试。执行mylist.toString()需要一段时间才能获益,因为结果将被丢弃。因此,您将mylist作为参数传递给日志记录函数,并让它处理字符串格式。这样,只有在需要时才会执行格式化。


由于OP的问题特别提及Java,以下是如何使用上述内容:

  

我必须坚持认为问题不是“格式化”相关,而是“参数评估”相关(评估可能成本很高,就在调用一个什么都不做的方法之前)

诀窍是让对象在绝对需要之前不会执行昂贵的计算。这在Smalltalk或Python等支持lambdas和闭包的语言中很容易,但在Java中仍然可以用一些想象力来实现。

假设您有一个功能get_everything()。它会将数据库中的每个对象检索到一个列表中。如果显然会丢弃结果,你不想调用它。因此,不是直接调用该函数,而是定义一个名为LazyGetEverything的内部类:

public class MainClass {
    private class LazyGetEverything { 
        @Override
        public String toString() { 
            return getEverything().toString(); 
        }
    }

    private Object getEverything() {
        /* returns what you want to .toString() in the inner class */
    }

    public void logEverything() {
        log.info(new LazyGetEverything());
    }
}

在此代码中,对getEverything()的调用被包装,以便在需要之前不会实际执行。仅当启用调试时,日志记录功能才会对其参数执行toString()。这样,您的代码只会遭受函数调用的开销而不是完整的getEverything()调用。

答案 2 :(得分:6)

在支持lambda表达式或代码块作为参数的语言中,一种解决方案就是将其提供给日志记录方法。那个人可以评估配置,只有在需要实际调用/执行提供的lambda /代码块时。 但是还没试过。

理论上这是可能的。我不希望在生产中使用它,因为性能问题,我期望大量使用lamdas /代码块进行日志记录。

但与往常一样:如果有疑问,请测试它并测量对cpu负载和内存的影响。

答案 3 :(得分:4)

感谢您的所有答案!你们摇滚:)

现在我的反馈并不像你的那样直截了当:

是的,对于一个项目(如'在一个生产平台上部署和运行的一个程序'),我想你可以全力以赴地了解我:

  • 专用的“Log Retriever”对象,可以传递给只调用toString()的Logger包装器
  • 与日志variadic function(或普通的Object []数组一起使用!)

并且你有它,正如@John Millikin和@erickson所解释的那样。

但是,这个问题迫使我们想一想'为什么我们首先要登录?' 我们的项目实际上是在各种生产平台上部署的30个不同项目(每个5到10人),具有异步通信需求和中央总线架构。 问题中描述的简单日志记录对于每个项目在开始(5年前)都很好,但从那时起,我们必须加强。输入KPI

我们要求自动创建的对象(称为KPI)来注册事件,而不是要求记录器记录任何内容。这是一个简单的调用(myKPI.I_am_signaling_myself_to_you()),并且不需要是条件的(这解决了“人为增加的圈复杂度”问题)。

该KPI对象知道谁调用它,并且因为他从应用程序的开头运行,他能够检索我们以前在记录时当场计算的大量数据。
此外,可以独立监控KPI对象,并根据需要在单个和单独的发布总线上计算/发布其信息 这样,每个客户端都可以询问他实际想要的信息(例如,'我的流程是否开始,如果是,从何时开始?'),而不是寻找正确的日志文件并为一个神秘的字符串... ... / p>

确实,问题是'为什么我们确实在首先登录?'让我们意识到我们不仅仅是为了程序员和他的单元或集成测试,而是为了更广泛的社区,包括一些最终客户自己。我们的“报告”机制必须是集中的,异步的,全天候。

该KPI机制的具体内容超出了本问题的范围。我只想说它适当的校准是迄今为止,我们面临的最复杂的非功能性问题。它仍然会不时地使系统处于膝盖状态!然而,正确校准,它可以节省生命。

再次感谢您提出的所有建议。当简单的记录仍然存在时,我们将考虑它们用于系统的某些部分 但这个问题的另一点是在更大,更复杂的背景下向你说明一个具体的问题 希望你喜欢它。我可能会问一个关于KPI的问题(到目前为止,不论是否相信,对SOF没有任何疑问!)。下周晚些时候。

我会把这个答案留到下周二投票,然后我会选择一个答案(显然不是这个答案;)

答案 4 :(得分:4)

也许这太简单了,但是使用“提取方法”围绕保护条款进行重构呢?您的示例代码:

public void Example()
{
  if(myLogger.isLoggable(Level.INFO))
      myLogger.info("A String");
  if(myLogger.isLoggable(Level.FINE))
      myLogger.fine("A more complicated String");
  // +1 for each test and log message
}

成为这个:

public void Example()
{
   _LogInfo();
   _LogFine();
   // +0 for each test and log message
}

private void _LogInfo()
{
   if(!myLogger.isLoggable(Level.INFO))
      return;

   // Do your complex argument calculations/evaluations only when needed.
}

private void _LogFine(){ /* Ditto ... */ }

答案 5 :(得分:3)

在C或C ++中,我使用预处理器而不是条件记录的if语句。

答案 6 :(得分:3)

将日志级别传递给记录器,让它决定是否编写日志语句:

//if(myLogger.isLoggable(Level.INFO) {myLogger.info("A String");
myLogger.info(Level.INFO,"A String");

更新:啊,我看到你想要在没有条件语句的情况下有条件地创建日志字符串。大概是在运行时而不是编译时。

我只想说我们解决这个问题的方法是将格式化代码放在logger类中,这样只有在级别通过时才会进行格式化。非常类似于内置的sprintf。例如:

myLogger.info(Level.INFO,"A String %d",some_number);   

这应符合您的标准。

答案 7 :(得分:2)

alt text http://www.scala-lang.org/sites/default/files/newsflash_logo.png

Scala有一个annontation @elidable(),允许您使用编译器标志删除方法。

使用scala REPL:

  

C:> scala

     

欢迎使用Scala版本2.8.0.final(Java HotSpot(TM)64位服务器VM,Java 1。   6.0_16)。   输入表达式以对其进行评估。   键入:帮助以获取更多信息。

     

阶> import scala.annotation.elidable   import scala.annotation.elidable

     

阶> import scala.annotation.elidable._   import scala.annotation.elidable ._

     

阶> @elidable(FINE)def logDebug(arg:String)= println(arg)

     

logDebug:(arg:String)单位

     

阶> logDebug( “测试”)

     

阶>

有elide-beloset

  

C:> scala -Xelide-below 0

     

欢迎使用Scala版本2.8.0.final(Java HotSpot(TM)64位服务器VM,Java 1。   6.0_16)。   输入表达式以对其进行评估。   键入:帮助以获取更多信息。

     

阶> import scala.annotation.elidable   import scala.annotation.elidable

     

阶> import scala.annotation.elidable._   import scala.annotation.elidable ._

     

阶> @elidable(FINE)def logDebug(arg:String)= println(arg)

     

logDebug:(arg:String)单位

     

阶> logDebug( “测试”)

     

测试

     

阶>

另见Scala assert definition

答案 8 :(得分:2)

条件记录是邪恶的。它会给你的代码带来不必要的混乱。

您应该始终将您拥有的对象发送到记录器:

Logger logger = ...
logger.log(Level.DEBUG,"The foo is {0} and the bar is {1}",new Object[]{foo, bar});

然后有一个java.util.logging.Formatter,它使用MessageFormat来展平foo并将bar放入要输出的字符串中。只有在记录器和处理程序将在该级别登录时才会调用它。

为了增加乐趣,您可以使用某种表达式语言来控制如何格式化已记录的对象(toString可能并不总是有用)。

答案 9 :(得分:1)

尽管我讨厌C / C ++中的宏,但在工作中我们为if部分设置了#defines,如果false则忽略(不评估)以下表达式,但如果为true则返回一个可以通过管道传输东西的流使用'<<<运营商。 像这样:

LOGGER(LEVEL_INFO) << "A String";

我认为这样可以消除工具看到的额外“复杂性”,并且还可以消除字符串的任何计算,或者在未达到级别时记录的任何表达式。

答案 10 :(得分:1)

这是使用三元表达式

的优雅解决方案

logger.info(logger.isInfoEnabled()?“Log Statement to here ...”:null);

答案 11 :(得分:1)

考虑一个日志工具函数......

void debugUtil(String s, Object… args) {
   if (LOG.isDebugEnabled())
       LOG.debug(s, args);
   }
);

然后使用“封闭”进行调用,围绕您想要避免的昂贵评估。

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);