webdevqa.jp.net

循環的複雑度を最小限に抑えた条件付きロギング

循環的複雑度のあなたの/良い限界は何ですか? 」を読んだ後、私の多くの同僚がこの新しいことにかなり悩まされていることに気づきました [〜#〜] qa [〜#〜] 私たちのプロジェクトのポリシー:10以上 循環的複雑度 関数ごと。

意味:「if」、「else」、「try」、「catch」、およびその他のコードワークフロー分岐ステートメントは10以下。正しい。 「 プライベートメソッドをテストしますか? 」で説明したように、このようなポリシーには多くの良い副作用があります。

しかし、私たち(200人-7年)のプロジェクトの最初は、幸いにもログに記録していました(そして、いや、それをある種の ' アスペクト指向プログラミング 'アプローチに簡単に委任することはできません)。ログ)。

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

そして、システムの最初のバージョンが稼働したとき、ロギング(ある時点でオフになっていた)のためではなく、ログパラメータ(文字列)。これは常に計算され、「info()」または「fine()」関数に渡されます。ログ記録のレベルが「OFF」であり、ログ記録が行われていないことがわかります開催中!

そこで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」などがあり、1つの密結合の容易に共有できないアルゴリズムと3つの重要なログアクションがある場合...条件付きログは本当にその関数の前述の複雑さの一部...

この状況にどのように対処しますか?
自分のプロジェクトでいくつかの興味深いコーディングの進化(「競合」のため)を見てきましたが、最初に考えてもらいたいだけです。


たくさんの回答ありがとうございます。
問題は「フォーマット」に関連するものではなく、「引数の評価」に関連するものであると主張する必要があります(何もしないメソッドを呼び出す直前に、評価に非常にコストがかかる可能性がある評価)
したがって、「A String」の上に書いたとき、私は実際にはaFunction()を意味し、aFunction()はStringを返し、表示されるすべての種類のログデータを収集および計算する複雑なメソッドの呼び出しでしたロガー...かどうか(したがって問題、および条件付きロギングを使用する義務、したがって、「循環的複雑度」の人為的な増加の実際の問題...)

これで、一部のユーザー(Johnに感謝します)が「 variadic function」ポイントを進めました。
注:Java6での簡単なテストは、私の varargs function が呼び出される前に引数を評価することを示しているため、関数呼び出しではなく、「ログ取得オブジェクト」に適用できます(または「関数ラッパー」)、必要な場合にのみtoString()が呼び出されます。とった。

このトピックに関する私の経験を投稿しました。
投票のために次の火曜日までそのままにしておきます。その後、あなたの回答の1つを選択します。
繰り返しますが、すべての提案に感謝します:)

64
VonC

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()呼び出しではなく、関数呼び出しのオーバーヘッドのみを受けます。

31
John Millikin

現在のロギングフレームワークでは、問題は未解決です

Slf4jやlog4j 2などの現在のロギングフレームワークでは、ほとんどの場合、guardステートメントは必要ありません。それらはパラメーター化されたログステートメントを使用して、イベントを無条件でログに記録できるようにしますが、メッセージのフォーマットはイベントが有効な場合にのみ発生します。メッセージの構築は、アプリケーションによって事前に実行されるのではなく、ロガーによって必要に応じて実行されます。

旧式のロギングライブラリを使用する必要がある場合は、以下を読んで、より多くの背景情報と、パラメータ化されたメッセージで古いライブラリを改造する方法を入手できます。

ガードステートメントは本当に複雑さを増していますか?

循環保護の計算からロギングガードステートメントを除外することを検討してください。

予測可能な形式のため、条件付きロギングチェックは実際にはコードの複雑さに寄与しないと主張することができます。

柔軟性のないメトリックは、他の点では優れたプログラマーを悪くする可能性があります。注意してください!

複雑さを計算するためのツールをその程度に調整できないと仮定すると、次のアプローチは回避策を提供する可能性があります。

条件付きロギングの必要性

次のようなコードがあるため、ガードステートメントが導入されたと思います。

_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では、各ログステートメントは新しいStringBuilderを作成し、文字列に連結された各オブジェクトでtoString()メソッドを呼び出します。これらのtoString()メソッドは、次に、独自のStringBuilderインスタンスを作成し、メンバーのtoString()メソッドを呼び出す可能性があります。ラージオブジェクトグラフ。 (Java 5以前は、StringBufferが使用され、そのすべての操作が同期されているため、さらに高額でした。)

