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

StackOverflow https://stackoverflow.com/questions/105852

Вопрос

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

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

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

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


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

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

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

Это было полезно?

Решение

В 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, в большинстве случаев не требуют инструкций guard.Они используют параметризованный оператор log, чтобы событие могло регистрироваться безоговорочно, но форматирование сообщения происходит только в том случае, если событие включено.Построение сообщения выполняется по мере необходимости регистратором, а не упреждающим образом приложением.

Если вам приходится использовать устаревшую библиотеку ведения журнала, вы можете продолжить чтение, чтобы получить дополнительную информацию и способ дооснастить старую библиотеку параметризованными сообщениями.

Действительно ли инструкции guard усложняют работу?

Рассмотрите возможность исключения инструкций logging guardians из расчета цикломатической сложности.

Можно было бы возразить, что из-за своей предсказуемой формы проверки условного ведения журнала на самом деле не вносят вклад в сложность кода.

Негибкие показатели могут привести к тому, что хороший программист в остальном станет плохим.Будь осторожен!

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

Необходимость условного ведения журнала

Я предполагаю, что ваши охранные инструкции были введены потому, что у вас был подобный код:

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 каждый из операторов log создает новый StringBuilder, и вызывает toString() метод для каждого объекта, объединенного со строкой.Эти toString() методы, в свою очередь, скорее всего, создадут StringBuilder собственные экземпляры и вызывают toString() методы их членов и так далее по всему потенциально большому объектному графу.(До Java 5 это было еще дороже, поскольку StringBuffer был использован, и все его операции синхронизированы.)

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

Это приводит к введению защитных инструкций вида:

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

С помощью этого средства защиты оценка аргументов d и w и конкатенация строк выполняется только тогда, когда это необходимо.

Решение для простого и эффективного ведения журнала

Однако, если регистратор (или оболочка, которую вы пишете вокруг выбранного вами пакета ведения журнала) принимает средство форматирования и аргументы для средства форматирования, построение сообщения может быть отложено до тех пор, пока не будет уверенности, что оно будет использовано, при одновременном устранении инструкций guard и их цикломатической сложности.

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() будут происходить вызовы с выделением буфера для них если только в них нет необходимости!Это эффективно устраняет снижение производительности, которое привело к появлению инструкций guard.Одним небольшим наказанием в Java была бы автоматическая блокировка любых аргументов примитивного типа, которые вы передаете регистратору.

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

Дальнейшие усовершенствования

Также обратите внимание, что в Java a MessageFormat объект может быть использован вместо "формата" String, что дает вам дополнительные возможности, такие как выбор формата для более аккуратной обработки кардинальных чисел.Другой альтернативой было бы реализовать вашу собственную возможность форматирования, которая вызывает некоторый интерфейс, который вы определяете для "оценки", а не базовый toString() способ.

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

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

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

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

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

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

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

и вот оно, как объяснили @John Millikin и @erickson.

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

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

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

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

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

Еще раз благодарю вас за все предложения.Мы рассмотрим их для некоторых частей нашей системы, когда простое ведение журнала все еще будет работать.
Но другой смысл этого вопроса состоял в том, чтобы проиллюстрировать вам конкретную проблему в гораздо более широком и сложном контексте.
Надеюсь, вам понравилось.Я мог бы задать вопрос по 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 для условного ведения журнала.

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

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

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

альтернативный текст http://www.scala-lang.org/sites/default/files/newsflash_logo.png

Скала имеет аннотацию @устраняемый() это позволяет вам удалять методы с флагом компилятора.

С помощью scala REPL:

C:>scala

Добро пожаловать в Scala версии 2.8.0.final (64-разрядная серверная виртуальная машина Java HotSpot (TM), Java 1.6.0_16).Введите выражения, чтобы их можно было вычислить.Введите:справка для получения дополнительной информации.

scala> импортировать scala.annotation.elidable импортировать scala.annotation.elidable

scala> импортировать scala.annotation.elidable._ импортировать scala.annotation.elidable._

scala> @устраняемый (FINE) лог-ошибка определения (arg:String) = println(arg)

логДебуг:(аргумент:Строка)Единица измерения

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

скала>

С элиде-белосет

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

Добро пожаловать в Scala версии 2.8.0.final (64-разрядная серверная виртуальная машина Java HotSpot (TM), Java 1.6.0_16).Введите выражения, чтобы их можно было вычислить.Введите:справка для получения дополнительной информации.

scala> импортировать scala.annotation.elidable импортировать scala.annotation.elidable

scala> импортировать scala.annotation.elidable._ импортировать scala.annotation.elidable._

scala> @устраняемый (FINE) лог-ошибка определения (arg:String) = println(arg)

логДебуг:(аргумент:Строка)Единица измерения

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

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

скала>

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

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

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

Logger logger = ...
logger.log(Level.DEBUG,"The foo is {0} and the bar is {1}",new Object[]{foo, bar});

и затем создайте java.util.logging.Форматировщик, который использует MessageFormat для сглаживания foo и bar в строку для вывода.Он будет вызван только в том случае, если регистратор и обработчик будут регистрироваться на этом уровне.

Для дополнительного удовольствия у вас мог бы быть какой-нибудь язык выражений, позволяющий получить точный контроль над тем, как форматировать зарегистрированные объекты (toString не всегда может быть полезен).

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

LOGGER(LEVEL_INFO) << "A String";

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

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

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

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

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;
       }
    }
);
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top