Pergunta

Depois de ler " Qual é o seu / a bom limite para a complexidade ciclomática? ", percebo que muitos dos meus colegas foram bastante irritado com esta nova QA política em nosso projeto :. não mais 10 cyclomatic complexidade por função

Significado: não mais do que 10 'se', 'else', 'try', 'pegar' e outros códigos de fluxo de trabalho ramificação declaração. Certo. Como expliquei em ' Você testar método privado? ', tal política tem muitos bons efeitos colaterais.

Mas: No início dos nossos (200 pessoas - 7 anos) projeto, fomos alegremente login (e não, não podemos facilmente delegar essa a algum tipo de ' abordagem orientada a aspectos de programação ' para logs).

myLogger.info("A String");
myLogger.fine("A more complicated String");
...

E quando as primeiras versões do nosso sistema entrou em operação, tivemos enorme problema de memória não por causa do logging (que estava em um ponto desligado), mas por causa das parâmetros de log (as cordas ), que são sempre calculados, em seguida, passou para o 'info ()' ou 'bem ()' funções, apenas para descobrir que o nível de registro foi 'OFF', e que nenhum registro foram ocorrendo!

Assim QA voltou e pediu que nossos programadores para fazer o registo condicional. Sempre.

if(myLogger.isLoggable(Level.INFO) { myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE) { myLogger.fine("A more complicated String");
...

Mas agora, com que 10 nível de complexidade ciclomática 'pode-não-ser-mudou' limite por função, eles argumentam que os diversos registros que eles colocam na sua função é sentida como um fardo, porque cada "se (isLoggable () )" é contado como uma complexidade ciclomática!

Assim, se uma função tem 8 'se', 'else' e assim por diante, em um firmemente acoplado algoritmo não-facilmente-compartilhável, e 3 acções de log críticos ... eles violar o limite embora os logs condicional pode não ser realmente parte da referida complexidade dessa função ...

Como você resolver esta situação?
Eu vi um par de interessante codificação evolução (devido a que 'conflito') no meu projeto, mas eu só quero pegar seus pensamentos em primeiro lugar.


Obrigado por todas as respostas.
Devo insistir que o problema não é 'formatação' relacionados, mas "avaliação argumento relacionado (avaliação que pode ser muito caro, você só precisa antes de chamar um método que não fará nada)
Assim, quando um escrevi acima "A String", eu realmente quis dizer AFunction (), com AFunction () retornar um String, e sendo uma chamada para um método complicado coleta e computação todo o tipo de dados de registro a ser exibido pelo logger ... ou não (daí o problema, e o obrigação para usar o log condicional, portanto, a questão real de aumento artificial da 'complexidade ciclomática' ...)

Agora eu obter o ' variádica função' ponto avançado por alguns de vocês (obrigado John ).
Nota: um teste rápido em mostras java6 que o meu varargs função faz avaliar os seus argumentos antes de ser chamado, por isso não pode ser aplicado para chamada de função, mas para 'Log retriever objeto' (ou 'função wrapper'), em que o toString () só vai ser chamado, se necessário. Entendi.

Eu já postei a minha experiência sobre este tema.
Vou deixá-lo lá até a próxima terça para a votação, então eu vou selecionar uma das suas respostas.
Mais uma vez, obrigado por todas as sugestões:)

Foi útil?

Solução

Em Python você passar os valores formatados como parâmetros para a função de registro. formatação corda só é aplicada se o registo está activado. Há ainda a sobrecarga de uma chamada de função, mas isso é minúsculo em comparação com a formatação.

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

Você pode fazer algo assim para qualquer linguagem com argumentos variádicos (C / C ++, C # / Java, etc).


Esta não é realmente destinado para quando os argumentos são difíceis de recuperar, mas para quando formatá-los para cadeias é caro. Por exemplo, se o seu código já tem uma lista de números na mesma, você pode querer log que lista para depuração. Execução mylist.toString() vai demorar um pouco para nenhum benefício, como o resultado vai ser jogado fora. Então você passar mylist como um parâmetro para a função de registro, e deixá-lo lidar com corda formatação. Dessa forma, a formatação só será realizada se necessário.


Uma vez que a pergunta do OP menciona especificamente Java, aqui está como o acima podem ser utilizados:

Devo insistir que o problema não é 'formatação' relacionados, mas "avaliação argumento relacionado (avaliação que pode ser muito caro, você só precisa antes de chamar um método que não fará nada)

O truque é ter objetos que não irá executar cálculos caros até que seja absolutamente necessário. Isso é fácil em linguagens como Smalltalk ou Python que lambdas de apoio e fechamentos, mas ainda é factível em Java com um pouco de imaginação.

Digamos que você tenha uma função get_everything(). Ele irá recuperar todos os objetos do banco de dados em uma lista. Você não quer chamar a isto se o resultado será descartado, obviamente. Então, em vez de usar uma chamada para essa função diretamente, você define uma classe interna chamada 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());
    }
}