これは、特にログステートメントが頻繁に実行されるコードパスにある場合、比較的コストがかかる可能性があります。また、上記のように記述すると、ログレベルが高すぎるためにロガーが結果を破棄するようバインドされている場合でも、その高価なメッセージフォーマットが発生します。

これにより、次の形式のガードステートメントが導入されます。

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

このガードを使用すると、引数dおよびwと文字列連結の評価は、必要な場合にのみ実行されます。

シンプルで効率的なロギングのソリューション

ただし、ロガー(または選択したロギングパッケージの周囲に記述するラッパー)がフォーマッターとフォーマッターの引数を取る場合、メッセージの構築は、ガードステートメントとそのステートメントを削除しながら、使用されることが確実になるまで遅らせることができます。循環的複雑さ。

_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()呼び出しは発生しませんそれらが必要でない限り!これにより、ガードステートメントにつながるパフォーマンスヒットが効果的に排除されます。 Javaでの小さなペナルティの1つは、ロガーに渡すプリミティブ型の引数の自動ボックス化です。

乱雑な文字列の連結がなくなっているため、ロギングを実行するコードはおそらくこれまでよりもさらにクリーンです。フォーマット文字列が外部化されている場合(ResourceBundleを使用して)はさらにきれいになり、ソフトウェアのメンテナンスやローカライズにも役立ちます。

さらなる強化

また、Javaでは、MessageFormatオブジェクトを「フォーマット」Stringの代わりに使用できます。これにより、基数をより適切に処理するための選択フォーマットなどの追加機能が提供されます。もう1つの方法は、基本的なtoString()メソッドではなく、「評価」用に定義したインターフェイスを呼び出す独自のフォーマット機能を実装することです。

55
erickson

パラメータとしてラムダ式またはコードブロックをサポートする言語では、これに対する1つの解決策は、それをロギングメソッドに与えることです。その1つは構成を評価でき、必要な場合にのみ、実際に提供されたラムダ/コードブロックを呼び出し/実行できます。ただし、まだ試していない。

理論的にこれは可能です。ログにラムダ/コードブロックを大量に使用すると予想されるパフォーマンスの問題のため、本番環境では使用しません。

しかし、いつものように:疑わしい場合は、テストしてCPU負荷とメモリへの影響を測定してください。

6
pointernil

これは単純すぎるかもしれませんが、「extractメソッド」を使用して、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 ... */ }
4
flipdoubt

たくさんのご回答ありがとうございます!あなたたち最高 :)

今私のフィードバックはあなたのフィードバックほど簡単ではありません:

はい、1つのプロジェクトの場合(「1つのプログラムが単一の運用プラットフォームで独自にデプロイされて実行される」のように)、すべての技術に進むことができると思います私に:

  • 専用の「Log Retriever」オブジェクト。これは、toString()を呼び出すだけでLoggerラッパーに渡すことができます。
  • ロギングと組み合わせて使用​​ 変数関数 (またはプレーンなObject []配列!)

@John Millikinと@ericksonで説明されているように、そこにあります。

