Question

Après avoir lu " Ce que & # 8217; s votre / une bonne limite pour la complexité cyclomatique? " ;, je réalise que beaucoup de mes collègues ont été assez contrariés par cette nouvelle Politique d'assurance de la qualité sur notre projet: plus de 10 complexité cyclomatique par fonction.

Signification: pas plus de 10 instructions if, 'else', 'try', 'catch' et autres instructions de branchement de flux de travail de code. Droite. Comme je l'ai expliqué dans la Testez-vous la méthode privée? , une telle stratégie a de nombreux bons effets secondaires.

Mais: au début de notre projet (200 personnes - 7 ans), nous étions en train de nous connecter (et non, nous ne pouvons pas facilement déléguer cela à une sorte de ' Programmation orientée aspect pour les journaux).

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

Et lorsque les premières versions de notre système ont été mises en ligne, nous avons rencontré un énorme problème de mémoire, non pas à cause de la journalisation (qui était à un moment désactivée), mais à cause des paramètres de journalisation (les chaînes ), qui sont toujours calculés, puis passés aux fonctions 'info ()' ou 'fine ()', uniquement pour découvrir que le niveau de journalisation était 'OFF' et qu'aucune journalisation n'avait lieu!

Donc, QA est revenu et a exhorté nos programmeurs à faire de la journalisation conditionnelle. Toujours.

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

Mais maintenant, avec ce niveau de complexité cyclomatique 10 impossible à déplacer "par fonction, ils affirment que les différents journaux qu’ils mettent dans leur fonction sont perçus comme un fardeau, parce que chaque &"; (isLoggable ()) " est compté comme +1 complexité cyclomatique!

Donc, si une fonction a 8 'if', 'else' et ainsi de suite, dans un algorithme étroitement couplé qui ne peut pas être partagé facilement, et 3 actions de journalisation critiques ... elles dépassent la limite même si les journaux conditionnels peuvent ne pas être vraiment une partie de ladite complexité de cette fonction ...

Comment régleriez-vous cette situation?
J'ai vu quelques modifications de code intéressantes (en raison de ce "conflit") dans mon projet, mais je veux juste avoir votre avis en premier.

Merci pour toutes les réponses.
Je dois insister sur le fait que le problème n'est pas lié au "formatage", mais à "l'évaluation des arguments" (évaluation qui peut être très coûteuse à faire, juste avant d'appeler une méthode qui ne fera rien).
Ainsi, quand un & "Chaîne &"; Écrit ci-dessus, je voulais en fait une fonction (), avec une fonction () renvoyant une chaîne, et étant un appel à une méthode compliquée collectant et calculant toutes sortes de données de journal être affiché par l'enregistreur ... ou non (d'où le problème et l'obligation d'utiliser la journalisation conditionnelle, d'où le problème de l'augmentation artificielle de la "complexité cyclomatique" ...)

Je reçois maintenant le point de fonction variadic avancé par certains d'entre vous (merci John ).
Remarque: un test rapide en java6 montre que ma fonction varargs évalue ses arguments avant d'être appelé, il ne peut donc pas être appliqué pour un appel de fonction, mais pour un 'objet log retriever' (ou 'encapsuleur de fonction'), sur lequel toString () ne sera appelé que si nécessaire. J'ai compris.

J'ai maintenant posté mon expérience sur ce sujet.
Je vais en rester là jusqu'à mardi prochain pour le vote, puis je choisirai l'une de vos réponses.
Encore une fois, merci pour toutes les suggestions:)

Était-ce utile?

La solution

En Python, vous transmettez les valeurs formatées en tant que paramètres à la fonction de journalisation. Le formatage de chaîne n'est appliqué que si la journalisation est activée. Il y a toujours la surcharge d'un appel de fonction, mais c'est minuscule comparé au formatage.

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

Vous pouvez faire quelque chose comme ceci pour n'importe quel langage avec des arguments variadiques (C / C ++, C # / Java, etc.).

Cela n’est pas vraiment destiné aux arguments difficiles à récupérer, mais au formatage des chaînes, cela coûte cher. Par exemple, si votre code contient déjà une liste de nombres, vous pouvez enregistrer cette liste pour le débogage. L'exécution de mylist.toString() prendra un certain temps, mais le résultat sera jeté. Vous transmettez donc mylist en tant que paramètre à la fonction de journalisation et le laissez gérer le formatage des chaînes. De cette façon, le formatage ne sera effectué que si nécessaire.

