サイクロマティック複雑度の下げ方・考え方~目指せ良いコード~

ソフトウェア開発

現役組み込みソフトエンジニアのです。

サイクロマティック複雑度の概要や目安、計測方法を前回の記事で述べてきました。

前回の記事ではサイクロマティック複雑度が高いこと=良くないことと述べてきましたが、開発現場のコードは必ずしも低い複雑度を維持できていません。そこで高い複雑度のコードについてどのように考えるべきか、また対策としてサイクロマティック複雑度を下げる方法をまとめてみました。



高サイクロマティック複雑度のリスクと問題

サイクロマティック複雑度が高いとどんなリスクがあるか、また発生する問題として以下のようなことが考えられます。

高い複雑度のコードの3つの特徴

まず複雑度の高いコードの特徴は以下のようなものがあります。

  • 条件分岐が多い
  • 1関数・メソッドの行数が多い (傾向がある)
  • 条件分岐が入れ子(ネスト)が深い (傾向がある)

このような特徴のある関数・メソッドが抱える具体的な問題点は2つ。可読性の低さと、テストケースの増加が考えられます。

高い複雑度の問題①:可読性の低さ

ひとつ目の問題はシンプルに読みにくいことです。なんだそんなことかと思われたでしょうか。

読みにくいコードは組織のソフト開発パフォーマンスを下げてしまいます。企業でのソフト開発はチームを組んでの開発が一般的です。チームで取り組む以上、各開発メンバーのパフォーマンスが高いチームの方がチームとしてのパフォーマンスも高くなります。
もし、チームの中で可読性の低いコードでの開発を許してしまうとどうなるでしょうか?あるメンバーが書いた読みにくいコードを他のメンバーが読むことになります。可読性が低いため当然、理解するには時間がかかります。チームの規模が大きいほど読み手も増えるため、そのコードの解読のために浪費される時間は多くなります。結果として「コードを理解するのに掛ける時間」がチーム全体で増えてしまい、思うようなパフォーマンスが出せず生産性を下げてしまう可能性があります。
また、組織開発ではメンバーの入れ替えも発生します。チーム間の人事異動や、新人採用などケースはいくつかありますが、読みにくく難解なコードばかりの現場ではこういった新規参入メンバーがチームの戦力として実力を発揮するまでに時間を要するでしょう。

高い複雑度の問題②:テストケースの増加

サイクロマティック複雑度の数値の大きさは、分岐網羅(コードカバレッジ)テストに必要なテストケース数と相関します。つまり高い複雑度の関数はテストケースを大量に作らなければならならず、結果的にテストが難しくなります。複雑度が著しく高い場合は、すべての分岐網羅テストケースを作成すること自体が現実的ではないこともしばしばあります。
他にも関数・メソッドのテストでは特定の入力値を与えた場合に期待する出力を得られるかどうかをテストする入出力テスト等も実施すべきです。内部での分岐パターンが複雑になるにつれて入力に対する出力期待値の組み合わせが多くなるため、高い複雑度の関数・メソッドではテストが困難になります。

高いサイクロマティック複雑度への対策

対策:サイクロマティック複雑度を下げる(リファクタリング)

サイクロマティック複雑度を含むメトリクスの悪化は、その実装方法を再検討するべきであるという事実を示しています。サイクロマティック複雑度が高い場合、素直にリファクタリング(再設計)しましょう。具体的なリファクタリング手段としては関数を分割して1関数当たりの条件分岐を減らしたり、不要な分岐を消す等の方法が考えられます。
実際に簡単なリファクタリング例を挙げてみます。まずはリファクタリング前の関数がこちら↓↓

void function()
// 関数定義 サイクロマティック複雑度 +1
{
    switch(変数名)
    // switch サイクロマティック複雑度増加無し +0
    {
        case 値1:
        // case サイクロマティック複雑度 +1
            if(条件式1)
            // if サイクロマティック複雑度 +1
            {
                処理 ;
            }
            else if(条件式2)
            // else if サイクロマティック複雑度 +1
            {
                処理 ;
            }
        break;

        case 値2:
        // case サイクロマティック複雑度 +1
            if(条件式3)
            // if サイクロマティック複雑度 +1
            {
                処理 ;
            }
            else if(条件式4)
            // else if サイクロマティック複雑度 +1
            {
                処理 ;
            }
        break;

        default:
        // defaultではサイクロマティック複雑度増加無し +0
        処理 ;
        break;
    }
}

リファクタリング前のfunction()の複雑度は7です。複雑度としては深刻になるほど高くはありませんが、リファクタリングして1関数当たりのサイクロマティック複雑度を下げてみましょう。

void function()
// 関数定義 サイクロマティック複雑度 +1
{
    switch(変数名)
    // switch サイクロマティック複雑度増加無し +0
    {
        case 値1:
        // case サイクロマティック複雑度 +1
            calledFunction1();
        break;

        case 値2:
        // case サイクロマティック複雑度 +1
            calledFunction2();
        break;

        default:
        // defaultではサイクロマティック複雑度増加無し +0
        処理 ;
        break;
    }
}

/* case1の処理を関数化 */
void calledFunction1()
// 関数定義 サイクロマティック複雑度 +1
{
    処理;
    if(条件式1)
    // if サイクロマティック複雑度 +1
    {
        処理 ;
    }
    else if(条件式2)
    // else if サイクロマティック複雑度 +1
    {
        処理 ;
    }
}

/* case2の処理を関数化 */
void calledFunction2()
// 関数定義 サイクロマティック複雑度 +1
{
    処理;
    if(条件式3)
    // if サイクロマティック複雑度 +1
    {
        処理 ;
    }
    else if(条件式4)
    // else if サイクロマティック複雑度 +1
    {
        処理 ;
    }
}

リファクタリング前のswitch-case文の各case処理を関数化することで1関数での処理を3つの関数で処理するように変更しました。結果、リファクタリング後では各関数の複雑度は3になり、1関数あたりのサイクロマティック複雑度を下げることができました。このように1つの関数を分割する方法はサイクロマティック複雑度の低減に有効な方法の一つです。

対策:より注意深く扱う

高い複雑度の設計を完全に無くすことは困難です。リファクタリングすること自体が難しく複雑度を下げることができない、ということも現場では多々あります。そういった高いサイクロマティック複雑度でかつ複雑度の低減が難しい関数・メソッドはより一層注意深く扱う必要があります。例えば、以下のような扱い方を義務付けると一定の効果を得られるのではないでしょうか。

設計書やコメント文を充実させる

コードを読まなくても理解できるように自然言語の設計書やコメント文を充実させれば可読性の低さを補うことができます。

テストパターンを確実に更新し回帰テストを駆使する

設計変更の積み重ねによって複雑になっていく関数に対してテストを網羅できるように、コードの更新に合わせて必ずテストパターンの更新も行い、過去の設計を損なっていないかを回帰テストで振り返ると良いでしょう。

他プロジェクトへの転用を禁止する

「あまりに複雑度が高くこれ以上の設計変更は不可能!でも動く!」という社内あるあるの設計については、それ以上使いまわすことをあきらめて、どこかで捨てる覚悟をしましょう。使い続けることの方が危険です。

まとめ

この記事ではサイクロマティック複雑度が高い設計の問題点と、オーソドックスな対策例を紹介しました。大規模な開発現場になればなるほど、高い複雑度の設計が一定の確率で必ず生じます。しかし、高い複雑度の関数はその運用において何かしらのサポートを義務付けるなど、低い複雑度の関数よりも注意深く扱う構えを作ることが重要です。

参考書籍

コメント

タイトルとURLをコピーしました