ただし、この問題により、「なぜ最初からログインしていたのか」について少し考える必要がありました。
私たちのプロジェクトは実際には、非同期通信のニーズと中央バスアーキテクチャを備えたさまざまな本番プラットフォームにデプロイされた30の異なるプロジェクト(それぞれ5〜10人)です。
質問に記載されている単純なロギングは、各プロジェクトの最初(5年前)で問題ありませんでしたが、それ以来、ステップアップする。 [〜#〜] kpi [〜#〜] と入力します。

ロガーに何かを記録するように依頼する代わりに、自動的に作成されたオブジェクト(KPIと呼ばれる)にイベントを登録するように依頼します。これは単純な呼び出し(myKPI.I_am_signaling_myself_to_you())であり、条件付きである必要はありません(これにより、「循環的複雑度の人工的な増加」の問題が解決されます)。

そのKPIオブジェクトは、誰がそれを呼び出すかを認識しており、アプリケーションの最初から実行しているため、以前ログに記録していたときに、その場で計算していた大量のデータを取得できます。
さらに、KPIオブジェクトを個別に監視し、その情報を単一の別個のパブリケーションバスでオンデマンドで計算/パブリッシュできます。
そのようにして、各クライアントは、正しいログファイルを探して不可解なものを探すのではなく、実際に必要な情報を要求できます(たとえば、「プロセスが開始され、開始された場合、いつから?」)。ストリング...

実際、「なぜ最初からログに記録していたのか」という質問です。プログラマーと彼のユニットまたは統合テストのためだけにログを記録しているのではなく、最終的なクライアント自身を含むより広範なコミュニティのためにログを記録していることを私たちに認識させました。私たちの「報告」メカニズムは、中央集中型、非同期、24時間年中無休である必要がありました。

そのKPIメカニズムの詳細は、この質問の範囲外です。適切なキャリブレーションは、断然、私たちが直面している最も複雑な非機能的な問題の1つであると言えば十分です。それでも時々システムをひざまずきます!適切に調整されていますが、それは命の恩人です。

繰り返しますが、すべての提案に感謝します。単純なロギングがまだ実施されている場合は、システムの一部についてそれらを考慮します。
しかし、この質問のもう1つのポイントは、はるかに大きく複雑な状況で特定の問題を説明することでした。
あなたはそれが好きだと思います。来週遅くに、KPIについて質問するかもしれません(信じるかどうかは別として、SOFについてこれまでのところ質問していません!)。

私はこの答えを次の火曜日まで投票のために残し、それから答えを選択します(これは明らかにこれではありません;))

4
VonC

CまたはC++では、条件付きロギングのifステートメントの代わりにプリプロセッサーを使用します。

3
Tom Ritter

ログレベルをロガーに渡し、ログステートメントを書き込むかどうかをロガーに決定させます。

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

更新:ああ、条件付きステートメントなしで条件付きでログ文字列を作成したいと思います。おそらくコンパイル時ではなく実行時に。

これを解決した方法は、ロガークラスにフォーマットコードを配置して、レベルが合格した場合にのみフォーマットが行われるようにすることです。組み込みのsprintfとよく似ています。例えば:

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

それはあなたの基準を満たしているはずです。

3
Greg Miskin

条件付きロギングは悪です。コードに不要な混乱を追加します。

あなたは常にあなたが持っているオブジェクトをロガーに送るべきです:

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

次に、MessageFormatを使用してfooをフラット化し、出力する文字列にバーを挿入するJava.util.logging.Formatterを用意します。ロガーとハンドラーがそのレベルでログを記録する場合にのみ呼び出されます。

追加の喜びのために、ログに記録されたオブジェクトをフォーマットする方法を細かく制御できるようにするために、ある種の式言語を使用できます(toStringは常に役立つとは限りません)。

2
simon

alt text
(ソース: scala-lang.org

Scala には注釈があります @ elidable() コンパイラフラグを使用してメソッドを削除できます。

scala REPL:

C:>スカラ

Scalaバージョン2.8.0.finalへようこそ(Java HotSpot(TM)64ビットサーバーVM、Java 1. 6.0_16)。式を入力して、詳細については、:helpと入力してください。

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

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

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

logDebug:(arg:String)Unit

scala> logDebug( "testing")

scala>

Elide-belosetを使用

C:> scala -Xelide-below 0

Scalaバージョン2.8.0.finalへようこそ(Java HotSpot(TM)64ビットサーバーVM、Java 1. 6.0_16)。式を入力して、詳細については、:helpと入力してください。

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

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

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

logDebug:(arg:String)Unit

scala> logDebug( "testing")

テスト

scala>

参照 Scalaアサート定義

2
oluies

ロギングutil関数を検討してください...

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;
       }
    }
);
1
johnlon

ここに三項式を使用したエレガントなソリューションがあります

logger.info(logger.isInfoEnabled()? "ログステートメントがここに表示されます...":null);

1

私がC/C++でマクロを嫌っているのと同じくらい、職場ではif部分に#definesがあります。これはfalseの場合、次の式を無視(評価しません)しますが、trueの場合、 'を使用してパイプすることができるストリームを返します。 << '演算子。このような:

LOGGER(LEVEL_INFO) << "A String";

これにより、ツールに表示される余分な「複雑さ」がなくなり、文字列の計算や、レベルに達しなかった場合にログに記録される式もすべてなくなると思います。

1
quamrana