有条件的记录最少的循环的复杂性
-
01-07-2019 - |
题
在阅读"什么是你一个很好的限制的圈复杂性?"我意识到我的许多同事非常烦恼这个新的 QA 策略上我们的项目:没有更多的10 圈复杂度 每个功能。
意义:不超过10'如果','其他','尝试','抓'和其他代码的工作流程的分支发言。正确的。正如我在解释'做你的测试的私人方法?',这种策略具有许多良好的副作用。
但是:在我们开始(200人--7年之久)的项目,我们高兴地记录(不,我们不能容易地委派到某种'面向方面的编程'办法,为日志)。
myLogger.info("A String");
myLogger.fine("A more complicated String");
...
当第一个版本的我们的系统去现场,我们经历了巨大的存储器的问题不是因为记录(这一点的关闭),但是因为 日志的参数 (strings),它们总是计算,然后传递到的信息()'或'收()'功能,只发现的水平记录为"关闭",而且没有记录都是把地方!
所以QA回来了并且敦促我们的程序员做到有条件的记录。总是如此。
if(myLogger.isLoggable(Level.INFO) { myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE) { myLogger.fine("A more complicated String");
...
但现在,'可以-不可移动'10圈复杂程度每个功能限制,他们认为,各种日志,他们把在自己的功能被认为是一种负担,因为每一个"如果(isLoggable())"被计为+1圈复杂!
因此,如果一个功能都有8'如果','其他'等等,在一个紧密联接不容易-共享的算法,以及3个关键日志操作...他们违反限制,即使有条件的记录可能不是 真的 部分所述的复杂的功能...
你会如何处理这一情况?
我已经看到一些有趣的编码进化(由于是'冲突')在我的项目,但是我只想得到你的想法的第一个。
谢谢你所有的答案。
我必须坚持认为,问题不是'格式相关,但是'参数评价相关(评估可能非常昂贵的事,只是前呼吁的方法,这将做什么)
因此,当一个上面写的"String",我实际上意味着一个函(),与一个函()返回串,并正在呼吁一个复杂的方法收集和计算所有类型的登录的数据以显示通过该记录器...或者没有(因此这一问题, 义务 使用条件记录,因此实际问题的人为增加的'循环的复杂性'...)
我现在得到'可变 功能的角先进通过你们中的一些(谢谢你约翰*).
注:一个快速测试在java6显示了我 varargs功能 不会评估它的参数以前被称,因此它不能适用于功能的电话,但是对于'志猎犬目'(或'功能包装'),对其中的toString()将只能被称为如果需要的话。得到它。
我现在已经公布我的经验这个问题。
我将它留在那里,直到下周二进行表决,然后,我会选择一个你的答案。
再次感谢您所有的建议:)
解决方案
在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()
。这样,您的代码只会遭受函数调用的开销而不是完整的<=>调用。
其他提示
使用当前的日志框架,问题是没有意义的
在大多数情况下,当前的日志框架(如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()
方法。反过来,这些StringBuffer
方法可能会创建自己的d
实例,并在潜在的大对象图中调用其成员的w
方法,等等。 (在Java 5之前,它甚至更昂贵,因为ResourceBundle
被使用,并且所有操作都是同步的。)
这可能相对昂贵,特别是如果日志语句位于某些执行严重的代码路径中。并且,如上所述,即使记录器因为日志级别太高而必然会丢弃结果,也会发生昂贵的消息格式化。
这导致引入以下形式的保护声明:
if (log.isDebugEnabled())
log.debug("Attempting connection of dongle " + d + " to widget " + w);
使用此保护,仅在必要时执行参数MessageFormat
和String
以及字符串连接的评估。
简单,高效记录的解决方案
但是,如果记录器(或您在所选日志包中编写的包装器)采用格式化程序的格式化程序和参数,则可以延迟消息构造,直到确定它将被使用,同时消除保护陈述及其圈复杂度。
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;
}
}
现在,除非必要,否则不会发生任何带缓冲区分配的级联<=>调用!这有效地消除了导致保护声明的性能损失。在Java中,一个小的惩罚就是自动装入传递给记录器的任何原始类型参数。
执行日志记录的代码可以说比以前更清晰,因为不整齐的字符串连接已经消失。如果格式化字符串被外部化(使用<=>),它甚至可以更清晰,这也可以帮助维护或本地化软件。
进一步改进
另请注意,在Java中,可以使用<=>对象代替<!> quot; format <!> quot; <=>,它为您提供了更多功能,例如选择格式,以便更整齐地处理基数。另一种方法是实现您自己的格式化功能,该功能调用您为<!> quot; evaluation <!>“;而不是基本<=>方法定义的某个接口。
在支持lambda表达式或代码块作为参数的语言中,一种解决方案就是将其提供给日志记录方法。那个人可以评估配置,只有在需要实际调用/执行提供的lambda /代码块时。 但是还没试过。
理论上这是可能的。我不希望在生产中使用它,因为性能问题,我期望大量使用lamdas /代码块进行日志记录。
但与往常一样:如果有疑问,请测试它并测量对cpu负载和内存的影响。
谢谢你的答案!你们摇滚:)
现在我的反馈意见是不为直接的作为你的:
是的,对 一个项目 (如在一个计划的部署和运行对其自己在一个生产平台'),我猜你可以去的所有技术上的我:
- 专用的'日志猎对象,这可以通过一个记录器包装只叫toString()是必要的
- 结合使用记录 可变的功能 (或者一个普通的对象[]array!)
和你有它,作为解释@约翰*米利金和@埃里克森。
然而,这个问题迫使我们思考一些关于'为什么我们被记录在首位?'
我们的项目实际上是30个不同的项目(5至10人)部署在各个生产平台,用异步通信的需求和中央总的架构。
简单的记录中描述的问题是,现为每个项目 在开始 (5年前),但自那时以来,我们已经步骤。输入 KPI.
而不是要求一个记录日志的任何东西,我们要求自动创建的对象(称为KPI)登记的一个事件。这是一个简单的电话(myKPI.I_am_signaling_myself_to_you()),并且不需要被有条件(其解决'人为增加的循环的复杂性'问题)。
该KPI对象知道谁叫它,因为他从一开始就应用程序,他能够检索大量的数据我们先前计算在现场的时候,我们都记录。
再加上那KPI对象可以是独立监测和计算/发布关于其需求的信息在一个单一的和单独的出版物车。
这样,每个客户可以要求的信息实际上,他希望(一样,'我开始的进程,如果是的话,什么时候开始的?'), 而不是寻找正确记录文件和grepping为一个隐晦的字符串...
事实上,问题'为什么我们被记录在首位?'让我们认识到,我们没有记录,只是对程序员和他的单位或一体化试验,但对于一个更广泛的社区包括一些最后的客户自己。我们的'报告'的机构必须集中、异步24/7.
具体的KPI机构的方式的范围这一问题。我只想说其适当的校正是迄今为止,双手放下来,一个最复杂的非功能性的问题上,我们面对的问题。它仍然不会带来系统在其膝时!正确的校准,然而,它是一个生命的保护。
再次感谢你的所有建议。我们会考虑他们的一些部分我们的系统当简单的记录仍然有效。
但其他的问题是说明你的特定问题中的一个更大和更复杂的上下文。
希望你喜欢它。我可能会问一个问题上KPI(其中,信不信由你,不是在任何问题上SOF迄今为止!) 以后的下一星期。
我将离开这个回答了投票,直到下个星期二,然后我会选择一个答复(不是这样的一个明显))
也许这太简单了,但是如何使用<!> quot; extract方法<!>围绕守卫条款进行重构?您的示例代码:
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 ... */ }
在C或C ++中,我使用预处理器而不是条件记录的if语句。
将日志级别传递给记录器,让它决定是否编写日志语句:
//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);
这应符合您的标准。
alt text http://www.scala-lang.org /sites/default/files/newsflash_logo.png
Scala 有一个公告 @ elidable(),它允许您使用编译器标志删除方法。
使用scala REPL:
C:<!> gt; scala
欢迎使用Scala版本2.8.0.final(Java HotSpot(TM)64位服务器VM,Java 1。 6.0_16)。 输入表达式以对其进行评估。 键入:帮助以获取更多信息。
<!>阶GT; import scala.annotation.elidable import scala.annotation.elidable
<!>阶GT; import scala.annotation.elidable._ import scala.annotation.elidable ._
<!>阶GT; @elidable(FINE)def logDebug(arg:String)= println(arg)
logDebug:(arg:String)单位
<!>阶GT; logDebug(<!> QUOT; <!>测试QUOT)
阶GT <!>;
有elide-beloset
C:<!> gt; scala -Xelide-below 0
欢迎使用Scala版本2.8.0.final(Java HotSpot(TM)64位服务器VM,Java 1。 6.0_16)。 输入表达式以对其进行评估。 键入:帮助以获取更多信息。
<!>阶GT; import scala.annotation.elidable import scala.annotation.elidable
<!>阶GT; import scala.annotation.elidable._ import scala.annotation.elidable ._
<!>阶GT; @elidable(FINE)def logDebug(arg:String)= println(arg)
logDebug:(arg:String)单位
<!>阶GT; logDebug(<!> QUOT; <!>测试QUOT)
测试
阶GT <!>;
另请参阅 Scala断言定义
条件记录是邪恶的。它会给你的代码带来不必要的混乱。
您应该始终将您拥有的对象发送到记录器:
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可能并不总是有用)。
尽管我讨厌C / C ++中的宏,但在工作中我们为if部分设置了#defines,如果false则忽略(不评估)以下表达式,但如果为true则返回一个可以通过管道传输东西的流使用'<!> lt; <!> lt;'运营商。 像这样:
LOGGER(LEVEL_INFO) << "A String";
我认为这样可以消除工具看到的额外“复杂性”,并且还可以消除字符串的任何计算,或者在未达到级别时记录的任何表达式。
这是使用三元表达式
的优雅解决方案logger.info(logger.isInfoEnabled()?<!>“; Log Statement在这里...... <!> quot;:null);
考虑一个日志工具函数......
void debugUtil(String s, Object… args) {
if (LOG.isDebugEnabled())
LOG.debug(s, args);
}
);
然后使用<!> quot; closure <!>进行调用;围绕您想要避免的昂贵评估。
debugUtil(“We got a %s”, new Object() {
@Override String toString() {
// only evaluated if the debug statement is executed
return expensiveCallToGetSomeValue().toString;
}
}
);