RubyアプリケーションのABCSizeの目安

Posted by Kumojima Kenta on February 23, 2016 · 5 mins read

チームで開発する際はコーディング規約を決めて行うことが多いと思います。
実際に書いたコードが規約に従っているかどうかを検査するため、筆者はRuboCopを使用しています。
コーディング規約そのものはWeb上に公開されているものを参考にして決めていますが、制限が厳しすぎる等の意見がチーム内であった場合は適宜規約を調整しています。
最近チーム内でABCSizeに基づく警告を出す閾値がもう少し高くても良いのではという意見が出たことをきっかけにいくつかのアプリケーションでの例を調べたので紹介します。

ABCSizeについて

ABCSize(ABCMetrics)は次のように定義されます。(ABC Metricより)

Assignment -- an explicit transfer of data into a variable, e.g. = = /= %= += <<= >>= &= |= ^= >>>= ++ --
Branch -- an explicit forward program branch out of scope -- a function call, class method call, or new operator
Condition -- a logical/Boolean test, == != <= >= < > else case default try catch ? and unary conditionals.
A scalar ABC size value (or "aggregate magnitude") is computed as:
|ABC| = sqrt((A
A)+(BB)+(CC))

代入(A)、メソッド呼び出し等(B)、条件式(C)の数を数え、それぞれの2乗の和の平方根がABCSizeになります。

一般に一つのメソッドが複雑になりすぎると保守性が下がるため、メソッドを分割するための指標が必要になります。
RuboCopではメソッドごとにこのABCSizeを集計し、デフォルトで15を超えると警告を出すようになっています。

直感的には値が大きいほど複雑であることは分かりますが、どれくらいの値が適切であるかまでは分かりません。

各プロジェクトでの分布

設定値の目安にするため、いくつかのプロジェクトのソースコードにおけるメソッドごとのABCSizeの分布を調べました。
ある程度の開発期間・コード規模をもつプロジェクトとして次のリポジトリを対象とします。

これらのリポジトリ上のソースコード中のメソッドごとのABCSizeの分布は次のようになります。
横軸がABCSize、縦軸がメソッド数を表します。
(簡単のためABCSizeは小数第1位を四捨五入しています。)

Rails

data.addRows([
  [1, 3061],[2, 3478],[3, 2658],[4, 2620],[5, 1687],[6, 1440],[7, 1188],[8, 962],[9, 810],[10, 679],[11, 510],[12, 502],[13, 306],[14, 326],[15, 274],[16, 263],[17, 165],[18, 186],[19, 122],[20, 108],[21, 108],[22, 104],[23, 73],[24, 72],[25, 65],[26, 36],[27, 37],[28, 50],[29, 28],[30, 26],[31, 26],[32, 32],[33, 16],[34, 14],[35, 14],[36, 15],[37, 17],[38, 15],[39, 15],[40, 19],[41, 12],[42, 6],[43, 6],[44, 7],[45, 5],[46, 4],[47, 2],[48, 3],[49, 1],[50, 4]
]);

var chart = new google.visualization.ColumnChart(document.getElementById('rails_chart'));
chart.draw(data, options);

}
google.charts.setOnLoadCallback(drawRailsChart);

Rubocop

data.addRows([
  [1, 221],[2, 293],[3, 215],[4, 217],[5, 197],[6, 156],[7, 129],[8, 86],[9, 94],[10, 84],[11, 75],[12, 54],[13, 53],[14, 47],[15, 26],[16, 39],[17, 31],[18, 27],[19, 14],[20, 19],[21, 9],[22, 17],[23, 3],[24, 14],[25, 8],[26, 2],[27, 9],[28, 8],[29, 3],[30, 3],[31, 1]
]);

var chart = new google.visualization.ColumnChart(document.getElementById('rubocop_chart'));
chart.draw(data, options);

}
google.charts.setOnLoadCallback(drawRubocopChart);

rubygems.org

data.addRows([
  [1, 115],[2, 99],[3, 81],[4, 42],[5, 32],[6, 37],[7, 24],[8, 16],[9, 15],[10, 12],[11, 13],[12, 6],[13, 6],[14, 3],[15, 3],[16, 4],[17, 4],[18, 5],[19, 3],[20, 1],[21, 2],[22, 2],[23, 2],[24, 0],[25, 2],[26, 4],[27, 2],[28, 0],[29, 2],[30, 2],[31, 0],[32, 1],[33, 0],[34, 0],[35, 0],[36, 0],[37, 1],[38, 1],[39, 0],[40, 1]
]);

