当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《条件记录具有最小的圈复杂度》经验,为你挑选了3个好方法。

在阅读" 你的/你的圈复杂度有什么限制? "之后,我意识到我的很多同事对我们项目的新QA政策非常恼火:每个功能不再有10个圈复杂度.

含义:不超过10'if','else','try','catch'和其他代码工作流程分支语句.对.正如我在' 你测试私人方法吗?',这样的政策有很多好的副作用.

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

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"时,我实际上意味着机能缺失(),与机能缺失()返回一个字符串,并且是一个复杂的方法收集的调用和计算所有类型的日志数据被记录器...与否(因此该问题,并显示义务,以使用条件记录,因此人为增加'圈复杂度'的实际问题......)

我现在得到你们中某些人提出的" 可变函数"点(谢谢John).
注意:java6中的快速测试表明我的varargs函数在被调用之前会对其参数进行求值,所以它不能用于函数调用,而是用于'Log Retriever object'(或'function wrapper'),其中toString( )只有在需要时才会被调用.得到它了.

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



1> erickson..:

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

在大多数情况下,当前的日志框架(如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中,一个小的惩罚就是自动装入传递给记录器的任何原始类型参数.

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

进一步增强

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



2> John Milliki..:

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

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



3> pointernil..:

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

从理论上讲,这是可能的.由于性能问题,我不希望在生产中使用它,因为大量使用lamdas/code block进行日志记录.

但是一如既往:如果有疑问,请测试它并测量对cpu负载和内存的影响.

推荐阅读
牛尾巴2010
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有