1. はじめに
私達の世界にはクソコードという言葉があり、品質の低いコードを貶めるワードとして日々親しまれています。しかし、私達は一体どのようにクソコードを見分けているのでしょうか。その一つの指標として、ソフトウェアメトリックと呼ばれるコードの品質を測る方法論があります。
クソコードの定義は人により様々ですが、ここでは「複雑性の高いコード」と定義します。最も美しいコードはゼロ行のコードという極論が示唆するように、コードは可能な限り簡潔であるべきです。その理由として、コードの複雑性は可読性を下げ、再利用性を下げ、凝集度を下げ、拡張性を下げ、バグの温床になることが挙げられます。
本記事ではソフトウェアメトリックについて概説した後、その中でも汎用的なABC Metricという手法を実例を交えて紹介し、どのようにクソコードとの戦いに活かすか検討します。
2. ソフトウェアメトリックとは何か
広義のソフトウェアメトリックは、ソフトウェア開発にまつわる様々なプロセスを定量評価できるあらゆる指標を指します。粒度が小さい順に例を挙げると、関数の行数、テストカバレッジ、応答速度、CPU使用率などがあります。ほか、ソフトウェア開発において定量評価できるものはソフトウェアメトリックと呼んで差し支えないでしょう。
表題であるクソコードと戦うためには、ソフトウェアメトリックの中でも、コードの複雑性を評価できる必要があります。そのような手法として、コード行数、FP、LCOMなど様々なものが発表されていますが、ここでは汎用性と実効性のバランスに優れたABC Metricと呼ばれる手法を取り上げます。
(LCOMについては下の記事で解説しています)
3. ABC Metricとは何か
C++ Report誌上で1997年に発表された、コードの複雑性を測る指標です1。コードの行数よりも実用的な指標として考案されました。
ABC Metricは、簡単に言えばコード内の命令の個数です。命令を代入(Assignemnt)、分岐(Branch)、条件(Conditional)の3種類に分けカウントします。
それら3種類の命令の個数をそれぞれ$A, B, C$とすれば、ABC Metricは次式のベクトルとして表すことができます。
\overrightarrow{ABC} = (A, B, C)
通常はこのベクトルの絶対値を最終的なABC Metricの値として用います。
\rm{ABC} = |\overrightarrow{ABC}| = \sqrt{A^2 + B^2 + C^2}
$\rm{ABC}$の値が大きいほど、複雑性の高いコードと見做します。
実際に各$A, B, C$をどのように計算するかは、言語毎にルールを定める必要があります。原文ではC言語、C++言語、Java言語に対してどのようなルールを設定するべきか示しており、それらを考慮すると次のような共通ルールを抽出できます。
| 命令の種類 | ルール | トークン例 |
|---|---|---|
| $A$ | 定数定義を除く代入演算 | = *= /= %= += &= |
| $A$ | インクリメント/デクリメント | ++ -- |
| $B$ | 関数呼び出し | func() |
| $B$ | new/delete | new Class delete hoge |
| $C$ | 比較演算 | == != <= >= < > |
| $C$ | 条件キーワード | try else case default ? |
| $C$ | 単項条件式 | if (a) という式の a の部分 |
このルールに従い、実際のコードについて計算してみましょう。
4. 計算例
ここではRubyとTypeScriptで書かれたコードを例に計算します。
4-1. Rubyでの計算例
# rails/activerecord/lib/arel/table.rb (一部改変)
def initialize(name, as: nil, klass: nil, type_caster: klass&.type_caster)
@name = name.to_s # AB
@klass = klass # A
@type_caster = type_caster # A
if as.to_s == @name # BC
as = nil # A
end
@table_alias = as # A
end
したがって、ABC Metricの値は次のようになります。
\overrightarrow{ABC} = (5, 2, 1) \\
\rm{ABC} = |\overrightarrow{ABC}| = \sqrt{5^2 + 2^2 + 1^2} = 5.48
4-2. TypeScriptでの計算例
material-uiというレポジトリから、deepmergeを行う関数を引用し計算します。
// material-ui/packages/material-ui-utils/src/deepmerge.ts (一部改変)
export default function deepmerge<T>(
target: T,
source: unknown,
options: DeepmergeOptions = { clone: true },
): T {
const output = options.clone ? { ...target } : target; // ACC (三項演算子はif-elseと解釈)
if (isPlainObject(target) && isPlainObject(source)) { // BB
Object.keys(source).forEach((key) => { // BB
// Avoid prototype pollution
if (key === '__proto__') { // C
return;
}
if (isPlainObject(source[key]) && // B
key in target && // B
isPlainObject(target[key]) // B
) {
(output as Record<keyof any, unknown>)[key] = // A
deepmerge(target[key], source[key], options); // B
} else { // C
(output as Record<keyof any, unknown>)[key] = source[key]; // A
}
});
}
return output;
}
したがって、ABC Metricの値は次のようになります。
\overrightarrow{ABC} = (3, 8, 4) \\
\rm{ABC} = |\overrightarrow{ABC}| = \sqrt{3^2 + 8^2 + 4^2} = 9.43
このようにABC Metricはスカラ値として算出できるため、事前に閾値を定めておき、それを超えたら警告を出すという運用がなされます。
5. なぜABC Metricなのか
コードの複雑性を測る指標として、ABC Metircの持つ利点は次の3つです。
5-1. 言語非依存
上の例で見てきたように、ABC Metricは特定の言語に依存しません。例えばコードの複雑性を測る指標としてFlogがありますが、これはRuby言語に特化しています。また凝集度を判定するLCOMという指標がありますが、これはJavaやSwiftのようなクラスモジュールを基本とする言語でなければ有効に機能せず、TypeScriptやGoのような関数ベースの言語には適用できないものです。
その点、ABC Metricは代入、分岐、条件といった、実用的な言語であれば必ず備えている要素を評価対象としているため、あらゆる言語に適用することができます。
5-2. 説明の容易さ
ABC Metricは命令をカウントするだけという非常に分かりやすいアイデアであることは勿論ですが、命令を代入、分岐、条件に分けていることも重要です。ABC Metricのベクトル表現を見れば、代入、分岐、条件のいずれに比重が偏っているかが分かります。そのためプログラマはどの命令を減らすべきか容易に分かります。
5-3. 柔軟性
本記事ではABC Metricをスカラ値として表す際、単に絶対値を計算しましたが、次式のように重みベクトル$W$を設定することも可能です。私も実際に試したことはないですし、原著者も可能性について言及しているだけですが、面白いアイデアだと思います。
\overrightarrow{ABC} = (A, B, C) \\
\overrightarrow{W} = (W_A, W_B, W_C)
\\
\rm{ABC} = |\overrightarrow{ABC} \,\cdot\, \overrightarrow{W}| = \sqrt{(AW_A)^2 + (AW_B)^2 + (AW_C)^2}
例えばGo言語ではエラーハンドリングの都合上、A(代入)とC(条件)のカウントが増えるため、これらを減じる重みを設定するという使い方が可能です。
6. で、実際にどう導入するの?
ここまで丁寧にABC Metricについて見てきましたが、残念ながら主要なLinterの中でABC Metricのルールを備えているものはRubyのRuboCopしか知りません(他に知っていたら教えてください)。
Rubyistの方は引き続きRuboCopの恩恵に預かりましょう。他の言語の方はLinterのルールプラグインをぜひ作ってください(他力本願)。
7. おわりに
コードの複雑性を定量評価できると何が嬉しいのかというと、自動化が可能になるからです。クソコードを避けるためには、SOLID原則をはじめとする経験則や、DDDに代表されるアーキテクチャパターンについて知っており、さらにある程度の実装経験を積んでいることが求められます。そのためクソコードを避けるためには膨大な時間がかかり、さらにコードレビューやモブプログラミングなどを通じてベテランから知識を引き出すことも必要になります。
定量評価のメリットは、そのようにベテランの手を借りることなく、クソコードに対して機械的に警告を受けられることにあります。警告の種類は限定的ではありますが、それでもゼロコストでコードの複雑性を減らしてくれるツールを利用しない手はないように思えます。
ABC Metricが考案されたのは20年以上前ですが、そのシンプルなアイデアは現代のプログラミングでも十分に活用でき、コードの複雑性を測る指標として再考されるべきものの一つだと言えます。
Twitter:リーダブル秋山@aki202
-
原文では "software size" を測る指標と言っていますが、ここでは簡単のために複雑性の指標と読み替えます。 ↩