Neste código, a chamada para getEverything() é enrolado de modo que não será realmente executado até que seja necessário. A função de registro irá executar toString() nos parâmetros somente se depuração está habilitado. Dessa forma, seu código vai sofrer somente a sobrecarga de uma chamada de função em vez da chamada getEverything() completo.

Outras dicas

Com estruturas de registro atual, a questão é discutível

estruturas atuais de registro como slf4j ou log4j 2 não exigem declarações de guarda na maioria dos casos. Eles usam uma declaração log parametrizado de modo que um evento pode ser registrada de forma incondicional, mas a mensagem formatação só ocorre se o evento está habilitado. construção da mensagem é executada conforme a necessidade do logger, ao invés de preventivamente pela aplicação.

Se você tem que usar uma biblioteca de registro antigo, você pode ler sobre para obter mais fundo e uma maneira de modernizar a antiga biblioteca com mensagens parametrizadas.

são declarações de guarda realmente aumentar a complexidade?

Considere a exclusão de guardas madeireiras declarações do cálculo complexidade ciclomática.

Pode-se argumentar que, devido à sua forma previsível, cheques madeireiras condicionais realmente não contribuem para a complexidade do código.

métricas inflexíveis pode fazer uma outra boa programador turn ruim. Tenha cuidado!

Assumindo que suas ferramentas para calcular a complexidade não pode ser adaptado para esse grau, a seguinte abordagem pode oferecer uma solução alternativa.

A necessidade de registro de condicional

Eu assumo que suas declarações de guarda foram introduzidas porque você teve um código como este:

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

Em Java, cada uma das afirmações de log cria um novo StringBuilder, e invoca o método toString() em cada objeto concatenado para a cadeia. Estes métodos toString(), por sua vez, são susceptíveis de criar instâncias StringBuilder de seus próprios, e chamar os métodos toString() dos seus membros, e assim por diante, através de um gráfico de objeto potencialmente grande. (Antes de Java 5, que era ainda mais caro, uma vez StringBuffer foi usado, e todas as suas operações são sincronizados.)

Isto pode ser relativamente caro, especialmente se a declaração de registro está em algum caminho de código fortemente executado. E, escrito como acima, que a formatação caro mensagem ocorre mesmo se o logger é obrigado a descartar o resultado porque o nível de log é muito alto.

Isso leva à introdução de declarações de guarda da forma:

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

Com este guarda, é realizada a avaliação de argumentos d e w ea concatenação somente quando necessário.

Uma solução para simples, logging eficiente

No entanto, se o logger (ou um invólucro que você escreve em torno de seu pacote de registro escolhido) leva um formatador e argumentos para o formatador, a construção de mensagens pode ser adiada até que esteja certo de que ele será usado, enquanto eliminando a guarda declarações e sua complexidade ciclomática.

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

}

Agora, nenhum dos cascata chamadas toString() com suas alocações de buffer ocorrerá a menos que eles são necessários! Isso elimina efetivamente o impacto no desempenho que levou às declarações de guarda. Um pequeno grande penalidade, em Java, seria auto-boxing de quaisquer argumentos de tipo primitivo que você passa para o logger.

