Registro condicional com complexidade ciclomática mínima

Depois de ler ” Qual é o seu / um bom limite para a complexidade ciclomática? “, Percebo que muitos dos meus colegas estavam bastante incomodados com essa nova política de QA em nosso projeto: não mais 10 complexidade ciclomática por function.

Significado: não mais que 10 ‘if’, ‘else’, ‘try’, ‘catch’ e outra instrução de ramificação de stream de trabalho de código. Certo. Como expliquei em ‘ Você testa o método privado? ‘, tal política tem muitos bons efeitos colaterais.

Mas: No início de nosso projeto (200 pessoas – 7 anos de duração), estávamos logando alegremente (e não, não podemos facilmente delegar isso a algum tipo de abordagem de ‘ programação orientada a aspectos ‘ para logs).

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

E quando as primeiras versões do nosso sistema foram ativadas, passamos por um grande problema de memory não por causa do logging (que estava em um ponto desativado), mas por causa dos parâmetros de log (strings), que são sempre calculados. as funções ‘info ()’ ou ‘fine ()’, apenas para descobrir que o nível de registro estava ‘OFF’, e que nenhum registro estava ocorrendo!

Então, o controle de qualidade voltou e instou nossos programadores a fazer o registro 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 esse nível de complexidade ciclomática ‘não-possível-ser-movido’ por limite de function, eles argumentam que os vários logs que eles colocam em sua function são sentidos como um fardo, porque cada “if (isLoggable ())” é contada como +1 complexidade ciclomática!

Então, se uma function tiver 8 ‘se’, ‘else’ e assim por diante, em um algoritmo fortemente não acoplado e de três partes críticas … eles violam o limite mesmo que os logs condicionais possam não ser realmente parte da dita complexidade dessa function …

Como você lidaria com essa situação?
Eu vi um par de interessante evolução de codificação (devido a esse ‘conflito’) no meu projeto, mas eu só quero ter seus pensamentos em primeiro lugar.


Obrigado por todas as respostas.
Devo insistir que o problema não está relacionado à “formatação”, mas à “avaliação de argumentos” (avaliação que pode ser muito custosa de fazer, pouco antes de chamar um método que não fará nada).
Então quando escrevi acima “A String”, eu realmente quis dizer aFunction (), com aFunction () retornando um String, e sendo uma chamada para um método complicado coletando e computando todo tipo de dados de log a serem exibidos pelo logger … ou não (daí a questão, e a obrigação de usar a extração condicional, daí a questão real do aumento artificial da ‘complexidade ciclomática’ …)

Eu agora recebo o ponto ‘function variadica ‘ avançado por alguns de vocês (obrigado John).
Nota: um teste rápido em java6 mostra que minha function varargs avalia seus argumentos antes de ser chamada, portanto não pode ser aplicada para chamada de function, mas para ‘Log retriever object’ (ou ‘function wrapper’), no qual toString ( ) só será chamado se necessário. Consegui.

Eu postei agora minha experiência neste tópico.
Vou deixar lá até a próxima terça-feira para votar, então vou selecionar uma de suas respostas.
Mais uma vez, obrigado por todas as sugestões 🙂

No Python, você passa os valores formatados como parâmetros para a function de log. A formatação de string só é aplicada se o log estiver ativado. Ainda há a sobrecarga de uma chamada de function, 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 variadic (C / C ++, C # / Java, etc).


Isso não é realmente destinado a quando os argumentos são difíceis de recuperar, mas para quando formatá-los em cadeias de caracteres é caro. Por exemplo, se o seu código já tiver uma lista de números, você poderá registrar essa lista para debugging. Executar mylist.toString() levará algum tempo sem nenhum benefício, pois o resultado será descartado. Portanto, você passa mylist como um parâmetro para a function de registro e permite que ele manipule a formatação de string. Dessa forma, a formatação só será executada se necessário.


Como a pergunta do OP menciona especificamente o Java, veja como o acima pode ser usado:

Devo insistir que o problema não está relacionado à “formatação”, mas à “avaliação de argumentos” (avaliação que pode ser muito custosa de fazer, pouco antes de chamar um método que não fará nada).

O truque é ter objects que não executem cálculos caros até que sejam absolutamente necessários. Isso é fácil em linguagens como Smalltalk ou Python que suportam lambdas e closures, mas ainda é possível em Java com um pouco de imaginação.

Digamos que você tenha uma function get_everything() . Ele irá recuperar todos os objects do seu database em uma lista. Você não quer chamar isto se o resultado for descartado, obviamente. Então, ao invés de usar uma chamada para essa function diretamente, você define uma class 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()); } } 

Nesse código, a chamada para getEverything() é getEverything() para que não seja realmente executada até que seja necessária. A function de registro executará toString() em seus parâmetros somente se a debugging estiver ativada. Dessa forma, seu código sofrerá somente a sobrecarga de uma chamada de function em vez da chamada getEverything() completa.

