Условный каротаж с минимальной циклической сложностью

После прочтения "Что вы/хороший предел для циклической сложности?, я понимаю, что многие из моих коллег были очень недовольны этим новым QA в нашем проекте: не более 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");
...

Но теперь, когда уровень цикличности сложности "не может быть перемещен" на один функциональный предел, они утверждают, что различные журналы, которые они вносят в свою функцию, воспринимаются как бремя, потому что каждый "if (isLoggable() )" считается +1 циклической сложностью!

Итак, если функция имеет 8 'if', 'else' и т.д., в одном плотно связанном алгоритме с не легкостью и 3 критических действиях журнала... они нарушают предел, хотя условные журналы могут не быть действительно частью указанной сложности этой функции...

Как бы вы отреагировали на эту ситуацию?
Я видел пару интересных изменений в кодировании (из-за этого "конфликта" ) в моем проекте, но я просто хочу сначала высказать свои мысли.


Спасибо за все ответы.
Я должен настаивать на том, что проблема не связана с форматированием, а связана с "оценкой аргументов" (оценка, которая может быть очень дорогостоящей, перед вызовом метода, который ничего не сделает)
Поэтому, когда написано выше "A String", я на самом деле имел в виду aFunction(), aFunction() возвращал String и являлся вызовом сложного метода сбора и вычисления всех видов данных журнала, которые будут отображаться регистратором... или нет (следовательно, проблема и обязательство использовать условный каротаж, следовательно, актуальная проблема искусственного увеличения "циклической сложности"...)

Теперь я получаю сообщение " variadic функция, продвинутая некоторыми из вас (спасибо John).
Примечание: быстрый тест в java6 показывает, что моя varargs function действительно оценивает свои аргументы перед вызовом, поэтому его нельзя применять для вызова функции, но для "объекта ретрансляции журнала" (или "обертки функций" ), на который toTring() будет вызываться только при необходимости. Получил это.

Теперь я опубликовал свой опыт по этой теме.
Я оставлю его там до следующего вторника для голосования, затем я выберу один из ваших ответов.
Еще раз спасибо за все предложения:)

Ответ 1

В Python вы передаете форматированные значения в качестве параметров для функции ведения журнала. Форматирование строк применяется только при включенном ведении журнала. Там все еще накладные расходы на вызов функции, но это минус по сравнению с форматированием.

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

Вы можете сделать что-то подобное для любого языка с переменными аргументами (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

С текущими фреймворками регистрации вопрос спорный

Существующие рамки ведения журнала, такие как 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 вместо String можно использовать объект MessageFormat, который дает вам дополнительные возможности, такие как формат выбора, для более четкого управления кардинальными числами. Другой альтернативой было бы реализовать собственные возможности форматирования, которые вызывают некоторый интерфейс, который вы определяете для "оценки", а не базовый метод toString().

Ответ 3

В языках, поддерживающих лямбда-выражения или кодовые блоки в качестве параметров, одним из решений для этого было бы дать именно это методу ведения журнала. Это можно было бы оценить конфигурацию и только при необходимости на самом деле вызвать/выполнить предоставленный блок лямбда/кода. Впрочем, еще не попробовать.

Теоретически это возможно. Я бы не хотел использовать его в производстве из-за проблем с производительностью, которые я ожидаю от этого интенсивного использования блоков lamdas/code для регистрации.

Но как всегда: если есть сомнения, проверьте его и измерьте влияние на нагрузку процессора и память.

Ответ 4

В C или С++ я использовал препроцессор вместо инструкций if для условного ведения журнала.

Ответ 5

Спасибо за все ваши ответы! Вы, ребята, рок:)

Теперь моя обратная связь не так прямолинейна, как ваша:

Да, для одного проекта (как в "одной программе, развернутой и запущенной самостоятельно на одной производственной платформе" ), я полагаю, вы можете пойти на все технические вопросы:

  • выделенные объекты "Log Retriever", которые могут быть переданы в оболочку Logger, требуется только вызов toString()
  • используется совместно с каротажной вариационной функцией (или простым массивом Object []!)

и там у вас это есть, как объяснил @Джон Милликин и @erickson.

Однако этот вопрос заставил нас немного подумать о том, "Почему именно мы заходим в первую очередь?"
Наш проект - это фактически 30 различных проектов (по 5-10 человек каждый), развернутых на различных производственных платформах, с асинхронными потребностями связи и центральной архитектурой шины.
Простой журнал, описанный в вопросе, был хорош для каждого проекта в начале (5 лет назад), но с тех пор мы должны активизироваться. Введите KPI.

