Domanda

Dopo aver letto "Qual è il tuo/a buon limite per la complessità ciclomatica?"Mi rendo conto che molti dei miei colleghi sono stati molto infastidito con questo nuovo QA la politica sul nostro progetto:non più di 10 complessità ciclomatica per funzione.

Significato:non più di 10 'se', 'altro', 'prova', 'catch' e altro codice flusso di lavoro istruzione condizionale.Destra.Come ho spiegato in un 'Vuoi testare il metodo privato?', tale politica ha molti buoni effetti collaterali.

Ma:All'inizio della nostra (200 persone - 7 anni) progetto, siamo stati felici di registrazione (e no, non siamo in grado di facilmente delegato che per un qualche tipo di 'Aspect-oriented programming'approccio per i registri).

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

E quando la prima versione del nostro Sistema è andato in diretta, abbiamo sperimentato enorme problema di memoria, non a causa della registrazione (che era ad un punto spento), ma a causa del parametri di registro (le corde), che vengono sempre calcolati, per poi passare a 'info()' o 'fine()' funzioni, solo per scoprire che il livello di registrazione è 'OFF', e che nessuna registrazione stavano prendendo posto!

Così QA è tornato e ha esortato i nostri programmatori per fare condizionale di registrazione.Sempre.

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

Ma ora, con che 'può-non-essere-si e' mosso di 10 complessità ciclomatica per la funzione di limite, essi sostengono che i vari registri hanno messo nella loro funzione è sentito come un peso, perché ogni "se(isLoggable())" è considerato +1 complessità ciclomatica!

Quindi, se una funzione ha 8 'se', 'altro' e così via, in uno strettamente non-facilmente condivisibile algoritmo, e 3 critici registro di azioni...di violare il limite, anche se il condizionale registri non possono essere davvero parte di detto complessità della funzione...

Come affrontare questa situazione ?
Ho visto un paio di interessanti codifica evoluzione (a causa di questo 'conflitto') nel mio progetto, ma voglio solo per ottenere i vostri pensieri prima.


Grazie per tutte le risposte.
Devo insistere sul fatto che il problema non e 'formattazione', ma 'argomento di valutazione relativa alla valutazione (che può essere molto costoso da fare, prima di chiamare un metodo che non fa nulla)
Così, quando un scritto sopra "Una Stringa", io in realtà significava aFunction(), con aFunction() che ritorna una Stringa, e di essere una chiamata a un complicato metodo di raccolta e di elaborazione per tutti i tipi di dati di log per essere visualizzato dallo strumento...o non (quindi il problema, e la obbligo per utilizzare tale registrazione, quindi il problema effettivo di aumento artificiale della 'complessità ciclomatica'...)

Io ad ora e 'variadic la funzione di punto di avanzate da alcuni di voi (grazie Giovanni).
Nota:un test rapido in java6 dimostra che il mio varargs funzione non valutare i suoi argomenti, prima di essere chiamato, in modo che non può essere applicato per la chiamata di funzione, ma per il 'Registro retriever oggetto' (o 'funzione wrapper'), su cui il metodo toString() per essere chiamato solo se necessario.Capito.

Ora ho postato la mia esperienza su questo argomento.
Lascio lì fino a martedì prossimo per il voto, allora io seleziona una delle tue risposte.
Di nuovo, grazie per i suggerimenti :)

È stato utile?

Soluzione

In Python si passano i valori formattati come parametri alla funzione di registrazione. La formattazione delle stringhe viene applicata solo se la registrazione è abilitata. C'è ancora il sovraccarico di una chiamata di funzione, ma è minuscolo rispetto alla formattazione.

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