Com os atuais frameworks de logging, a questão é discutível

Estruturas de registro atuais como slf4j ou log4j 2 não exigem instruções de guarda na maioria dos casos. Eles usam uma instrução de log com parâmetros para que um evento possa ser registrado incondicionalmente, mas a formatação da mensagem ocorre apenas se o evento estiver habilitado. A construção da mensagem é executada conforme necessário pelo logger, em vez de preventivamente pelo aplicativo.

Se você tiver que usar uma biblioteca de registro antiga, poderá ler para obter mais informações e uma maneira de atualizar a biblioteca antiga com mensagens parametrizadas.

As declarações de guarda estão realmente adicionando complexidade?

Considere excluir as instruções de proteção de registro do cálculo de complexidade ciclomática.

Pode-se argumentar que, devido à sua forma previsível, as verificações de logs condicionais realmente não contribuem para a complexidade do código.

Métricas inflexíveis podem fazer com que um bom programador se torne ruim. Seja cuidadoso!

Supondo que suas ferramentas para calcular a complexidade não possam ser adaptadas a esse nível, a abordagem a seguir pode oferecer uma solução alternativa.

A necessidade de registro condicional

Eu suponho que suas declarações de guarda foram introduzidas porque você tinha 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 instruções de log cria um novo StringBuilder e invoca o toString() em cada object concatenado à string. Esses methods toString() , por sua vez, provavelmente criarão instâncias próprias do StringBuilder e invocarão os methods toString() de seus membros, e assim por diante, em um gráfico de object potencialmente grande. (Antes do Java 5, era ainda mais caro, já que o StringBuffer era usado e todas as suas operações eram sincronizadas.)

Isso pode ser relativamente caro, especialmente se a instrução de log estiver em algum caminho de código fortemente executado. E, como descrito acima, essa formatação de mensagem cara ocorre mesmo se o agente de log está obrigado a descartar o resultado porque o nível de log é muito alto.

Isso leva à introdução de instruções de guarda do formulário:

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

Com essa proteção, a avaliação dos argumentos de w e a concatenação da sequência são executadas somente quando necessário.

Uma solução para registro simples e eficiente

No entanto, se o criador de logs (ou um wrapper que você escreve em torno do seu pacote de log escolhido) tiver um formatador e argumentos para o formatador, a construção da mensagem poderá ser adiada até ter certeza de que será usada, eliminando as instruções de guarda 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, nenhuma das chamadas toString() cascata com suas alocações de buffer ocorrerá a menos que sejam necessárias! Isso efetivamente elimina o impacto no desempenho que levou às declarações de guarda. Uma pequena penalidade, em Java, seria o auto-boxing de qualquer tipo de argumento primitivo que você passa para o logger.

O código que faz o logging é indiscutivelmente ainda mais limpo do que nunca, já que a concatenação de cadeias de caracteres desordenada desapareceu. Pode ser ainda mais claro se as strings de formato forem externalizadas (usando um ResourceBundle ), o que também poderia ajudar na manutenção ou localização do software.

Melhorias adicionais

Observe também que, em Java, um object MessageFormat poderia ser usado no lugar de uma String “format”, que fornece resources adicionais, como um formato de opção para manipular números cardinais com mais precisão. Outra alternativa seria implementar sua própria capacidade de formatação que invoca alguma interface que você define para “avaliação”, em vez do toString() básico toString() .

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

Teoricamente isso é possível. Eu não gostaria de usá-lo na produção devido a problemas de desempenho que eu esperava com o uso pesado de lamdas / code blocks para logging.

Mas, como sempre: em caso de dúvida, teste-o e meça o impacto sobre a carga e a memory da CPU.

Em C ou C ++ eu usaria o pré-processador em vez das instruções if para o log condicional.

Obrigado por todas as suas respostas! Vocês são foda 🙂

Agora meu feedback não é tão direto quanto o seu:

Sim, para um projeto (como em ‘um programa implantado e funcionando sozinho em uma única plataforma de produção’), suponho que você possa me tornar técnico em todos os aspectos:

  • Objetos ‘Log Retriever’ dedicados, que podem ser passados ​​para um wrapper do Logger, somente chamando toString () é necessário
  • usado em conjunto com uma function variadic de registro (ou uma matriz Object [] simples!)

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

No entanto, esta questão nos obrigou a pensar um pouco sobre “Por que exatamente nós estávamos logando em primeiro lugar?”
Nosso projeto é na verdade 30 projetos diferentes (5 a 10 pessoas cada) implantados em várias plataformas de produção, com necessidades de comunicação assíncrona e arquitetura de barramento central.
O registo simples descrito na pergunta foi bom para cada projeto no início (há 5 anos), mas desde então, temos que intensificar. Digite o KPI .

Em vez de pedir a um registrador para registrar qualquer coisa, solicitamos a um object criado automaticamente (chamado KPI) para registrar um evento. É uma chamada simples (myKPI.I_am_signaling_myself_to_you ()) e não precisa ser condicional (o que resolve o problema de ‘aumento artificial da complexidade ciclomática’).

