Условное ведение журнала с минимальной цикломатической сложностью
Прочитав " Каков ваш / хороший предел для цикломатической сложности? ", Я понимаю, что многие из моих коллег были весьма недовольны этой новой политикой обеспечения качества в нашем проекте: не более 10 цикломатических сложностей на функцию.
Значение: не более 10 'if', 'else', 'try', 'catch' и другие операторы ветвления потока кода. Правильно. Как я объяснил в " Вы тестируете приватный метод? "Такая политика имеет много хороших побочных эффектов.
Но: в начале нашего проекта (200 человек - 7 лет) мы счастливо регистрировались (и нет, мы не можем легко делегировать это какому-то подходу " аспектно-ориентированного программирования " для журналов).
myLogger.info("A String");
myLogger.fine("A more complicated String");
...
И когда первые версии нашей Системы были запущены, у нас возникла огромная проблема с памятью не из-за ведения журнала (которое в какой-то момент было отключено), а из-за параметров журнала (строк), которые всегда рассчитывались, а затем передавались в функции 'info()' или 'fine()' только для того, чтобы обнаружить, что уровень ведения журнала был 'ВЫКЛ' и что регистрация не выполнялась!
Поэтому 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, и являющейся вызовом сложного метода, собирающего и вычисляющего все виды данных журнала, которые должны отображаться регистратором... или нет (отсюда и проблема, и обязанность использовать условное ведение журнала, отсюда и актуальная проблема искусственного увеличения "цикломатической сложности"...)
Теперь я понимаю, что некоторые из вас выдвигают точку зрения " функции вариации " (спасибо, Джон).
Примечание: быстрый тест в java6 показывает, что моя функция varargs оценивает свои аргументы перед вызовом, поэтому она не может быть применена для вызова функции, но для "объекта получения журнала" (или "обертки функции"), для которого toString() будет вызываться только при необходимости. Понял.
Я уже опубликовал свой опыт по этой теме.
Я оставлю это там до следующего вторника для голосования, затем выберу один из ваших ответов.
Еще раз спасибо за все предложения:)
12 ответов
В Python вы передаете отформатированные значения в качестве параметров в функцию регистрации. Форматирование строки применяется только в том случае, если включено ведение журнала. Затраты на вызов функции все еще существуют, но это ничтожно мало по сравнению с форматированием.
log.info ("a = %s, b = %s", a, b)
Вы можете сделать что-то подобное для любого языка с переменными аргументами (C/C++, C#/Java и т. Д.).
Это на самом деле не предназначено для случаев, когда аргументы трудно получить, но для форматирования их в строки стоит дорого. Например, если в вашем коде уже есть список чисел, вы можете записать этот список для отладки. проведение mylist.toString()
займет некоторое время без пользы, так как результат будет выброшен. Итак, вы проходите mylist
в качестве параметра функции регистрации, и пусть он обрабатывает форматирование строки. Таким образом, форматирование будет выполняться только при необходимости.
Поскольку в вопросе OP конкретно упоминается Java, вот как можно использовать приведенное выше:
Я должен настаивать на том, что проблема связана не с "форматированием", а с "оценкой аргументов" (оценка, которая может быть очень дорогостоящей, непосредственно перед вызовом метода, который ничего не будет делать)
Хитрость заключается в том, чтобы иметь объекты, которые не будут выполнять дорогостоящие вычисления до тех пор, пока они не потребуются. Это легко сделать в таких языках, как Smalltalk или Python, которые поддерживают лямбды и замыкания, но все же выполнимо в 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()
вызов.
С текущими фреймворками логирования вопрос спорный
Современные структуры ведения журналов, такие как 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);
С этим охранником, оценка аргументов 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
объект может быть использован вместо "формата" String
, что дает вам дополнительные возможности, такие как выбор формата для более аккуратной обработки кардинальных чисел. Другой альтернативой может быть реализация вашей собственной возможности форматирования, которая вызывает некоторый интерфейс, который вы определяете для "оценки", а не основной toString()
метод.
В языках, поддерживающих лямбда-выражения или кодовые блоки в качестве параметров, одним из решений для этого было бы дать именно это методу регистрации. Он может оценить конфигурацию и только при необходимости вызвать / выполнить указанный блок лямбда / кода. Хотя пока не пробовал.
Теоретически это возможно. Я не хотел бы использовать его в производстве из-за проблем с производительностью, которые я ожидаю с таким интенсивным использованием блоков lamdas / code для ведения журнала.
Но как всегда: если есть сомнения, протестируйте его и измерьте влияние на загрузку процессора и память.
Спасибо за все ваши ответы! Вы, ребята, рок:)
Теперь мой отзыв не так прост, как ваш:
Да, для одного проекта (например, "одна программа развернута и работает самостоятельно на одной производственной платформе"), я полагаю, вы можете использовать все технические данные для меня:
- необходимы выделенные объекты 'Log Retriever', которые могут быть переданы в оболочку Logger только при вызове toString()
- используется вместе с логарифмической переменной (или простым массивом Object[]!)
и вот, у вас это есть, как объяснили @Джон Милликин и @erickson.
Тем не менее, эта проблема заставила нас задуматься о том, почему именно мы вошли в систему?
Наш проект на самом деле представляет собой 30 различных проектов (от 5 до 10 человек каждый), развернутых на различных производственных платформах с асинхронными коммуникационными потребностями и архитектурой центральной шины.
Простое ведение журнала, описанное в этом вопросе, подходило для каждого проекта в начале (5 лет назад), но с тех пор мы должны активизироваться. Введите KPI.
Вместо того, чтобы просить регистратора что-либо регистрировать, мы просим автоматически созданный объект (называемый KPI) зарегистрировать событие. Это простой вызов (myKPI.I_am_signaling_myself_to_you()), и он не должен быть условным (что решает проблему "искусственного увеличения цикломатической сложности").
Этот объект KPI знает, кто его вызывает, и, поскольку он запускается с самого начала приложения, он может извлечь много данных, которые мы ранее вычисляли на месте, когда мы регистрировали.
Кроме того, объект KPI можно отслеживать независимо и вычислять / публиковать по требованию свою информацию на одной и отдельной шине публикации.
Таким образом, каждый клиент может запросить информацию, которую он на самом деле хочет (например, "начался ли мой процесс, и если да, с каких пор?"), Вместо поиска правильного файла журнала и поиска загадочной строки...
Действительно, вопрос "Почему именно мы вошли в первую очередь?" заставили нас понять, что мы работали не только для программиста и его модульных или интеграционных тестов, но и для гораздо более широкого сообщества, включая самих конечных клиентов. Наш "отчетный" механизм должен быть централизованным, асинхронным, круглосуточным.
Специфика этого механизма KPI является выходом за рамки этого вопроса. Достаточно сказать, что правильная калибровка - это, безусловно, единственная сложнейшая нефункциональная проблема, с которой мы сталкиваемся. Это все еще время от времени ставит систему на колени! Однако при правильной калибровке это спасатель жизни.
Еще раз спасибо за все предложения. Мы рассмотрим их для некоторых частей нашей системы, когда простое ведение журнала все еще на месте.
Но другой смысл этого вопроса состоял в том, чтобы проиллюстрировать вам конкретную проблему в гораздо более широком и более сложном контексте.
Надеюсь, тебе понравилось. Я мог бы задать вопрос по KPI (которого, верьте или нет, пока нет в вопросе о SOF!) Позже на следующей неделе.
Я оставлю этот ответ на голосование до следующего вторника, затем выберу ответ (очевидно, не этот;))
Может быть, это слишком просто, но как насчет использования рефакторинга "extract method" вокруг предложения guard? Ваш пример кода этого:
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, чтобы форматирование происходило только при прохождении уровня. Очень похоже на встроенный спринтф. Например:
myLogger.info(Level.INFO,"A String %d",some_number);
Это должно соответствовать вашим критериям.
http://www.scala-lang.org/sites/default/files/newsflash_logo.png
Scala имеет аннотацию @elidable(), которая позволяет удалять методы с флагом компилятора.
С scala REPL:
C:> Scala
Добро пожаловать в Scala версии 2.8.0.final (Java HotSpot(TM) 64-разрядная серверная виртуальная машина, Java 1. 6.0_16). Введите выражения для их оценки. Тип: помощь для получения дополнительной информации.
scala> import scala.annotation.elidable import scala.annotation.elidable
scala> импорт scala.annotation.elidable._ импорт scala.annotation.elidable._
scala> @elidable (FINE) def logDebug (arg: String) = println (arg)
logDebug: (arg: String) Unit
scala> logDebug ("тестирование")
Скала>
С элид-белосеть
C:>scala -Xelide-ниже 0
Добро пожаловать в Scala версии 2.8.0.final (Java HotSpot(TM) 64-разрядная серверная виртуальная машина, Java 1. 6.0_16). Введите выражения для их оценки. Тип: помощь для получения дополнительной информации.
scala> import scala.annotation.elidable import scala.annotation.elidable
scala> импорт scala.annotation.elidable._ импорт scala.annotation.elidable._
scala> @elidable (FINE) def logDebug (arg: String) = println (arg)
logDebug: (arg: String) Unit
scala> logDebug ("тестирование")
тестирование
Скала>
См. Также определение Scala Assert
Условное ведение журнала - это зло. Это добавляет ненужный беспорядок в ваш код.
Вы должны всегда отправлять объекты, которые у вас есть, в регистратор:
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 и вставить строку для вывода. Он будет вызываться только в том случае, если регистратор и обработчик будут регистрироваться на этом уровне.
Для дополнительного удовольствия у вас может быть какой-то язык выражений, чтобы иметь возможность точно контролировать, как форматировать зарегистрированные объекты (toString не всегда может быть полезным).
Рассмотрим функцию ведения журнала...
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;
}
}
);
Как бы я ни ненавидел макросы в C/C++, на работе у нас есть #defines для части if, которая, если false, игнорирует (не оценивает) следующие выражения, но если true возвращает поток, в который материал может быть передан с помощью '<< 'оператор. Как это:
LOGGER(LEVEL_INFO) << "A String";
Я предполагаю, что это устранит дополнительную "сложность", которую видит ваш инструмент, а также исключит любые вычисления строки или любые выражения, которые будут записываться в журнал, если уровень не был достигнут.
Вот элегантное решение с использованием троичного выражения
logger.info(logger.isInfoEnabled()? "Оператор журнала идет сюда...": null);