Puoi fare qualcosa del genere per qualsiasi linguaggio con argomenti variadici (C / C ++, C # / Java, ecc.)


Questo non è realmente destinato a quando gli argomenti sono difficili da recuperare, ma quando formattarli in stringhe è costoso. Ad esempio, se il tuo codice contiene già un elenco di numeri, potresti voler registrarlo per il debug. L'esecuzione di mylist.toString() richiederà un po 'di tempo, poiché il risultato verrà eliminato. Quindi si passa mylist come parametro alla funzione di registrazione e si lascia che gestisca la formattazione della stringa. In questo modo, la formattazione verrà eseguita solo se necessario.


Poiché la domanda del PO menziona specificamente Java, ecco come si può usare quanto sopra:

  

Devo insistere sul fatto che il problema non è legato alla "formattazione", ma alla "valutazione dell'argomento" (valutazione che può essere molto costosa da fare, prima di chiamare un metodo che non farà nulla)

Il trucco è avere oggetti che non eseguiranno calcoli costosi fino a quando non saranno assolutamente necessari. Questo è facile in linguaggi come Smalltalk o Python che supportano lambda e chiusure, ma è ancora fattibile in Java con un po 'di immaginazione.

Supponi di avere una funzione get_everything(). Recupererà ogni oggetto dal database in un elenco. Non vuoi chiamarlo se il risultato verrà scartato, ovviamente. Quindi, invece di utilizzare direttamente una chiamata a quella funzione, si definisce una classe interna chiamata 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());
    }
}

In questo codice, la chiamata a getEverything() è racchiusa in modo che non venga effettivamente eseguita fino a quando non è necessaria. La funzione di registrazione eseguirà toString() sui suoi parametri solo se il debug è abilitato. In questo modo, il tuo codice subirà solo l'overhead di una chiamata di funzione anziché la <=> chiamata completa.

Altri suggerimenti

Con gli attuali framework di registrazione, la domanda è controversa

I framework di registrazione attuali come slf4j o log4j 2 non richiedono dichiarazioni di guardia nella maggior parte dei casi. Usano un'istruzione di registro con parametri in modo che un evento possa essere registrato incondizionatamente, ma la formattazione dei messaggi si verifica solo se l'evento è abilitato. La costruzione del messaggio viene eseguita come richiesto dal logger, piuttosto che preventivamente dall'applicazione.

Se devi usare una libreria di log antica, puoi continuare a leggere per ottenere più background e un modo per aggiornare la vecchia libreria con messaggi parametrizzati.

Le dichiarazioni di guardia aggiungono davvero complessità?

Considera di escludere le dichiarazioni delle guardie forestali dal calcolo della complessità ciclomatica.

Si potrebbe sostenere che, a causa della loro forma prevedibile, i controlli di registrazione condizionale in realtà non contribuiscono alla complessità del codice.

Metriche non flessibili possono rendere un programmatore altrimenti buono cattivo. Stai attento!

Supponendo che i tuoi strumenti per il calcolo della complessità non possano essere adattati a quel livello, il seguente approccio potrebbe offrire una soluzione.

La necessità di una registrazione condizionale

Presumo che le tue dichiarazioni di guardia siano state introdotte perché avevi un codice come questo:

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

In Java, ciascuna delle istruzioni del registro crea un nuovo StringBuilder e invoca il metodo toString() su ciascun oggetto concatenato alla stringa. Questi StringBuffer metodi, a loro volta, possono creare d istanze proprie e invocare i w metodi dei loro membri, e così via, attraverso un grafico di oggetti potenzialmente di grandi dimensioni. (Prima di Java 5, era ancora più costoso, poiché veniva utilizzato ResourceBundle e tutte le sue operazioni sono sincronizzate.)

Questo può essere relativamente costoso, specialmente se l'istruzione log si trova in un percorso di codice fortemente eseguito. E, scritto come sopra, quella costosa formattazione dei messaggi si verifica anche se il logger è destinato a scartare il risultato perché il livello di log è troppo alto.

Questo porta all'introduzione di dichiarazioni di guardia del modulo:

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

Con questa protezione, la valutazione degli argomenti MessageFormat e String e la concatenazione di stringhe viene eseguita solo quando necessario.

Una soluzione per la registrazione semplice ed efficiente

Tuttavia, se il logger (o un wrapper che si scrive attorno al pacchetto di registrazione scelto) accetta un formattatore e argomenti per il formattatore, la costruzione del messaggio può essere ritardata fino a quando non si è certi che verrà utilizzata, eliminando la protezione dichiarazioni e la loro complessità ciclomatica.

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

}

Ora, nessuna delle chiamate <=> in cascata con le loro allocazioni del buffer avverrà a meno che non siano necessarie! Questo elimina efficacemente il colpo di prestazione che ha portato alle dichiarazioni di guardia. Una piccola penalità, in Java, sarebbe il boxing automatico di qualsiasi argomento di tipo primitivo che passi al logger.