Esse object de KPI sabe quem o chama e, desde que ele é executado desde o início do aplicativo, ele pode recuperar muitos dados que antes estávamos computando no momento em que estávamos registrando.
Além disso, esse object de KPI pode ser monitorado independentemente e computar / publicar sob demanda suas informações em um barramento de publicação único e separado.
Dessa forma, cada cliente pode solicitar as informações que ele realmente deseja (como, ‘meu processo foi iniciado e, se sim, desde quando?’), Em vez de procurar o arquivo de log correto e o grepping de uma String enigmática …

De fato, a pergunta “Por que exatamente estávamos logando em primeiro lugar?” nos fez perceber que não estávamos registrando apenas para o programador e sua unidade ou testes de integração, mas para uma comunidade muito mais ampla, incluindo alguns dos próprios clientes finais. Nosso mecanismo de ‘relatórios’ tinha que ser centralizado, asynchronous, 24/7.

O específico desse mecanismo de KPI está bem fora do escopo desta questão. Basta dizer que a calibração correta é, de longe, a questão não funcional mais complicada que estamos enfrentando. Ainda traz o sistema de joelhos de tempos em tempos! Corretamente 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 registro simples ainda estiver em vigor.
Mas o outro ponto desta questão foi ilustrar para você um problema específico em um contexto muito maior e mais complicado.
Espero que você tenha gostado. Eu poderia fazer uma pergunta sobre o KPI (que, acredite ou não, não está em dúvida em SOF até agora!) Mais tarde na próxima semana.

Deixarei esta resposta para votação até a próxima terça, então selecionarei uma resposta (não esta obviamente;))

Talvez isso seja muito simples, mas o que dizer de usar a refatoração “método de extração” ao redor da cláusula de guarda? Seu código de 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 isso:

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

Passe o nível de log para o logger e deixe que ele decida se deve ou não gravar a instrução de log:

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

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

Vou apenas dizer que a maneira como resolvemos isso é colocar o código de formatação na class do criador de logs para que a formatação ocorra somente se o nível passar. Muito semelhante a um sprintf integrado. Por exemplo:

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

Isso deve atender aos seus critérios.

alt text http://sofpt.miximages.com/language-agnostic/newsflash_logo.png

Scala tem uma annontation @elidable () que permite remover methods com um sinalizador de compilador.

Com o scala REPL:

C:> scala

Bem-vindo ao Scala versão 2.8.0.final (Java HotSpot (TM) VM de servidor de 64 bits, Java 1. 6.0_16). Digite expressões para que elas sejam avaliadas. Digite: help para mais informações.

scala> import scala.annotation.elidable import scala.annotation.elidable

scala> importar scala.annotation.elidable._ import scala.annotation.elidable._

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

logDebug: (arg: String) Unit

scala> logDebug (“testing”)

scala>

Com elide-beloset

C:> scala -Xelide-below 0

Bem-vindo ao Scala versão 2.8.0.final (Java HotSpot (TM) VM de servidor de 64 bits, Java 1. 6.0_16). Digite expressões para que elas sejam avaliadas. Digite: help para mais informações.

scala> import scala.annotation.elidable import scala.annotation.elidable

scala> importar scala.annotation.elidable._ import scala.annotation.elidable._

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

logDebug: (arg: String) Unit

scala> logDebug (“testing”)

testando

scala>

Veja também Scala asser definição

O log condicional é maligno. Adiciona desordem desnecessária ao seu código.

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

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

e, em seguida, ter um java.util.logging.Formatter que usa MessageFormat para achatar foo e barrar na cadeia a ser gerada. Ele só será chamado se o registrador e o manipulador fizerem login nesse nível.

Para maior prazer, você pode ter algum tipo de linguagem de expressão para poder obter um bom controle sobre como formatar os objects registrados (toString pode nem sempre ser útil).

Por mais que eu odeie macros em C / C ++, no trabalho nós temos #defines para a parte if, que se false ignora (não avalia) as seguintes expressões, mas se true retorna um stream no qual as coisas podem ser canalizadas usando o ‘ << 'operador. Como isso:

 LOGGER(LEVEL_INFO) << "A String"; 

Eu suponho que isso eliminaria a 'complexidade' extra que sua ferramenta vê, e também elimina qualquer cálculo da string, ou qualquer expressão a ser registrada se o nível não for atingido.

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

logger.info (logger.isInfoEnabled ()? “A instrução de log vai aqui …”: null);

Considere uma function util de log …

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

Em seguida, faça a chamada com um “encerramento” em torno da avaliação dispendiosa que você deseja evitar.

 debugUtil(“We got a %s”, new Object() { @Override String toString() { // only evaluated if the debug statement is executed return expensiveCallToGetSomeValue().toString; } } ); 
Intereting Posts