Вместо того, чтобы просить регистратор регистрировать что-либо, мы запрашиваем автоматически созданный объект (называемый KPI) для регистрации события. Это простой вызов (myKPI.I_am_signaling_myself_to_you()) и необязательно быть условным (что решает проблему "искусственного увеличения сложности циклической сложности" ).

Этот объект KPI знает, кто его называет, и с тех пор, как он запускается с самого начала приложения, он может получить много данных, которые мы ранее вычисляли на месте, когда мы регистрировались.
Кроме того, объект KPI может контролироваться независимо и вычислять/публиковать по требованию его информацию на единой и отдельной шине публикации.
Таким образом, каждый клиент может запросить информацию, которую он действительно хочет (например, "начался мой процесс, и если да, с тех пор?" ), Вместо поиска правильного файла журнала и grepping для критической строки...

В самом деле, вопрос: "Почему именно мы заходим в первую очередь?" заставило нас понять, что мы не регистрировались только для программиста и его подразделения или интеграционных тестов, но для гораздо более широкого сообщества, в том числе и для самих конечных клиентов. Наш механизм отчетности должен был быть централизованным, асинхронным, 24/7.

Специфика этого механизма KPI выходит за рамки этого вопроса. Достаточно сказать, что его правильная калибровка - далеко, руки вниз, самая сложная нефункциональная проблема, с которой мы сталкиваемся. Время от времени оно все еще приносит систему на колено! Правильно откалиброван, однако, это спасатель жизни.

Еще раз спасибо за все предложения. Мы рассмотрим их для некоторых частей нашей системы, когда будет проведен простой журнал.
Но другой момент этого вопроса состоял в том, чтобы проиллюстрировать вам конкретную проблему в гораздо более сложном и сложном контексте. Надеюсь, вам понравилось. Я могу задать вопрос о KPI (который, верьте или нет, пока не находится в вопросе о SOF!) На следующей неделе.

Я оставлю этот ответ до голосования до следующего вторника, затем я выберу ответ (не этот, очевидно;))

Ответ 6

Может быть, это слишком просто, но как насчет использования метода "extract" для рефакторинга вокруг предложения 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 ... */ }

Ответ 7

Пропустите уровень журнала в регистраторе и дайте ему возможность решить, следует ли записывать оператор журнала:

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

UPDATE: Я вижу, что вы хотите условно создать строку журнала без условного оператора. Предположительно во время выполнения, а не времени компиляции.

Я просто скажу, что способ, которым мы решили это, - поместить код форматирования в класс журнала, чтобы форматирование выполнялось только в том случае, если уровень проходит. Очень похоже на встроенный sprintf. Например:

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

Это должно соответствовать вашим критериям.

Ответ 8

alt text 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-Bit Server VM, Java 1. 6.0_16). Введите выражения, чтобы они были оценены. Тип: помощь для получения дополнительной информации.

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) Единица

scala > logDebug ( "тестирование" )

scala >

С elide-beloset

C: > scala -Xelide-ниже 0

Добро пожаловать в scala версию 2.8.0.final(Java HotSpot (TM) 64-Bit Server VM, Java 1. 6.0_16). Введите выражения, чтобы они были оценены. Тип: помощь для получения дополнительной информации.

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) Единица

scala > logDebug ( "тестирование" )

Тестирование

scala >

См. также scala утверждение определения

Ответ 9

Условный журнал - это зло. Это добавляет ненужный беспорядок в ваш код.

Вы всегда должны отправлять объекты, которые у вас есть в журнале:

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 может не всегда быть полезным).

Ответ 10

Насколько я ненавижу макросы в C/С++, на работе у нас есть #defines для if, что если false игнорирует (не оценивает) следующие выражения, но если true возвращает поток, в который может быть передан материал с использованием < оператор. Вот так:

LOGGER(LEVEL_INFO) << "A String";

Я предполагаю, что это устранит лишнюю "сложность", которую видит ваш инструмент, а также исключает любое вычисление строки или любых выражений, которые будут регистрироваться, если уровень не был достигнут.

Ответ 11

Вот элегантное решение, использующее тернарное выражение

logger.info(logger.isInfoEnabled()? "Заявление журнала идет здесь...": null);

Ответ 12

Рассмотрим функцию использования каротажа...

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