Il codice che esegue la registrazione è probabilmente ancora più pulito che mai, poiché la concatenazione di stringhe disordinata è sparita. Può essere anche più pulito se le stringhe di formato sono esternalizzate (usando un <=>), il che potrebbe anche aiutare nella manutenzione o localizzazione del software.

Ulteriori miglioramenti

Si noti inoltre che, in Java, un oggetto <=> potrebbe essere utilizzato al posto di un " formato " <=>, che offre funzionalità aggiuntive come un formato di scelta per gestire i numeri cardinali in modo più accurato. Un'altra alternativa sarebbe quella di implementare la propria capacità di formattazione che invoca alcune interfacce definite per & Quot; assessment & Quot ;, piuttosto che il metodo di base <=>.

Nei linguaggi che supportano espressioni lambda o blocchi di codice come parametri, una soluzione sarebbe quella di fornire proprio questo al metodo di registrazione. Quello potrebbe valutare la configurazione e solo se necessario effettivamente chiamare / eseguire il blocco lambda / code fornito. Non l'ho ancora provato, però.

Teoricamente questo è possibile. Non vorrei usarlo in produzione a causa di problemi di prestazioni che mi aspetto con quel pesante uso di lamdas / blocchi di codice per la registrazione.

Ma come sempre: in caso di dubbio, testalo e misura l'impatto sul carico e sulla memoria della cpu.

Grazie per tutte le tue risposte! Ragazzi rock :)

Ora il mio feedback non è diretto come il tuo:

Sì, per un progetto (come in "un programma distribuito e in esecuzione da solo su una singola piattaforma di produzione"), suppongo che tu possa andare tutto tecnico su di me:

  • oggetti 'Log Retriever' dedicati, che possono essere passati a un wrapper Logger solo chiamando toString () è necessario
  • utilizzato in combinazione con una funzione variadic di registrazione (o una semplice matrice Object []! )

e il gioco è fatto, come spiegato da @John Millikin e @erickson.

Tuttavia, questo problema ci ha costretto a riflettere un po 'su "Perché esattamente stavamo registrando in primo luogo?"
Il nostro progetto è attualmente composto da 30 progetti diversi (da 5 a 10 persone ciascuno) distribuiti su varie piattaforme di produzione, con esigenze di comunicazione asincrone e architettura centrale del bus.
La semplice registrazione descritta nella domanda andava bene per ogni progetto all'inizio (5 anni fa), ma da allora, dobbiamo intensificare. Inserisci il KPI .

Invece di chiedere a un logger di registrare qualsiasi cosa, chiediamo a un oggetto creato automaticamente (chiamato KPI) di registrare un evento. È una semplice chiamata (myKPI.I_am_signaling_myself_to_you ()) e non deve essere condizionale (che risolve il problema dell '"aumento artificiale della complessità ciclomatica").

Quell'oggetto KPI sa chi lo chiama e, poiché viene eseguito dall'inizio dell'applicazione, è in grado di recuperare molti dati che in precedenza stavamo calcolando sul posto durante la registrazione.
Inoltre, l'oggetto KPI può essere monitorato in modo indipendente e calcola / pubblica su richiesta le sue informazioni su un bus di pubblicazione singolo e separato.
In questo modo, ogni cliente può chiedere le informazioni che desidera effettivamente (come, "è iniziato il mio processo, e se sì, da quando?"), Invece di cercare il file di registro corretto e cercare una stringa criptica ...

In effetti, la domanda "Perché esattamente stavamo registrando in primo luogo?" ci ha fatto capire che non stavamo registrando solo per il programmatore e la sua unità o i test di integrazione, ma per una comunità molto più ampia, compresi alcuni dei clienti finali stessi. Il nostro meccanismo di "segnalazione" doveva essere centralizzato, asincrono, 24/7.

Lo specifico di quel meccanismo KPI è al di fuori dell'ambito di questa domanda. Basti pensare che la sua corretta calibrazione è di gran lunga, senza dubbio, il singolo problema non funzionale più complicato che stiamo affrontando. Di tanto in tanto porta ancora il sistema in ginocchio! Adeguatamente calibrato, è un salvavita.

Ancora una volta, grazie per tutti i suggerimenti. Li prenderemo in considerazione per alcune parti del nostro sistema quando sarà ancora disponibile la registrazione semplice.
Ma l'altro punto di questa domanda è stato quello di illustrarti un problema specifico in un contesto molto più ampio e complicato.
Spero ti sia piaciuto. Potrei fare una domanda su KPI (che, credendo o no, non è in nessuna domanda su SOF finora!) Più avanti la prossima settimana.

Lascerò questa risposta per la votazione fino a martedì prossimo, quindi selezionerò una risposta (non ovviamente questa;))