O código fazendo o registro é sem dúvida ainda mais limpa do que nunca, uma vez desarrumado concatenação está desaparecido. Pode ser ainda mais limpa se as cadeias de formato são externalizados (usando um ResourceBundle), que também poderia ajudar na manutenção ou localização do software.

Outras melhorias

Observe também que, em Java, um objeto MessageFormat poderia ser usado no lugar de um String "format", que lhe dá capacidades adicionais, tais como um formato de escolha para lidar com números cardinais mais ordenadamente. Outra alternativa seria a de implementar a sua própria capacidade de formatação que invoca alguma interface que você definir para "avaliação", ao invés do método básico toString().

Em linguagens que suportam expressões lambda ou blocos de código como parâmetros, uma solução para este seria dar exatamente isso ao método de registro. Que se poderia avaliar a configuração e apenas se necessário realmente chamar / executar o bloco lambda / código fornecido. Não tente isso ainda, apesar de tudo.

Teoricamente isso é possível. Eu não gostaria de usá-lo na produção devido a problemas de desempenho i esperar com que o uso pesado de lamdas blocos / código para registro.

Mas, como sempre:. Em caso de dúvida, testá-lo e medir o impacto sobre a carga da CPU e memória

Obrigado por todas as suas respostas! Vocês Rock:)

Agora, a minha opinião não é tão simples e direta como o seu:

Sim, para um projeto (como em 'um programa implementado e em execução em seu próprio em uma única plataforma de produção'), acho que você pode ir toda técnica sobre mim:

  • dedicado objetos 'Log retriever', que pode ser passe para um Logger invólucro única chamando toString () é necessário
  • usado em conjunto com um registro de função de aridade variável (ou um objeto simples [arranjo]! )

e aí está, como explicado por @ John Millikin e @erickson.

No entanto, esta questão nos obrigou a pensar um pouco 'Por que exatamente estávamos registrando em primeiro lugar?'
Nosso projeto é, na verdade, 30 projetos diferentes (de 5 a 10 pessoas cada) implantados em várias plataformas de produção, com as necessidades de comunicação assíncrona e arquitetura de barramento central.
O registro simples descrito na pergunta foi bom para cada projeto no começo (5 anos atrás), mas desde então, tem que acelerar. Digite o KPI .

Em vez de pedir a um logger para registrar qualquer coisa, pedimos para um objeto criado automaticamente (chamado de KPI) para registrar um evento. É uma simples chamada (myKPI.I_am_signaling_myself_to_you ()), e não precisa ser condicional (o que resolve o 'aumento artificial da complexidade ciclomática' questão).

Isso KPI objeto sabe que chama-lo e uma vez que ele é executado desde o início da aplicação, ele é capaz de recuperar grandes quantidades de dados que anteriormente eram de computação no local quando estávamos logging.
Além disso, que KPI objeto pode ser monitorado de forma independente e computação / publicar sob demanda suas informações em um único e separado ônibus publicação.
Dessa forma, cada cliente pode pedir a informação que ele realmente quer (como, 'tem o meu processo iniciado, e se sim, desde quando?'), Em vez de olhar para o arquivo de log correto e grepping para uma String enigmática ...

Na verdade, a pergunta "Por que exatamente estávamos registrando em primeiro lugar? nos fez perceber que não estávamos registrando apenas para o programador e seus testes de unidade ou integração, mas para uma comunidade muito mais ampla, incluindo alguns dos próprios clientes finais. Nosso mecanismo de 'relatórios' teve que ser centralizada, assíncrona, 24/7.

O específica desse mecanismo de KPI é caminho para fora do âmbito desta questão. Basta dizer que a sua calibração adequada é, de longe, as mãos para baixo, o único e mais complicado problema não funcional que estamos enfrentando. Ele ainda traz o sistema em seu joelho ao longo do tempo! Devidamente calibrado no entanto, é um salva-vidas.

