FindBugsとPMDを使ったコード品質規則の紹介

FindBugsとPMDを使用したコード品質ルールの概要

1. 概要

この記事では、FindBugs、PMD、CheckStyleなどのコード分析ツールに搭載されている重要なルールの一部を取り上げます。

2. 巡回複雑度

2.1. 循環的複雑度とは何ですか?

コードの複雑さは重要ですが、測定が難しい指標です。 PMDは、Code Size Rules sectionの下に一連の堅固なルールを提供します。これらのルールは、メソッドのサイズと構造の複雑さに関する違反を検出するように設計されています。

CheckStyleは、コーディング標準およびフォーマット規則に対してコードを分析する機能で知られています。 ただし、複雑さmetricsを計算することで、クラス/メソッド設計の問題を検出することもできます。

両方のツールで最も重要な複雑度測定の1つは、CC(Cyclomatic Complexity)です。

CC値は、プログラムの独立した実行パスの数を測定することで計算できます。

たとえば、次のメソッドは3の循環的複雑度をもたらします。

public void callInsurance(Vehicle vehicle) {
    if (vehicle.isValid()) {
        if (vehicle instanceof Car) {
            callCarInsurance();
        } else {
            delegateInsurance();
        }
    }
}

CCは、条件ステートメントとマルチパートブール式のネストを考慮します。

一般的に、CCの観点から11より大きい値を持つコードは、非常に複雑で、テストと保守が難しいと考えられています。

静的分析ツールで使用されるいくつかの一般的な値を以下に示します。

  • 1-4:複雑性が低い–テストが簡単

  • 5-7:中程度の複雑さ–許容範囲

  • 8-10:高度な複雑さ–テストを容易にするためにリファクタリングを検討する必要があります

  • 11 +非常に高い複雑さ-テストが非常に難しい

複雑さのレベルは、コードのテスト容易性the higher the CC, the higher the difficulty to implement pertinent testsにも影響します。 実際、循環的複雑度の値は、100%の分岐カバレッジスコアを達成するために必要なテストケースの数を正確に示しています。

callInsurance()メソッドに関連するフローグラフは次のとおりです。

flowgraph_cc-1

可能な実行パスは次のとおりです。

  • 0⇒3

  • 0⇒1⇒3

  • 0⇒2⇒3

数学的に言えば、CCは次の簡単な式を使用して計算できます。

CC = E - N + 2P
  • E:エッジの総数

  • N:ノードの総数

  • P:出口点の総数

2.2. 循環的複雑度を減らす方法は?

実質的にそれほど複雑でないコードを記述するために、開発者は状況に応じて異なるアプローチを使用する傾向があります。

  • デザインパターンを使用して、長いswitchステートメントを記述しないでください。 ビルダーと戦略パターンは、コードサイズと複雑さの問題に対処するのに適した候補です。

  • コード構造をモジュール化し、Single Responsibility Principleを実装することにより、再利用可能で拡張可能なメソッドを記述します

  • Following other PMD code size rules may have a direct impact on CC、例: 過度のメソッド長ルール、単一クラスのフィールドが多すぎる、単一メソッドの過剰なパラメータリストなど

コードサイズと複雑さに関する次の原則とパターンを検討することもできます。 KISS (Keep It Simple and Stupid) principle、およびDRY (Don’t Repeat Yourself).

3. 例外処理ルール

例外に関連する欠陥は通常ありますが、それらのいくつかは非常に過小評価されており、本番コードでの重大な機能不全を回避するために修正する必要があります。

PMDとFindBugsは、例外に関する少数のルールセットを提供します。 例外を処理するときにJavaプログラムで重要と見なされる可能性のあるものを以下に示します。

3.1. 最後に例外をスローしないでください

ご存知かもしれませんが、Javaのfinally\{}ブロックは通常、ファイルを閉じたりリソースを解放したりするために使用されます。他の目的で使用すると、code smellと見なされる場合があります。

典型的なエラーが発生しやすいルーチンは、finally\{}ブロック内で例外をスローします。

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    throw new IOException();
}

このメソッドはNullPointerExceptionをスローすることになっていますが、驚くべきことにIOExceptionをスローするため、呼び出し元のメソッドが間違った例外を処理する可能性があります。

3.2. finallyブロックに戻る

finally\{}ブロック内でreturnステートメントを使用することは、混乱を招くだけかもしれません。 このルールが非常に重要である理由は、コードが例外をスローするたびに、returnステートメントによって破棄されるためです。

たとえば、次のコードはエラーなしで実行されます。

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    return;
}

NullPointerExceptionはまだキャッチされていませんが、finallyブロックのreturnステートメントによって破棄されています。

3.3. 例外でストリームを閉じられない

ストリームを閉じることがfinallyブロックを使用する主な理由の1つですが、それは簡単な作業ではないようです。

次のコードは、finallyブロック内の2つのストリームを閉じようとします。

OutputStream outStream = null;
OutputStream outStream2 = null;
try {
    outStream = new FileOutputStream("test1.txt");
    outStream2  = new FileOutputStream("test2.txt");
    outStream.write(bytes);
    outStream2.write(bytes);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        outStream.close();
        outStream2.close();
    } catch (IOException e) {
        // Handling IOException
    }
}

outStream.close()命令がIOExceptionをスローした場合、outStream2.close()はスキップされます。

簡単な解決策は、別のtry / catchブロックを使用して2番目のストリームを閉じることです。

finally {
    try {
        outStream.close();
    } catch (IOException e) {
        // Handling IOException
    }
    try {
        outStream2.close();
    } catch (IOException e) {
        // Handling IOException
    }
}

連続するtry/catchブロックを回避するための優れた方法が必要な場合は、Apache CommonsのIOUtils.closeQuietyメソッドを確認してください。これにより、IOExceptionをスローせずに閉じるストリームを簡単に処理できます。

5. 悪い習慣

5.1. クラスはCompareto()を定義し、Object.Equals()を使用します

compareTo()メソッドを実装するときはいつでも、equals()メソッドで同じことを行うことを忘れないでください。そうしないと、このコードによって返される結果が混乱する可能性があります。

Car car = new Car();
Car car2 = new Car();
if(car.equals(car2)) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}
if(car.compareTo(car2) == 0) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}

結果:

They're not equal
They're equal

混乱を解消するために、代わりにComparable,を実装するときにObject.equals()が呼び出されないようにすることをお勧めします。代わりに、次のようなものでオーバーライドしてみてください。

boolean equals(Object o) {
    return compareTo(o) == 0;
}

5.2. ヌルポインタの逆参照の可能性

NullPointerException(NPE)は、Javaプログラミングで最も遭遇するExceptionと見なされ、FindBugsは、NullPointeDの間接参照をスローしないように文句を言います。

NPEをスローする最も基本的な例は次のとおりです。

Car car = null;
car.doSomething();

NPEを回避する最も簡単な方法は、nullチェックを実行することです。

Car car = null;
if (car != null) {
    car.doSomething();
}

NULLチェックはNPEを回避できますが、広範囲に使用すると、コードの可読性に確実に影響します。

したがって、ヌルチェックなしでNPEを回避するために使用されるいくつかのテクニックを次に示します。

6. 結論

この記事では、静的分析ツールによって検出されたいくつかの重大な欠陥について、検出された問題に適切に対処するための基本的なガイドラインとともに、全体的な検討を行いました。

次のリンクにアクセスすると、各ルールの完全なセットを参照できます:FindBugsPMD