La question du PO mentionnant spécifiquement Java, voici comment utiliser ce qui précède:

  

Je dois insister sur le fait que le problème n'est pas lié au "formatage", mais à "l'évaluation des arguments" (évaluation qui peut être très coûteuse à faire, juste avant d'appeler une méthode qui ne fera rien)

Le truc, c’est d’avoir des objets qui ne feront pas de calculs coûteux avant d’être absolument nécessaires. C’est facile dans des langages comme Smalltalk ou Python qui prennent en charge les lambdas et les fermetures, mais cela reste réalisable en Java avec un peu d’imagination.

Disons que vous avez une fonction get_everything(). Il va récupérer chaque objet de votre base de données dans une liste. Vous ne voulez pas appeler cela si le résultat est ignoré, évidemment. Donc, au lieu d’appeler directement cette fonction, vous définissez une classe interne appelée 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());
    }
}

Dans ce code, l'appel à getEverything() est encapsulé afin qu'il ne soit réellement exécuté que si nécessaire. La fonction de journalisation ne s’exécutera toString() sur ses paramètres que si le débogage est activé. De cette manière, votre code ne subira que la surcharge d’un appel de fonction au lieu de <=> l'appel complet.

Autres conseils

Avec les frameworks de journalisation actuels, la question est sans objet

Les infrastructures de journalisation actuelles, telles que slf4j ou log4j 2, n’exigent pas d’instructions de protection dans la plupart des cas. Ils utilisent une instruction de journal paramétrée pour qu'un événement puisse être consigné de manière inconditionnelle, mais le formatage des messages ne se produit que si l'événement est activé. La construction du message est effectuée à la demande de l'enregistreur, plutôt que de manière préventive par l'application.

Si vous devez utiliser une bibliothèque de journalisation antique, poursuivez votre lecture pour obtenir davantage d'informations sur l'arrière-plan et la possibilité de moderniser l'ancienne bibliothèque avec des messages paramétrés.

Les déclarations de garde ajoutent-elles vraiment de la complexité?

Envisagez d’exclure les instructions des gardes de la journalisation du calcul de la complexité cyclomatique.

On pourrait faire valoir qu'en raison de leur forme prévisible, les vérifications de la journalisation conditionnelle ne contribuent pas vraiment à la complexité du code.

Des métriques inflexibles peuvent gâcher le bon programmeur. Attention!

En supposant que vos outils de calcul de la complexité ne puissent pas être adaptés à ce degré, l'approche suivante peut offrir une solution de contournement.

Nécessité d'une journalisation conditionnelle

Je suppose que vos déclarations de garde ont été introduites parce que vous aviez un code comme celui-ci:

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

En Java, chacune des instructions de journal crée un nouveau StringBuilder et appelle la méthode toString() sur chaque objet concaténé à la chaîne. Ces méthodes StringBuffer, à leur tour, sont susceptibles de créer d leurs propres instances et d'appeler les méthodes w de leurs membres, etc., sur un graphe d'objet potentiellement volumineux. (Avant Java 5, cela coûtait encore plus cher, car ResourceBundle était utilisé et toutes ses opérations étaient synchronisées.)

Cela peut être relativement coûteux, en particulier si l’instruction du journal se trouve dans un chemin de code très exécuté. Et, écrit comme ci-dessus, ce formatage de message coûteux se produit même si le consignateur est tenu de rejeter le résultat car le niveau de consignation est trop élevé.

Ceci conduit à l'introduction de déclarations de garde de la forme:

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

Avec cette protection, l'évaluation des arguments MessageFormat et String et de la concaténation de chaînes est effectuée uniquement lorsque cela est nécessaire.

Une solution pour une journalisation simple et efficace

Toutefois, si le consignateur (ou un wrapper que vous écrivez autour du package de consignation choisi) utilise un formateur et des arguments pour le formateur, la construction du message peut être retardée jusqu'à ce qu'il soit certain qu'il sera utilisé, tout en éliminant la protection. déclarations et leur complexité cyclomatique.

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

}

Maintenant, aucun des appels <=> en cascade avec leurs allocations de mémoire tampon ne se produira , à moins qu'ils ne soient nécessaires! Cela élimine efficacement l'impact sur les performances qui a conduit aux déclarations de garde. Une petite pénalité, en Java, serait la mise en boîte automatique de tous les arguments de type primitif que vous transmettez au consignateur.

