61


34

"https://stackoverflow.com/questions/20702/whats-youra-git-limit-for- cyclomatic-complexity [循環的複雑さに対するあなたの/良い限界は何ですか?]"を読んだ後、私の同僚の多くがかなりの人たちだったことを私は実感します私たちのプロジェクトでは、この新しい QAポリシーに悩まされていました:関数ごとにこれ以上10 cyclomatic complexはありません。

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

しかし:私たちのプロジェクト(200人 - 7年間)の始めには、私たちは喜んでログを記録していました(そして、いいえ、それをある種の 'http://en.wikipedia.org/wiki/Aspect-に簡単に委任することはできません。 oriented_programming [アスペクト指向プログラミング]のログへのアプローチ)。

myLogger.info( "A String"); myLogger.fine( "より複雑な文字列");
...

そして私たちのシステムの最初のバージョンが稼働するとき、私たちはロギング(一時的にオフにされていた)のためではなく、常に計算され、次に渡される_log parameters_のために巨大なメモリ問題を経験しました。 'info()'または 'fine()'関数。ログ記録のレベルが 'OFF’であり、ログ記録が行われていないことを検出するためだけに使用されます。

そこで、QAが戻ってきて、プログラマに条件付きロギングをするように勧めました。 常に。

if(myLogger.isLoggable(Level.INFO){myLogger.info( "A String")); if(myLogger.isLoggable(Level.FINE){myLogger.fine( "より複雑なString");
...

しかし、今や、その「移動できない」10個の関数ごとの循環的複雑度レベルでは、それぞれの "if(isLoggable())"が1巡回複雑度として数えます!

そのため、関数が8つの「if」、「else」などを、密接に結合された、共有が容易ではないアルゴリズム、および3つの重要なログアクションで実行するとします。 条件付きログがその関数の複雑さの_実際_の一部ではないかもしれないとしても、彼らは限界に違反します…​

この状況にどう対処しますか。 私のプロジェクトでは(その「衝突」のために)いくつか興味深いコーディングの進化を見ましたが、最初にあなたの考えを得たいと思います。

'' '' '

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

私は今、「 variadic関数」というポイントをあなたの何人かによって進めています(ありがとうJohn)。 注:java6での簡単なテストでは、私の varargs関数は呼び出される前に引数を評価することを示しています。関数呼び出しには適用されませんが、toString()が必要な場合にのみ呼び出される 'Log Retrieverオブジェクト'(または 'function wrapper')には適用されません。 とった。

私は今このトピックに関する私の経験を投稿しました。 私は投票のために来週の火曜日までそこに残しておきます、そして私はあなたの答えの一つを選びます。 もう一度、すべての提案をありがとうございました:)

12 Answer


53


現在のロギングフレームワークでは、問題は議論の余地があります

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

特にlogステートメントが実行頻度の高いコードパスにある場合、これは比較的コストがかかります。 そして、上で書かれたように、ログレベルが高すぎるのでロガーが結果を捨てることに束縛されていてもその高価なメッセージフォーマットが発生します。

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

  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」オブジェクトを使用できます。これにより、基数をよりきちんと処理するための選択フォーマットなどの追加機能が提供されます。 もう1つの選択肢は、基本的な `toString()`メソッドではなく、「評価」用に定義したインタフェースを呼び出す独自のフォーマット機能を実装することです。


30


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

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

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

'' '' '

これは、引数を取得するのが難しい場合には、実際には意図されていませんが、それらを文字列にフォーマットすることに費用がかかる場合のためです。 たとえば、コードにすでに番号のリストが含まれている場合は、そのリストをデバッグ用にログに記録することをお勧めします。 `mylist.toString()`を実行しても、結果が捨てられるので何の利益もないのに少し時間がかかります。 そこで、 `mylist`をパラメータとしてlogging関数に渡し、それに文字列フォーマットを処理させます。 そうすれば、フォーマットは必要な場合にのみ実行されます。

'' '' '

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()`呼び出しの代わりに関数呼び出しのオーバーヘッドだけを被るでしょう。


6


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

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

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


4


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

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

はい、1つのプロジェクト(「1つのプログラムを単一のプロダクションプラットフォーム上で独自にデプロイして実行する」の場合)については、すべて技術的なことが可能です。

  • toString()を呼び出すだけのLoggerラッパーに渡すことができる専用の「Log Retriever」オブジェクト

  • ロギング variadic function(または単純なObject []配列!)と組み合わせて使用​​します。

@ John Millikinと@ericksonによって説明されているとおりです。

しかし、この問題は私たちに「なぜ私たちがそもそも伐採していたのか」について少し考えることを強いました。私たちのプロジェクトは実際には非同期通信のニーズと中央バスアーキテクチャで、さまざまなプロダクションプラットフォームにデプロイされた30の異なるプロジェクト(それぞれ5〜10人)です。 質問で説明されている単純なロギングは、始めの(5年前の)各プロジェクトにとっては問題ありませんでしたが、それ以来、私たちはステップアップしなければなりません。 KPIを入力してください。

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

そのKPIオブジェクトは誰がそれを呼び出すのかを知っており、彼はアプリケーションの最初から実行されるので、ログ記録時にその場で以前に計算していた大量のデータを取得することができます。 さらに、そのKPIオブジェクトは独立して監視でき、オンデマンドでその情報を1つの別々の公開バスで計算/公開できます。 こうすることで、各クライアントは正しいログファイルを探したり、暗号化された文字列を探したりするのではなく、自分が実際に欲しい情報を尋ねることができます。

確かに、「なぜ私たちはそもそもログインしていたのですか」という質問です。プログラマと彼の単体テストまたは統合テストのためだけにログを記録しているのではなく、最終的なクライアント自体を含むもっと広いコミュニティのためにログを記録していることに気付かせました。 私たちの '報告’メカニズムは、24時間365日、一元化され、非同期でなければなりませんでした。

そのKPIメカニズムの詳細は、この質問の範囲外です。 適切な校正は、私たちが直面している最も複雑で機能しない単一の問題です。 それはまだ時々膝の上にシステムをもたらします! 正しく校正されていても、それは命を救うものです。

もう一度、すべての提案をありがとうございました。 単純なロギングがまだ整っている場合は、システムの一部について考慮します。 しかし、この質問のもう1つのポイントは、はるかに大きく複雑な状況で特定の問題をあなたに説明することでした。 あなたがそれを好き願っています。 私は来週後半にKPIについて質問するかもしれません(これは今までのところSOFに関する質問には関係ありません!)。

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


4


これは単純すぎるかもしれませんが、guard節の周囲にある "extract method"リファクタリングを使用するのはどうでしょうか。 これのあなたの例のコード:

public void Example(){if(myLogger.isLoggable(Level.INFO))myLogger.info( "A String"); if(myLogger.isLoggable(Level.FINE))myLogger.fine( "より複雑な文字列"); //テストとログメッセージごとに1

これになります:

public void Example(){_LogInfo();} _LogFine(); //各テストおよびログメッセージに対して0

private void _LogInfo(){if(!myLogger.isLoggable(Level.INFO))が返す。

//複雑な引数の計算/評価は必要なときにだけ行ってください。 }

private void _LogFine(){/ *同上... * /}


3


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


3


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

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

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

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

myLogger.info(Level.INFO、 "A文字列%d"、some_number);

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


2


Scalaにはhttp://daily-scala.blogspot.com/2010/04/elidable-remove-method-calls-at-compile.html[@elidableの告知がありますこれにより、コンパイラフラグを使用してメソッドを削除できます。

スカラとREPL:

_ _ C:>スカラ

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

scala>インポートscala.annotation.elidableインポートscala.annotation.elidable

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

scala> @elidable(FINE)def logDebug(引数:文字列)= println(引数)

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>インポートscala.annotation.elidableインポートscala.annotation.elidable

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

scala> @elidable(FINE)def logDebug(引数:文字列)= println(引数)

logDebug:(引数:文字列)単位

scala> logDebug( "テスト")

テスト中

スカラ> _ _


2


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

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

ロガーロガー= ... logger.log(Level.DEBUG、 "fooは{0}、barは{1}です"、new Object [] {foo、bar});

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

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


1


C / Cのマクロが嫌いなのと同じように、if部分には#definesが定義されています。falseの場合、次の式は無視されます(評価されません)。 << '演算子 このような:

LOGGER(LEVEL_INFO)<< "文字列";

私はこれがあなたのツールが見る余分な「複雑さ」を排除し、そしてまた文字列の計算、またはレベルに達していない場合に記録されるべきどんな表現も排除すると思います。