var chart = new google.visualization.ColumnChart(document.getElementById('rubygems_org_chart'));
chart.draw(data, options);

};

google.charts.setOnLoadCallback(drawRubyGemsOrgChart);

Gitlab

data.addRows([
  [1, 684],[2, 822],[3, 626],[4, 556],[5, 387],[6, 295],[7, 207],[8, 186],[9, 154],[10, 124],[11, 85],[12, 85],[13, 59],[14, 55],[15, 54],[16, 41],[17, 39],[18, 21],[19, 30],[20, 24],[21, 15],[22, 15],[23, 17],[24, 14],[25, 15],[26, 13],[27, 7],[28, 9],[29, 7],[30, 6],[31, 8],[32, 8],[33, 5],[34, 4],[35, 3],[36, 3],[37, 2],[38, 2],[39, 2],[40, 1],[41, 1],[42, 1],[43, 2],[44, 2],[45, 2],[46, 3],[47, 0],[48, 2],[49, 2],[50, 1]
]);

var chart = new google.visualization.ColumnChart(document.getElementById('gitlab_chart'));
chart.draw(data, options);

};
google.charts.setOnLoadCallback(drawGitlabChart);

Redmine

data.addRows([
  [1, 780],[2, 715],[3, 566],[4, 668],[5, 557],[6, 543],[7, 454],[8, 400],[9, 412],[10, 289],[11, 267],[12, 276],[13, 217],[14, 167],[15, 149],[16, 148],[17, 112],[18, 123],[19, 93],[20, 88],[21, 58],[22, 49],[23, 49],[24, 43],[25, 29],[26, 46],[27, 35],[28, 23],[29, 34],[30, 20],[31, 26],[32, 19],[33, 28],[34, 17],[35, 15],[36, 16],[37, 21],[38, 5],[39, 8],[40, 13],[41, 9],[42, 16],[43, 3],[44, 7],[45, 5],[46, 4],[47, 2],[48, 5],[49, 8],[50, 7]
]);

var chart = new google.visualization.ColumnChart(document.getElementById('redmine_chart'));
chart.draw(data, options);

}
google.charts.setOnLoadCallback(drawRedmineChart);

spree

data.addRows([
  [1, 363],[2, 303],[3, 282],[4, 250],[5, 186],[6, 152],[7, 103],[8, 89],[9, 85],[10, 70],[11, 44],[12, 44],[13, 33],[14, 39],[15, 20],[16, 33],[17, 16],[18, 16],[19, 20],[20, 16],[21, 6],[22, 7],[23, 8],[24, 4],[25, 7],[26, 3],[27, 4],[28, 3],[29, 2],[30, 5],[31, 4],[32, 1],[33, 0],[34, 1],[35, 2],[36, 1],[37, 3],[38, 2],[39, 1],[40, 0],[41, 1],[42, 0],[43, 0],[44, 1],[45, 2],[46, 0],[47, 0],[48, 1],[49, 0],[50, 1]
]);

var chart = new google.visualization.ColumnChart(document.getElementById('spree_chart'));
chart.draw(data, options);

}
google.charts.setOnLoadCallback(drawSpreeChart);

ABCSizeの設定値はどうすべきか

ABCSizeの分布をみるとどれも0〜20あたりにほとんどのメソッドが集中していることが読み取れます。

もう少し詳しく、ABCSizeの値に対してメソッドのカバー率を表す累積分布グラフが次のようになります。
横軸がABCSizeの値、縦軸がそのアプリケーションにおけるそのABCSize以下のメソッドの割合です。

var chart = new google.visualization.AreaChart(document.getElementById('cumulative_distribution_chart'));
chart.draw(data, {
  width: 800,
  height: 400,
  title: '累積分布',
  hAxis: { title: 'ABCSize',  titleTextStyle: { color: '#333' } },
  vAxis: { minValue: 0 }
});

}
google.charts.setOnLoadCallback(drawCumulativeDistributionChart);

累積分布のグラフからキリのいい割合におけるABCSizeを抜き出したのが次の表です。

アプリ 70% 80% 90% 100%
RuboCop 8 11 15 31
Rails 7 9 14 400
rubygems.org 6 8 12 40
Gitlab 6 9 13 243
Redmine 11 14 20 334
spree 7 9 14 364

過半数のプロジェクトで100%をカバーするには3桁の数字を設定しなければならず非現実的ですが、12〜15に設定すれば6個中5つのプロジェクトで90%をカバーできるため、このあたりが妥当な設定値と言えるのではないでしょうか。
ということで、もともとRubocopがデフォルトに設定している15という設定値が妥当であると結論付けて、特に値を変えずに使用しています。