Le code effectuant la journalisation est encore plus propre que jamais, car la concaténation de chaînes désordonnée a disparu. Cela peut même être plus propre si les chaînes de format sont externalisées (avec un <=>), ce qui pourrait également faciliter la maintenance ou la localisation du logiciel.

Autres améliorations

Notez également qu'en Java, un objet <=> peut être utilisé à la place d'un " format " <=> qui vous offre des fonctionnalités supplémentaires telles qu'un format de choix pour gérer les nombres cardinaux de manière plus nette. Une autre solution consisterait à implémenter votre propre fonctionnalité de formatage qui appelle une interface que vous définissez pour & Quot; evaluation & Quot ;,, plutôt que la méthode de base <=>.

Dans les langages prenant en charge les expressions lambda ou les blocs de code, une solution à cela serait de donner cela à la méthode de journalisation. Celui-ci pourrait évaluer la configuration et, si nécessaire, appeler / exécuter le bloc lambda / code fourni. Je n'ai pas encore essayé, cependant.

Théoriquement c'est possible. Je ne voudrais pas l'utiliser en production en raison de problèmes de performances, je m'attends à cette utilisation intensive de lamdas / blocs de code pour la journalisation.

Mais comme toujours: en cas de doute, testez-le et mesurez son impact sur la charge et la mémoire du processeur.

Merci pour toutes vos réponses! Vous êtes rock:)

Maintenant, mes commentaires ne sont pas aussi simples que les vôtres:

Oui, pour un projet (comme dans "un programme déployé et exécuté de manière autonome sur une seule plate-forme de production"), je suppose que vous pouvez utiliser toute la technique sur moi:

  • les objets 'Log Retriever' dédiés, qui peuvent être transmis à un wrapper de l'enregistreur, seul l'appel de toString () est nécessaire
  • utilisé en conjonction avec une fonction variadic (ou un tableau Objet simple]! )

et voilà, comme l'expliquent @John Millikin et @erickson.

Cependant, cette question nous a obligés à réfléchir un peu sur la question de savoir pourquoi, exactement, nous nous connections en premier lieu?

Notre projet consiste actuellement en 30 projets différents (de 5 à 10 personnes chacun) déployés sur diverses plates-formes de production, avec des besoins de communication asynchrones et une architecture de bus central.
La journalisation simple décrite dans la question était correcte pour chaque projet au début (il y a 5 ans), mais depuis lors, nous devons intensifier nos efforts. Entrez le indicateur de performance clé .

Au lieu de demander à un enregistreur de consigner quoi que ce soit, nous demandons à un objet créé automatiquement (appelé KPI) d’enregistrer un événement. Il s’agit d’un appel simple (myKPI.I_am_signaling_myself_to_you ()), qui n’a pas besoin d’être conditionnel (ce qui résout le problème de l’augmentation artificielle de la complexité cyclomatique).

Cet objet KPI sait qui l'appelle et, puisqu'il s'exécute depuis le début de l'application, il est capable de récupérer de nombreuses données que nous étions en train de calculer sur place au moment de la journalisation.
De plus, cet objet KPI peut être surveillé indépendamment et calculer / publier à la demande ses informations sur un bus de publication unique et séparé.
Ainsi, chaque client peut demander les informations qu’il souhaite réellement (par exemple, «mon processus a-t-il commencé et si oui, depuis quand?»), Au lieu de rechercher le fichier journal correct et de rechercher une chaîne cryptée ...

En effet, la question "Pourquoi exactement nous nous connections en premier lieu?" nous a fait comprendre que nous ne nous connections pas uniquement pour le programmeur et ses tests unitaires ou d'intégration, mais pour une communauté beaucoup plus large comprenant certains des clients finaux eux-mêmes. Notre mécanisme de "reporting" devait être centralisé, asynchrone, 24 heures sur 24, 7 jours sur 7.

La spécificité de ce mécanisme d'indicateur de performance clé est hors du champ de cette question. Il suffit de dire que son bon calibrage est de loin le problème le plus compliqué et non fonctionnel le plus compliqué auquel nous sommes confrontés. Il met toujours le système sur ses genoux de temps en temps! Correctement calibré, c’est une bouée de sauvetage.