Mais uma vez, obrigado por todas as sugestões. Vamos considerá-los para algumas partes do nosso sistema quando o log simples ainda está em vigor.
Mas outro ponto desta questão era para ilustrar a você um problema específico em um contexto muito maior e mais complicado.
Espero que você gostou. Eu poderia fazer uma pergunta sobre KPI (que, acredite ou não, não é em qualquer pergunta sobre SOF até agora!) Mais tarde na próxima semana.

Vou deixar esta resposta for submetida a votação até a próxima terça-feira, então eu vou selecionar uma resposta (não este, obviamente;))

Talvez isso é muito simples, mas que sobre usando o "método de extrair" refatoração em torno da cláusula de guarda? Seu código exemplo disso:

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
}

Torna-se o seguinte:

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 ... */ }

Em C ou C ++ que eu usaria o pré-processador em vez do if para o registro condicional.

Passe o nível de registro para o logger e deixá-lo decidir se quer ou não escrever a declaração de log:

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

UPDATE: Ah, eu vejo que você deseja criar condicionalmente a cadeia de log sem uma declaração condicional. Presumivelmente, em tempo de execução em vez de tempo de compilação.

Eu vou apenas dizer que a forma como nós resolvemos isso é colocar o código de formatação na classe logger de modo que a formatação só ocorre se o nível passa. Muito semelhante a um sprintf embutido. Por exemplo:

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

Isso deve atender aos seus critérios.

alt texto http://www.scala-lang.org /sites/default/files/newsflash_logo.png

Scala tem uma annontation @ elidable () que permite remover métodos com uma bandeira do compilador.

Com o REPL scala:

C:> scala

Bem-vindo ao Scala versão 2.8.0.final (Java HotSpot (TM) 64-Bit VM Server, Java 1. 6.0_16). Digite expressões para tê-los avaliado. Tipo:. Ajuda para mais informações

scala> scala.annotation.elidable importação importação scala.annotation.elidable

scala> scala.annotation.elidable._ importação importação scala.annotation.elidable ._

scala> @elidable (FINA) def logDebug (arg: String) = println (arg)

logDebug: (arg: String) Unidade

scala> logDebug ( "testing")

scala>

Com elide-beloset

C:> scala -Xelide-abaixo de 0

Bem-vindo ao Scala versão 2.8.0.final (Java HotSpot (TM) 64-Bit VM Server, Java 1. 6.0_16). Digite expressões para tê-los avaliado. Tipo:. Ajuda para mais informações

scala> scala.annotation.elidable importação importação scala.annotation.elidable

scala> scala.annotation.elidable._ importação importação scala.annotation.elidable ._

scala> @elidable (FINA) def logDebug (arg: String) = println (arg)

logDebug: (arg: String) Unidade

scala> logDebug ( "testing")

testing

scala>

Veja também Scala assert definição

logging condicional é mau. Acrescenta desordem desnecessária ao seu código.

Você deve sempre enviar os objetos que você tem que o logger:

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

e depois ter uma java.util.logging.Formatter que usos MessageFormat para achatar foo e bar na corda para ser emitido. Ele só vai ser chamado se o logger e manipulador irá registrar a esse nível.

Para mais prazer você poderia ter algum tipo de linguagem de expressão para ser capaz de obter um bom controle sobre como formatar o objetos registrados (toString nem sempre pode ser útil).

Por mais que eu odeio macros em C / C ++, no trabalho que temos # define para o que parte, que se falsas ignora (não avalia) as seguintes expressões, mas se for verdade retorna um fluxo no qual o material pode ser canalizada usando o '<<' operador. Como esta:

LOGGER(LEVEL_INFO) << "A String";

Eu suponho que este eliminaria a 'complexidade' extra que sua ferramenta vê, e também elimina qualquer cálculo da cadeia, ou quaisquer expressões para ser registrado se o nível não foi alcançado.

Aqui é uma solução elegante usando expressão ternária

logger.info (logger.isInfoEnabled () "Declaração de Log vai aqui ...?": Null);

Considere um registro de util função ...

void debugUtil(String s, Object… args) {
   if (LOG.isDebugEnabled())
       LOG.debug(s, args);
   }
);

Em seguida, fazer a chamada com um "fechamento" round a avaliação caro que você quer evitar.

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top