有条件的记录,具有最小的循环复杂性

时间:2020-03-06 14:27:59  来源:igfitidea点击:

在阅读了"我们/对环复杂性有什么好的限制?"之后,我意识到我的许多同事对我们项目中的新质量保证政策感到非常恼火:每个功能不再有10个环复杂性。

含义:不超过10个'if','else','try','catch'和其他代码工作流分支语句。正确的。正如我在"我们测试私有方法吗?"中所解释的那样,这样的策略具有许多良好的副作用。

但是:在我们的项目(长达200名员工,历时7年)开始时,我们很高兴地进行日志记录(而且不行,我们不能轻易地将其委托给某种"面向方面的编程"日志方法)。

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

当我们的系统的第一个版本上线时,我们遇到了巨大的内存问题,这不是因为日志记录(在某个时间点已关闭),而是因为始终计算并随后传递给的日志参数(字符串) " info()"或者" fine()"函数,只是发现日志记录级别为" OFF",并且没有进行日志记录!

因此,质量检查人员回来了,并敦促我们的程序员进行条件记录。总是。

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,并且是对复杂方法的调用,该方法收集并计算记录器要显示的各种日志数据...是否存在(因此,以及使用条件日志记录的义务,因此是人为地增加"循环复杂性"的实际问题...)

现在我得到了一些人提出的"可变函数"要点(谢谢约翰)。
注意:在Java6中进行的快速测试表明,我的varargs函数在调用之前确实会对它的参数进行了评估,因此它不能用于函数调用,只能用于"日志检索器对象"(或者"函数包装器"),在其上toString( )仅在需要时调用。知道了。

我现在已经发布了有关该主题的经验。
我将其保留到下周二进行投票,然后选择答案之一。

解决方案

再次感谢所有建议:)

在C或者C ++中,我将使用预处理器代替条件记录的if语句。

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

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

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

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

我只想说我们解决此问题的方法是将格式化代码放入logger类,以便仅在级别通过时才进行格式化。与内置sprintf非常相似。例如:

对于当前的日志记录框架,这个问题尚无定论

那应该符合标准。

当前的日志记录框架(例如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,并且它的所有操作都是同步的,因此成本甚至更高。)

这可能是相对昂贵的,特别是如果log语句在某些执行频繁的代码路径中。而且,如上所述,即使由于日志级别过高而使记录器不得不丢弃结果,也会发生昂贵的消息格式化。

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

这导致引入了以下形式的警卫声明:

简单有效的日志记录解决方案

使用此保护措施,仅在必要时才执行对参数d和w以及字符串连接的评估。

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对象代替" format"String,它为我们提供了其他功能,例如选择格式以更整洁地处理基数。另一种选择是实现自己的格式化功能,该功能调用为"求值"定义的某些接口,而不是基本的" toString()"方法。

LOGGER(LEVEL_INFO) << "A String";

尽管我讨厌C / C ++中的宏,但在工作中我们对if部分使用#defines定义,如果if false忽略(不求值)以下表达式,但是如果true则返回一个流,可以使用' <<'运算符。
像这样:

我认为这将消除工具所看到的额外"复杂性",并且还将消除对字符串的任何计算,或者消除未达到该级别时要记录的任何表达式。

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

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

但还是一如既往:如果有疑问,请对其进行测试并衡量对CPU负载和内存的影响。

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

在Python中,我们将格式化后的值作为参数传递给日志记录函数。仅在启用日志记录的情况下才应用字符串格式。函数调用仍然有开销,但是与格式化相比,这是微不足道的。

我们可以对带有可变参数(C / C ++,C#/ Java等)的任何语言执行类似的操作。

当参数难于检索时,这并不是真正的目的,但是将其格式化为字符串时则很昂贵。例如,如果代码中已经有一个数字列表,则可能要记录该列表以进行调试。执行mylist.toString()将花费一些时间,没有任何好处,因为结果将被丢弃。因此,我们可以将" mylist"作为参数传递给日志记录功能,并让其处理字符串格式。这样,仅在需要时才执行格式化。

I must insist that the problem is not 'formatting' related, but 'argument evaluation' related (evaluation that can be very costly to do, just before calling a method which will do nothing)

由于OP的问题专门提到Java,因此可以使用以下方法:

诀窍是要有一些在绝对需要之前不会执行昂贵计算的对象。在支持lambda和闭包的Smalltalk或者Python之类的语言中,这很容易,但是在Java中仍然可以想象得到。

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());
    }
}

