71

「あなた/循環的複雑度の適切な制限は何ですか? を読んだ後、同僚の多くが、私たちのプロジェクトに関するこの新しいQAポリシーに非常に腹を立てていることに気付きました。

意味: '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 の循環的複雑度レベルが制限されているため、関数に入れるさまざまなログは負担として感じられると主張しています。 +1 循環的複雑度としてカウントされます!

そのため、関数に 8 つの「if」、「else」などがある場合、1 つの密結合された簡単に共有できないアルゴリズムと 3 つの重要なログ アクションがあります...条件付きログが実際にはそうでなくても、それらは制限に違反します。その機能の前述の複雑さの一部...

この状況にどのように対処しますか?
私のプロジェクトでは、(その「競合」による) 興味深いコーディングの進化がいくつか見られましたが、最初にあなたの考えを聞きたいだけです.


すべての回答に感謝します。 問題は「フォーマット」関連ではなく、「引数評価」
関連であると主張しなければなりません(何もしないメソッドを呼び出す直前に、非常にコストがかかる可能性のある評価)実際には aFunction() を意味し、 aFunction
() は文字列を返し、ロガーによって表示されるすべての種類のログ データを収集および計算する複雑なメソッドへの呼び出しである... またはそうでない (したがって、問題、および条件付きログを使用するため、「循環的複雑さ」の人為的な増加という実際の問題...)

私は今、あなたの何人かによって進められた '可変個引数関数' ポイントを取得します (ジョンに感謝します)。
注: java6 での簡単なテストでは、varargs 関数が呼び出される前に引数を評価することが示されているため、関数呼び出しには適用できませんが、「ログ取得オブジェクト」(または「関数ラッパー」) には適用できません。 ) は、必要な場合にのみ呼び出されます。とった。

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

4

12 に答える 12

64

現在のロギング フレームワークでは、問題は意味がありません

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

古いログ ライブラリを使用する必要がある場合は、このまま読み進めて、背景情報を取得し、パラメータ化されたメッセージで古いライブラリを改造する方法を確認してください。

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

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

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

柔軟性のないメトリクスは、そうでなければ優秀なプログラマーを悪者に変える可能性があります。気をつけて!

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

条件付きログの必要性

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

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()これらのメソッドは、独自のインスタンスを作成し、潜在的に大きなオブジェクト グラフ全体でそのメンバーのメソッドを呼び出す可能性があります。(Java 5 より前は、が使用され、すべての操作が同期されていたため、さらに高価でした。)toString()StringBuildertoString()StringBuffer

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

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

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

このガードにより、引数dw文字列の連結の評価は、必要な場合にのみ実行されます。

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

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

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オブジェクトを "format" の代わりに使用できることにも注意してくださいString。これにより、基数をより適切に処理するための選択形式などの追加機能が提供されます。もう 1 つの方法は、基本的なメソッドではなく、「評価」用に定義したインターフェイスを呼び出す独自の書式設定機能を実装することですtoString()

于 2008-09-19T21:46:24.293 に答える
33

Python では、フォーマットされた値をパラメーターとしてロギング関数に渡します。文字列の書式設定は、ログが有効になっている場合にのみ適用されます。関数呼び出しのオーバーヘッドはまだありますが、それは書式設定に比べればごくわずかです。

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

可変引数を持つ任意の言語 (C/C++、C#/Java など) に対して、このようなことを行うことができます。


これは、引数を取得するのが難しい場合を意図したものではありませんが、引数を文字列にフォーマットするとコストがかかる場合を対象としています。たとえば、コードに既に数値のリストが含まれている場合、デバッグのためにそのリストをログに記録することができます。mylist.toString()結果が破棄されるため、実行にはしばらく時間がかかります。したがってmylist、パラメーターとしてロギング関数に渡し、文字列の書式設定を処理させます。そうすれば、フォーマットは必要な場合にのみ実行されます。


OPの質問は特にJavaに言及しているため、上記の使用方法は次のとおりです。

問題は「フォーマット」に関連しているのではなく、「引数の評価」に関連していると主張しなければなりません (何もしないメソッドを呼び出す直前に、実行するのに非常にコストがかかる評価)

秘訣は、絶対に必要になるまで高価な計算を実行しないオブジェクトを用意することです。これは、ラムダとクロージャをサポートする Smalltalk や Python などの言語では簡単ですが、Java でも少し想像力を働かせれば実行できます。

function があるとします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()呼び出しではなく、関数呼び出しのオーバーヘッドのみを被ります。

于 2008-09-19T22:06:12.153 に答える
6

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

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

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

于 2008-09-19T21:50:02.863 に答える
4

これは単純すぎるかもしれませんが、guard句の周りで「extractメソッド」リファクタリングを使用するのはどうでしょうか。これのサンプルコード:

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 ... */ }
于 2008-09-28T01:07:10.427 に答える
4

ご回答ありがとうございます。あなたたち最高 :)

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

はい、1 つのプロジェクト(「1 つのプログラムが 1 つの実稼働プラットフォームにデプロイされ、単独で実行されている」など) については、すべて技術的なことを私に任せることができると思います。

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

@John Millikin と @erickson が説明しているように、これで終わりです。

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

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

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

確かに、「そもそもなぜログを記録していたのか?」という疑問が生じます。プログラマーと彼のユニットまたは統合テストのためだけにログを記録しているのではなく、最終的なクライアント自身の一部を含むはるかに広いコミュニティのためにログを記録していることに気付きました。私たちの「レポート」メカニズムは、24 時間 365 日、非同期で一元化する必要がありました。

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

繰り返しますが、すべての提案に感謝します。単純なロギングがまだ行われているときに、システムの一部についてそれらを検討します。
しかし、この質問のもう 1 つのポイントは、はるかに大きく、より複雑なコンテキストで特定の問題を説明することでした。
気に入っていただければ幸いです。来週後半に KPI について質問するかもしれません (信じられないかもしれませんが、SOF については今のところ疑問の余地はありません!)。

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

于 2008-09-20T07:30:20.910 に答える
3

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

//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);   

それはあなたの基準を満たすはずです。

于 2008-09-19T21:45:56.650 に答える
3

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

于 2008-09-19T21:42:18.157 に答える
2

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

持っているオブジェクトを常にロガーに送信する必要があります。

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

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

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

于 2011-07-02T22:15:39.393 に答える
2

代替テキスト
(出典: 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: (引数: 文字列)単位

scala> logDebug("テスト")

スカラ>

エリデベロセット付き

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: (引数: 文字列)単位

scala> logDebug("テスト")

テスト

スカラ>

Scala assert の定義も参照してください。

于 2010-07-20T17:22:33.927 に答える
1

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

LOGGER(LEVEL_INFO) << "A String";

これにより、ツールが認識する余分な「複雑さ」が解消され、文字列の計算や、レベルに達していない場合にログに記録される式が解消されると思います。

于 2008-09-19T21:47:03.777 に答える
1

ロギング 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;
       }
    }
);
于 2011-11-22T13:48:50.837 に答える