Forse è troppo semplice, ma che dire dell'utilizzo del " extract method " refactoring attorno alla clausola di guardia? Il tuo codice di esempio di questo:

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
}

Diventa questo:

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

In C o C++ che mi piacerebbe utilizzare il preprocessore invece di if istruzioni per il condizionale registrazione.

Passa il livello di registro al logger e lascia che decida se scrivere o meno l'istruzione di registro:

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

AGGIORNAMENTO: Ah, vedo che vuoi creare in modo condizionale la stringa di registro senza un'istruzione condizionale. Presumibilmente in fase di esecuzione anziché in fase di compilazione.

Dirò solo che il modo in cui l'abbiamo risolto è inserire il codice di formattazione nella classe logger in modo che la formattazione avvenga solo se il livello passa. Molto simile a uno sprintf incorporato. Ad esempio:

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

Questo dovrebbe soddisfare i tuoi criteri.

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

Scala ha un annuncio @ elidable () che ti consente di rimuovere i metodi con un flag di compilazione.

Con la scala REPL:

  

C: > scala

     

Benvenuti in Scala versione 2.8.0.final (Java HotSpot (TM) VM a 64 bit Server, Java 1.   6.0_16).   Digita le espressioni per farle valutare.   Digitare: aiuto per ulteriori informazioni.

     &

Scala gt; import scala.annotation.elidable   import scala.annotation.elidable

     &

Scala gt; import scala.annotation.elidable._   import scala.annotation.elidable._

     &

Scala gt; @elidable (FINE) def logDebug (arg: String) = println (arg)

     

logDebug: (arg: String) Unit

     &

Scala gt; logDebug (" & test quot;)

     

Scala gt &;

Con elide-beloset

  

C: > scala -Xelide-below 0

     

Benvenuti in Scala versione 2.8.0.final (Java HotSpot (TM) VM a 64 bit Server, Java 1.   6.0_16).   Digita le espressioni per farle valutare.   Digitare: aiuto per ulteriori informazioni.

     &

Scala gt; import scala.annotation.elidable   import scala.annotation.elidable

     &

Scala gt; import scala.annotation.elidable._   import scala.annotation.elidable._

     &

Scala gt; @elidable (FINE) def logDebug (arg: String) = println (arg)

     

logDebug: (arg: String) Unit

     &

Scala gt; logDebug (" & test quot;)

     

test

     

Scala gt &;

Vedi anche Definizione di asserzione Scala

La registrazione condizionale è malvagia. Aggiunge disordine inutile al tuo codice.

Devi sempre inviare gli oggetti che hai al logger:

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

e quindi avere un java.util.logging.Formatter che utilizza MessageFormat per appiattire foo e bar nella stringa da emettere. Verrà chiamato solo se il logger e il gestore accederanno a quel livello.

Per ulteriore piacere potresti avere un qualche tipo di linguaggio di espressioni per poter avere un controllo accurato su come formattare gli oggetti registrati (toString potrebbe non essere sempre utile).

Per quanto odio le macro in C / C ++, al lavoro abbiamo #define per la parte if, che se false ignora (non valuta) le seguenti espressioni, ma se true restituisce un flusso in cui è possibile reindirizzare roba usando il "< < ' operatore. In questo modo:

LOGGER(LEVEL_INFO) << "A String";

Suppongo che ciò eliminerebbe l'ulteriore "complessità" che il tuo strumento vede, ed eliminerebbe anche qualsiasi calcolo della stringa o qualsiasi espressione da registrare se il livello non fosse raggiunto.

Ecco una soluzione elegante che utilizza l'espressione ternaria

logger.info (logger.isInfoEnabled ()? " L'istruzione di registro va qui ... ": null);

Considera una funzione util di registrazione ...

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

Quindi effettua la chiamata con un " chiusura " arrotondare la valutazione costosa che si desidera evitare.

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top