Encore une fois, merci pour toutes les suggestions. Nous les examinerons pour certaines parties de notre système lorsque la journalisation simple est toujours en place.
Mais l’autre objectif de cette question était de vous illustrer un problème spécifique dans un contexte beaucoup plus vaste et complexe.
J'espère que tu l'as aimé. Je pourrais poser une question sur les KPI (qui, croyez-le ou non, n’est pas une question sur les FOS jusqu’à présent!) Plus tard la semaine prochaine.

Je laisserai cette réponse en suspens jusqu'à mardi prochain, puis je choisirai une réponse (pas celle-ci évidemment;))

Peut-être que c'est trop simple, mais qu'en est-il de l'utilisation de la & "méthode d'extraction" & "; refactoring autour de la clause de garde? Votre exemple de code de ceci:

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
}

Devient ceci:

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

En C ou C ++, j'utiliserais le préprocesseur à la place des instructions if pour la journalisation conditionnelle.

Transmettez le niveau de journalisation à l'enregistreur et laissez-le décider s'il écrit ou non l'instruction de journalisation:

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

UPDATE: Ah, je vois que vous voulez créer conditionnellement la chaîne de log sans instruction conditionnelle. Probablement au moment de l'exécution plutôt que de la compilation.

Je dirai simplement que la solution que nous avons trouvée est de placer le code de formatage dans la classe de consignateur de sorte que le formatage ne soit effectué que si le niveau est correct. Très semblable à un sprintf intégré. Par exemple:

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

Cela devrait correspondre à vos critères.

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

Scala a une annontation @ elidable () qui vous permet de supprimer des méthodes avec un indicateur de compilation.

Avec le scala REPL:

  

C: > scala

     

Bienvenue dans la version 2.8.0.final de Scala (machine virtuelle serveur 64 bits Java HotSpot (TM), Java 1.   6.0_16).   Tapez des expressions pour les faire évaluer.   Tapez: help pour plus d'informations.

     

scala > importer scala.annotation.elidable   importer scala.annotation.elidable

     

scala > importer scala.annotation.elidable._   importer scala.annotation.elidable ._

     

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

     

logDebug: (arg: String) Unit

     

scala > logDebug (" tester ")

     

scala >

avec elide-beloset

  

C: > scala -Xelide-inférieur à 0

     

Bienvenue dans la version 2.8.0.final de Scala (machine virtuelle serveur 64 bits Java HotSpot (TM), Java 1.   6.0_16).   Tapez des expressions pour les faire évaluer.   Tapez: help pour plus d'informations.

     

scala > importer scala.annotation.elidable   importer scala.annotation.elidable

     

scala > importer scala.annotation.elidable._   importer scala.annotation.elidable ._

     

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

     

logDebug: (arg: String) Unit

     

scala > logDebug (" tester ")

     

test

     

scala >

Voir aussi Définition de l'assertion Scala

La journalisation conditionnelle est diabolique. Cela ajoute un fouillis inutile à votre code.

Vous devriez toujours envoyer les objets que vous avez à l'enregistreur:

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

et ensuite un java.util.logging.Formatter qui utilise MessageFormat pour aplatir foo et bar dans la chaîne à afficher. Il ne sera appelé que si le consignateur et le gestionnaire se connectent à ce niveau.

Pour encore plus de plaisir, vous pouvez utiliser un langage d’expression permettant de contrôler avec précision le formatage des objets enregistrés (toString peut ne pas toujours être utile).

Bien que je déteste les macros en C / C ++, au travail, nous avons #defines pour la partie if, qui si false ignore les expressions suivantes, mais si true renvoie un flux dans lequel des éléments peuvent être acheminés en utilisant le '< <' opérateur. Comme ceci:

LOGGER(LEVEL_INFO) << "A String";

Je suppose que cela éliminerait la "complexité" supplémentaire que votre outil verrait, ainsi que tout calcul de la chaîne ou toute expression à enregistrer si le niveau n'était pas atteint.

Voici une solution élégante utilisant l'expression ternaire

logger.info (logger.isInfoEnabled ()? " instruction de consignation va ici ... ": null);

Considérons une fonction d’utilisation de la journalisation ...

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

Puis appelez avec une " fermeture " arrondissez l’évaluation coûteuse que vous souhaitez éviter.

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top