假设我们有一个函数get_everything()。它将把数据库中的每个对象检索到一个列表中。显然,如果结果将被丢弃,则我们不想调用此方法。因此,我们可以定义一个名为LazyGetEverything的内部类,而不是直接使用对该函数的调用:

在这段代码中,对getEverything()的调用已被包装,因此只有在需要时才真正执行它。仅在启用调试的情况下,日志记录函数才会在其参数上执行toString()。这样,代码将仅遭受函数调用的开销,而不是完整的getEverything()调用。

感谢所有回答!你们好棒 :)

现在,我的反馈不像反馈那样直接:

  • 专用的"日志检索器"对象,可以将其传递给Logger包装器,仅需要调用toString()
  • 与日志记录可变参数函数(或者简单的Object []数组一起使用)!

是的,对于一个项目(例如"在单个生产平台上部署并独立运行一个程序"),我想我们可以掌握所有技术:

就像@John Millikin和@erickson所解释的那样,就在那里。

但是,这个问题迫使我们思考一下"为什么我们首先要登录?"
我们的项目实际上是30个不同的项目(每个5至10个人)部署在各种生产平台上,具有异步通信需求和中央总线体系结构。
问题中描述的简单日志记录在开始时(5年前)对每个项目都适用,但是从那时起,我们必须加紧努力。输入KPI。

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

该KPI对象知道调用它的人,并且由于他是从应用程序开始运行的,因此他能够在记录日志时检索我们先前在现场计算的大量数据。
此外,该KPI对象可以独立监视,并根据需要在单个和单独的发布总线上计算/发布其信息。
这样,每个客户都可以要求他真正想要的信息(例如,"我的过程是否已经开始,如果是,从什么时候开始?"),而不是寻找正确的日志文件并为一个神秘的字符串grepping ...

确实,这个问题"为什么我们首先要登录?"使我们意识到,我们不仅要为程序员及其单元测试或者集成测试进行日志记录,还要为更广泛的社区(包括某些最终客户本身)进行日志记录。我们的"报告"机制必须是集中式的,异步的24/7.

该KPI机制的具体含义超出了此问题的范围。可以说,到目前为止,正确的校准是我们面临的唯一最复杂的非功能性问题。它仍然会不时地使系统崩溃!正确校准后,它可以挽救生命。

再次感谢所有建议。当简单的日志记录仍然存在时,我们将在系统的某些部分中考虑它们。
但是,这个问题的另一点是要在更大更复杂的环境中向我们说明一个特定的问题。
希望你喜欢它。下周晚些时候,我可能会问一个关于KPI的问题(无论是否相信,到目前为止,关于SOF都没有问题!)。

我将这个答案留给投票直到下个星期二,然后我将选择一个答案(显然不是这个答案;))

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 ... */ }

变成这个:

这是一个使用三元表达式的优雅解决方案

logger.info(logger.isInfoEnabled()?"日志语句在这里...":null);

替代文字http://www.scala-lang.org/sites/default/files/newsflash_logo.png

Scala具有一个@elidable()注释,该注释使我们可以删除带有编译器标志的方法。

C:>scala 
  
  Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.
  6.0_16).
  Type in expressions to have them evaluated.
  Type :help for more information.
  
  scala> import scala.annotation.elidable
  import scala.annotation.elidable
  
  scala> import scala.annotation.elidable._
  import scala.annotation.elidable._
  
  scala> @elidable(FINE) def logDebug(arg :String) = println(arg)
  
  logDebug: (arg: String)Unit
  
  scala> logDebug("testing")
  
  scala>

使用scala REPL:

C:>scala -Xelide-below 0
  
  Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.
  6.0_16).
  Type in expressions to have them evaluated.
  Type :help for more information.
  
  scala> import scala.annotation.elidable
  import scala.annotation.elidable
  
  scala> import scala.annotation.elidable._
  import scala.annotation.elidable._
  
  scala> @elidable(FINE) def logDebug(arg :String) = println(arg)
  
  logDebug: (arg: String)Unit
  
  scala> logDebug("testing")
  
  testing
  
  scala>

与埃莱德·贝洛斯特

段落数量不匹配