雑記 |
Visual Basic 中学校 > 雑記 >
2023/1/15
目次
この記事ではサイクロマティック複雑度の指標を利用して、プログラムをシンプルで健全な状態に保つ方法を説明します。
サイクロマティック複雑度はコードの複雑性を表す指標の1つです。
この指標を定期的に確認して、悪くならないように注意することで、品質に優れたシステムを構築することができます。サイクロマティック複雑度の考え方はプログラミング言語に依存するものではなく、C#、VB、VBA, Python、JavaScript, C, C++, COBOL, Rust などさまざまな言語で通用します。
この記事では、下記の点について説明しています。
サイクロマティック複雑度を下げるプログラムの具体的な方法については、別の記事で説明しています。
サイクロマティック複雑度算出の一例として、Visual Studioを使う方法は下記を参照して下さい。
この記事のサイクロマティック複雑に管理する理論的な部分は、下記を基にしています。
Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metric
サイクロマティック複雑度の一般的な計算方法はツールを使うことです。
たとえば、Visual Studioではクリック2回程度でサイクロマティック複雑度を算出できます。Visual Studio Codeでは拡張機能のCodeMetricsを導入することでサイクロマティック複雑度を表示できます。他にもSonarQubeなどの静的コード分析ツールの多くはサイクロマティック複雑度を算出できます。
ツールの使用方法はこの記事ではふれません。
参考:Visual Studio を使用してサイクロマティック複雑度を算出する手順はこちらで公開しています。 → コード メトリックスを算出する
サイクロマティック複雑度は、最低限テストすべき実行パス(フロー)の数を表しています。
C#のプログラムを例に簡単に説明します。このプログラムは、患者に投与する薬の錠剤数を算出します。基本的に2錠投与しますが、体重が50Kg以上の場合、1錠追加ます。年齢が12歳以下の場合は、1錠減らします。
public static int CountMedicine(int weight, int age)
{
int number = 2;
if (weight >= 50)
{
number += 1;
}
if (age <= 12)
{
number -= 1;
}
return number;
}
このプログラムはグラフ図で模式的に次の通り表すことができます。
このメソッドのサイクロマティック複雑度は 3 です。これは、このメソッドをテストするには最低限3つの実行パスをテストする必要があるということを意味します。
このメソッドの場合、その3つの実行パスは次の通りです。
このパスの数の算出方法は完全に数学的な理論に基づいており代数的な計算で算出可能です。
テストすべきパスが多いプログラムはそれだけ複雑なのであるから、複雑性の指標としても有用というわけです。
このようにサイクロマティック複雑度はホワイトボックステストを主眼とした指標なので、テストケースを作成する際にも役に立つかもしれませんが、現在(2023年)はテストケースの考え方としてはC0と呼ばれる命令網羅、C1と呼ばれる分岐網羅、C2と呼ばれる条件網羅等の方が主流となっており、サイクロマティック複雑度を基にテストケースを作成したという話は、私がかかわったシステムでは聞いたことがありません。サイクロマティック複雑度はもっぱら、プログラムの複雑性を表す指標として活用されているのが現状だと思います。
※ここで説明するパスの計算は、サイクロマティック複雑度の理解を深めますが、この記事を理解するには必須というわけではありませんから、忙しい人は読み飛ばしていただいてOKです。
サイクロマティック複雑度の根拠となる上述の3つの実行パスを「基本的な実行パス」と呼びます。
メソッドには、基本的ではない実行パスも存在します。たとえば、このメソッドには次のような実行パスもありえます。これをパスXとします。
基本的でない実行パスは、基本的な実行パスの組み合わせから算出可能です。
パスXがどのように算出されるか見てみましょう。
まず、各実行パスがノードを通過する回数を表にすると次の通りになります。
この図では、それぞれの処理を ○ で表しており、これを「ノード」と呼びます。ノードをつなぐ線を 「エッジ」と呼びます。
ノードを通過する回数 | ||||||
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
パス1 | 1 | 1 | 1 | 1 | ||
パス2 | 1 | 1 | 1 | 1 | 1 | |
パス3 | 1 | 1 | 1 | 1 | 1 | |
パスX | 1 | 1 | 1 | 1 | 1 | 1 |
パスX は パス2 + パス3 - パス1 で求められます。
ご覧の通りです。
ノードを通過する回数 | ||||||
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
パス2 + パス3 | 2 | 2 | 1 | 2 | 1 | 2 |
パス2 + パス3 - パス1 | 1 | 1 | 1 | 1 | 1 | 1 |
では、パスXには意味がないのか、と言えば、もちろんそんなことはありませんが、サイクロマティック複雑度の数値には影響を与えません。
ところで、パス1 も パス2 + パス3 - パスX で表されるのであるから、基本的なパスは、パス2,パス3,パスX の3つではないか、と考えるのは何か間違いでしょうか?
結論から言うと間違いではありません。サイクロマティック複雑度の値が表すのは基本的なパスの数であって、どの3つが基本的なパスであるかは、恣意的に決定することができます。(この記事のように)複雑さだけを問題にしているのであれば、どの3つが基本的なパスでも関係ありません。とにかく基本的なパスが3つであるという数値が重要なのです。
このようなパスの計算方法なので、プログラム内にループがある場合でもループの回数は複雑度に影響を与えません。for や while にはループを続行するか条件を判断する機能があるので、通常はサイクロマティック複雑度を +1 しますが、ループの回数については2回ループしても、100回ループしても、それは他のパスから計算できるパスなので、サイクロマティック複雑度は同じです。
おまけ
グラフ図を書いたときに囲まれた領域の数は、サイクロマティック複雑度に一致します。この例では3つの領域があるので、サイクロマティック複雑度は3です。
サイクロマティック複雑度を求める公式は エッジの数 - ノードの数 + 2 です。この例の場合、エッジは9個、ノードが8個なので、 9 - 8 + 2 で、このプログラムのサイクロマティック複雑度は 3 です。
サイクロマティック複雑度の評価を表にすると次のようになります。この表はMaCabeのレポートとMathWorksのレポートをまとめたものです。
McCabeのレポートの評価(※1) | MathWorksのレポートの評価(※2) | |||
---|---|---|---|---|
サイクロマティック 複雑度 |
複雑さの状態 | バグ 混入確率 |
複雑さの状態 | バグ 混入確率 |
1~10 | シンプルで、リスクはほとんどない。 | 5% | 非常に良い構造 | 25% |
11~20 | 少し複雑で、緩やかなリスクがある。 | |||
21~30 | 複雑で、高いリスクがある。 | 20% | ||
31~40 | 構造的なリスクあり | 40% | ||
31~50 | ||||
51~74 | テスト不可能。とても高いリスクがある。 | 40% | テスト不可能 | 70% |
75以上 | いかなる変更も誤修正を生む | 98% | ||
100 | 60% |
※1 Software Quality Metrics to Identify Risk
2つのレポートでは以上、以下の区切りが違うので、上記の表より値が1ずれている部分があります。100の部分は、原文では「Approaching 100」となっています。なお、サイクロマティック複雑度の上限はなく、100以上もあり得ます。
ロジックの最小限の記述単位(=多くの言語で「メソッド」や「関数」や「サブルーチン」と呼ばれるもの)の、サイクロマティック複雑度は 10以下を目標にします。
ただし、10を超えるものはすべてダメというわけではなく、品質を担保するためのテストができる状態であれば15程度までは許容されます。
これは、1996年に記述された Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metric に記載されている基準です。次のように記載されています。
これは私個人の実感とも一致します。経験の浅いプログラマーの多くはこの基準の達成は無理だと感じるでしょう。そして、それが無理な理由をいろいろ並べることができると思います。だいたいが一般論や(この記事のような)アカデミックな観点は、現場のプログラムや、今作っているシステムの前提や仕様とかけ離れており、自分が担当しているプログラムには当てはまらないという内容になります。
私の経験では、こういった理由のほとんどは経験と知識の不足からくる罠です。ただし、問題があるのが担当しているプログラマーだと決めつけるのは早計です。設計者やマネジメントに問題があるかもしれません。サイクロマティック複雑度をいかにマネジメントするかは後述します。またプログラムの観点でどうやってサイクロマティック複雑度をさげるかは次回説明します。
上述したように、サイクロマティック複雑度は、メソッドなど、プログラムのロジックの最小単位を主眼にした数字です。上の基準もメソッドレベルの基準値です。
Visual Studioでコードメトリックスを取得する、メソッドだけでなく、クラスやプロジェクトにもサイクロマティック複雑度が表示されますが、これは単にその配下のサイクロマティック複雑度を合計しているだけです。だから、クラスやプロジェクトのサイクロマティック複雑度が上の基準に照らして異様に高くても慌てる必要はありません。
とはいえ、クラスの中にたくさんのメソッドがあり、それらのメソッドがお互いにお互いを複雑に呼び出しあうような構造になっているのはやはりリスクですから、クラスやプロジェクトのサイクロマティック複雑度に意味がないわけではありません。しかし、その数値をどのように評価すべきかは私はわかりません。
また、大量のメソッドやを含むクラスや大量のクラスを含むプロジェクトはサイクロマティック複雑度とは別の観点で問題がある可能性もあります。
そこで、Visual Studioはサイクロマティック複雑度とは別に保守容易性指数という指標を表示するようになっています。この指標がグリーン(20~100)であればそのクラスやプロジェクト自体には大きな問題がないことを示します。(その配下のメソッドには問題がある可能性はあります)
保守容易性インデックスは、演算の複雑性やサイクロマティック複雑度、ソースコードの行数などから算出されます。
Visual Studioはメソッド単位でも保守容易性インデックスを算出します。
サイクロマティック複雑度がなぜリスクをもたらすのでしょうか。そして、そのリスクとはどのようなものでしょうか?
サイクロマティック複雑度には2つの側面があります。1つはテストケースの数を表すという側面です。もう1つは、文字通りプログラムの複雑さを表すという側面です。
サイクロマティック複雑度が、高いということは、それだけテストすべき実行パスが多いということです。命令網羅(C0)、分岐網羅(C1)、条件網羅(C2)などの考え方と、サイクロマティック複雑度は異なりますが、相関関係にあり、サイクロマティック複雑度が高ければ、C0,C1,C2のテストケース数も増えます。
私の感覚では(もちろん状況にって異なりますが単純化して表現すると) C0のテストケース数 < C1のテストケース数 < サイクロマティック複雑度 < C2のテストケース数 です。ただ、私はこれを自信を持って言えるほどではなく、茶飲み話程度の感覚です。
さて、サイクロマティック複雑度が50あるメソッドがあったとすると、それを完全にテストするためには50程度のテストケースが必要ということになります。これはプログラムの構造上のテストケースの数であり、データベースの内容により結果が変化するメソッドなどプログラム構造外にもテストすべき要素がある場合、かけ算でテストケース数は増えます。多くのプログラムでデータベースなど外部の要因を使用していることを加味すると、サイクロマティック複雑度の何倍もテストケースが必要になる例は珍しくありません。
プログラムの構造上のテストケース数だけ問題にするとしても、この「50」という数値は適切な50個のテストケースが必要という意味であり、あまり練られていないテストケース(つまり、Cxやサイクロマティック複雑度の理論通りに算出されていないテストケース)はものの数に入りません。となると、このメソッドが十分にテストされていると言い切る自信があるでしょうか?十分に時間をかければそのようなテストは不可能ではありませんが、どのくらい時間をかければよく、それはコストに見合うでしょうか?(そのコストをかけるかわりにサイクロマティック複雑度を下げるべきでは?)
それから、テストの自動化が行われていない開発チームで、このようなテストを抱えてしまった場合、テストをやり直すたびに毎回時間がとられることになります。このような状況ではそのメソッドのプログラムを1行でも修正しようと思ったら躊躇することになります。結果としてリファクタリングの機会や、問題発見の機会、パフォーマンス向上の機会を逃し、エンドユーザーへの機能提供のアジリティが低下します。
これがサイクロマティック複雑度が高いケースの直接的なリスクです。つまり、完全なテストが困難であるので、バグ混入率があがり、品質の悪いプログラムになるということです。
サイクロマティック複雑度が高いプログラムは分岐が多いということなので複雑です。1直線に命令を次から次へと実行していくプログラムは理解しやすくてシンプルです。
それに比べて分岐が多いプログラムは、どういうときに何が実行されるのか、ある条件の時に実行されるのはどの部分とどの部分か、など分岐に応じて複雑になっていきます。
その結果、プログラムを理解するのが困難になり、プログラムのミスの発見が難しくなります。また、あとから機能を修正するのも困難です。ある部分を修正した時に、他のどの部分に影響するかわかりにくいからです。
上掲したMathWorksのレポートでは、サイクロマティック複雑度75のメソッドは、いかなる変更でもバグを発生させるとされており、バグ混入率は98%と評価されています。そんな状態のシステムを運用・保守・開発したいと思う人などいないでしょう。これが複雑さのリスクです。テスト困難であるリスクと表裏一体です。
リーダー・マネージャーの立場の人は、サイクロマティック複雑度が高くならないように、チームを運用してください。
まず、チームの標準として、サイクロマティック複雑度の上限を設定しましょう。上限は前述したように10や15と言いたいところです。経験の浅いチームにはこの目標の達成は難しいので、事前にどうすればサイクロマティック複雑度が高いプログラムが作れるのか学習やレクチャーが必要です。この記事と次回公開する予定のサイクロマティック複雑度を下げるプログラムの記事はその助けになります。
また、上限は100%守るべきものではなく、何か理由があれば柔軟に緩和してもよいです。たとえば、サイクロマティック複雑度以外の他の良いコーディングパターンと競合する場合、サイクロマティック複雑度が常に優先されるわけではありません。
ただ、こういったことを言い訳にして、サイクロマティック複雑度を軽んじた結果、ひどいシステムが完成してしまうことがあるので、言い訳をうのみにせず判断は慎重に行ってほしいです。
サイクロマティック複雑度が優先させるべきかの判断は、そのプログラムが十分なテストケースでテストできるのかという観点で考えれば良いと思います。何かの理由でサイクロマティック複雑度は高くても、十分なテストが可能で、サイクロマティック複雑度を下げるようにプログラムを改修するより安価であることが示されるのであれば、サイクロマティック複雑度にこだわる必要はありません。(ただ、そのような例外的な判断をするメソッドは多くないので、罠にはまらないように注意してください。)
十分なテストが可能であるかメンバーに確認するには次のような質問を投げかけましょう。ここでの観点は、品質の担保が可能か、コストはどれほどか、人依存が前提になっていないかです。
「分岐が多いプログラムになっているようだけど、すべての分岐を網羅するテストケースを作成できるのか?そのようなテストケースを作成するのにどのくらい時間が必要か?」
「そのテストを実行するのにかかる時間はどのくらいか?機能修正やバグ改修をしたときにまたテストすることになるが、その都度その時間をかけられるか?」(テストが自動化されており手間がかからないことを期待しますが、そうでないケースも珍しくはないようです。)
「担当者が変わってもそのテストケースのテストの実施をスムーズに引き継げるようになっているか?」
「新しい担当者のもとで機能が変更された場合、テストケースも変更する必要があるが、新しい担当者はどのくらいの時間でそれができるか?」
ちなみに、ツールが自動生成する類のソースコードのサイクロマティック複雑度をどう評価するかは、そのソースコードの品質に誰が責任を持つのか、その自動生成ツールはどういうポリシーでソースコードを生成しているのかなどによって扱いを変える必要があると思います。(そのソースコードを誰がテストするのかしないのかと同じ問題です。)
次に、チームメンバーの誰でも簡単にサイクロマティック複雑度を確認できるようにしましょう。Visual Studioのような自動算出機能があるツールを使うことがベストです。おそらく現在(2023年)ほとんどの開発ルールにはそのような機能があるはずです。
個々のプログラマーは適宜サイクロマティック複雑度を確認できて、その数値を見ながら、すぐにプログラムを変更できるようにするべきです。 個々のプログラマーにサイクロマティック複雑度を確認する手段がなければ、いちいちリーダーやマネージャーなど確認方法がある人に頼らなければ現状が把握できず、時間がかかりすぎます。
リーダーやマネージャーは定期的に自身の目でソースコードのサイクロマティック複雑度を監視してください。手遅れにならないうちに兆候を発見しましょう。レビューなどのタイミングではじめてサイクロマティック複雑度を確認して、問題を指摘するようだと手戻りの工数が大きくなります。1日1回くらい能動的に確認するのが良いと思いますが、経験を積んだチームならもっと少なくてもよいでしょう。
GitHubなどのプラットフォームや、何かの静的コード解析ツールを使用しているならば、それらの機能で毎日自動的にサイクロマティック複雑度が算出されるようになっていると素晴らしいです。たとえば、テストが自動化されており毎日実行される仕組みになっているのであれば、そのついでにサイクロマティック複雑度も算出するようにしてみましょう。このようなうまい仕組みを作るには、リーダークラスのプログラマーが率先してソースコード管理のプラットフォームを構築すると良いでしょう。
もし、このような自動的に仕組みが構築できなくても、サイクロマティック複雑度の算出方法は難しくなく時間もかからないので、手動でツールのボタンをぽちぽちおして算出して様子を見てみるのも良いかもしれません
Visual Studio を使用してサイクロマティック複雑度を算出する手順はこちらで公開しています。 → コード メトリックスを算出する
開発中のプログラムでは日々サイクロマティック複雑度も変わっていくことでしょう。私なら20を超えるようなメソッドが出始めたら、そっと様子を伺います。開発中で、まだ整理されていない粗雑なプログラムがあるだけなら問題ありません。それなりに整理されて、後戻りに手間がかかりそうなフェーズ(たとえば、量産が開始される)であればいったんストップをかけて、サイクロマティック複雑度を下げるように担当者と相談します。どうやってサイクロマティック複雑度を下げるのかは次回公開予定の記事で説明します。
最後に、最近(2023年)は見ることが少なくなってきましたが、詳細設計時点でプログラムのロジック的な部分まで決めてしまう設計手法をとっている場合は考えどころです。こういう手法を採ると設計の時点でサイクロマティック複雑度がある程度決まってしまうので、ソースコードの開発時点でサイクロマティック複雑度を気にしても手遅れかもしれません。私はこの手法は好きではなく、サイクロマティック複雑度にしばりをかけるレベルの設計は設計工程では止めましょうと言いたいです。
もし、この手法をとるのであれば、サイクロマティック複雑度の運用はまるで違うものを考える必要があると思いますが、私には案なしです。長年ウォーターフォールで年単位のスパンで巨大システムの改修を行っているような開発チームにはそのような運用が確立しているかもしれません。そのままのやりかたを継続してもよいのであれば、サイクロマティック複雑度の役割はこの記事で説明しているものよりもずっと小さくなるでしょう。「そのままのやりかたを継続して」本当に良いのかの判断は難しいかもしれませんがそれは別の問題です。
プログラマーは、まず、自分のプログラムのサイクロマティック複雑度を頻繁に確認するようにしてください。
不慣れなプログラマーは1時間に1回程度(最低半日に1回くらい)は自分のプログラムのサイクロマティック複雑度を確認して、10を超えそうであれば、低くなるように修正や方向転換をしていきましょう。
具体的にどうすればよいかは後述します。
前述したように1つのメソッドではサイクロマティック複雑度を10以下にしてください。
慣れてくれば逐一確認しなくてもだいたいのところはわかるようになります。そうなれば確認頻度は落としても良いです。
Visual Studioのようなツールでは、サイクロマティック複雑度は、2クリック程度で数秒で算出されますので、ちょっと修正して、サイクロマティック複雑度がどのようの変化したかを確認していると、どのようにプログラムすればよいのかわかってきます。
参考:Visual Studio を使用してサイクロマティック複雑度を算出する手順はこちらで公開しています。 → コード メトリックスを算出する
1つのメソッドの中で条件分岐を減らしてサイクロマティック複雑度をさげてください。
サイクロマティック複雑度の上限が10であるので、単純に考えると、1つのメソッド内で許される条件分岐は10回です。
このための具体的なプログラム方法論は冒頭でも紹介しましたが、こちらの記事で説明しています。