分散システムにおける適度な結合とは - Viadik Khononov氏のDDD Europeでの講演より を読んで、ソースコードの結合度を測るコナーセンスという概念を初めて知った。コナーセンスは日本語で検索しても情報がほとんどない。そこでコナーセンスの理解を深めるために英語サイト connascence.io を翻訳した。
翻訳サイトはこちら。元サイトが work in progress なのでコンテンツの分量は少ない。
この記事では上の翻訳サイトと Connascence - Wikipedia をもとにコナーセンスの概要を説明する。
コナーセンスとは
コナーセンスはソフトウェアの品質を測る結合度のメトリクスであり、Meilir Page-Jones により提唱された。初めて本格的に解説されたのは 1996 年の著作『What every programmer should know about object-oriented design』 においてである。歴史はわりと古い。
コナーセンスは変更容易性に着目した結合度の測り方である。その定義を簡単に言うと、コードベースにある2つのコンポーネント(関数やクラスやモジュールなど)にコナーセンスがあるとは、ソフトウェアを壊さずに変更を加えるためには一方を修正すると他方も修正しなければならない状態である。これは2つのコンポーネントが依存関係にあることを示しているが、コナーセンスはさらに踏み込んで依存関係をレベルで分類している。
コナーセンスには強度(Strength)、程度(Degree)、局所性(Locality)という3つの軸がある。まず強度から見ていこう。
コナーセンスの強度
コナーセンスにはいくつかのタイプがあり、強いものと弱いものがある。強いコナーセンスというのは、修正が難しかったりそもそもコナーセンスの発見が難しかったりして、変更コストが大きいものをいう。一般的には強いコナーセンスがあると結合度が大きくなるため、同じモジュール内などに押し込めて結合度を下げると良い。弱いコナーセンスはソフトウェアに必ず現れ、許容できるものである。
最も弱いコナーセンスは「名前のコナーセンス」と呼ばれる。これは変数名、関数名、メソッド名、クラス名、モジュール名等、名前による参照のことである。たとえば関数名は、コードベース内で関数を宣言する側と呼び出す側の少なくとも2つの箇所で使われる。このとき、両者の間に名前のコナーセンスがあるといえる。リファクタリングの過程で関数名を変更しようとすると、関数を宣言する箇所とその関数を呼び出す全箇所を修正しなければならない。
名前のコナーセンスは「弱い」、つまり発見しやすいし変更が容易である。変数名を変更するにはコードを静的解析すればよいため IDE の支援を受けられるし、最悪でもコードベース内を grep すれば探し出せる。
それより強いコナーセンスに「意味のコナーセンス」がある。これは特定のプリミティブな値に意味をもたせている場合で、わかりやすいのはいわゆるマジックナンバーである。ハードコーディングされた定数値がコードベース内に散在していると、値を変更しようとした場合に修正漏れが発生しやすくなる。そのためマジックナンバーは変数名と比べると変更コストが高く、「強い」コナーセンスであるというわけである。マジックナンバーを管理する定数を用意して定数名によってアクセスするようにすれば、意味のコナーセンスから名前のコナーセンスへとコナーセンスが「弱まる」。こうしたリファクタリングは、コナーセンスを弱めるという点で結合度を下げている。
上に挙げた2つのコナーセンスは静的コナーセンスと呼ばれ、基本的にソースコードを読めば発見できるものである。対して動的コナーセンスという分類もあり、こちらは実行時の挙動を知っていなければ発見できないもので、静的コナーセンスよりも一般的に「強い」。
動的コナーセンスにはたとえば「実行のコナーセンス」がある。これは実行の順序が重要な場合に発生する。順序正しく実行しないとエラーになるようなコンポーネントを想定しており、代表的なものは状態マシンである。状態マシンというものは、初期化済み状態になってはじめて何かを実行できるといったように、ある特定の状態にあるときにだけ特定の操作を許可する。状態マシンを内部に持つクラスがあるとして、それを使用する側のコードは(状態マシンが上手にモデル化されていれば別だが)「先に初期化処理をしなければあるメソッドを実行できない」という実行時の挙動を知っている必要がある。これは実行時のふるまいを知る必要があるという点で「動的」コナーセンスであり、呼び出す側のコードを読むだけでは変更の影響を知ることができないという点で「強い」コナーセンスである。
ここまで例をいくつか挙げたが、コナーセンスは結局、ある機能を提供する側のコンポーネントと使用する側のコンポーネントの間で何かが一致しなければならない状況のことである。名前のコナーセンスは何かを参照する名前が一致しなければいけないし、意味のコナーセンスは値の意味が一致しなければいけない。実行のコナーセンスは実行順序が一致しなければいけない。
以下はコナーセンスのタイプを弱いレベルから順に列挙したものである。順序は実際には厳密なものではなく目安である。
- 名前のコナーセンス - 変数名の一致
- 型のコナーセンス - データの型の一致
- 意味のコナーセンス - 特定の値に関する意味の一致
- 位置のコナーセンス - 引数の位置など、位置の順序の一致
- アルゴリズムのコナーセンス - エンコーディングなど、アルゴリズムの一致
- 実行のコナーセンス - 実行順序の一致
- タイミングのコナーセンス - 実行タイミングの一致
- 値のコナーセンス - 一緒に変更する値の一致
- 同一性のコナーセンス - エンティティ参照の一致
コナーセンスの程度
コナーセンスの程度とは、関連するコンポーネントの多さである。あるクラスを使用するクラスが100個あれば、程度は「大きい」。
一般にはコナーセンスの程度が大きくなると修正が困難になるので小さく保つのがよい。また、強度との関係で言えば、「弱い」コナーセンスなら程度が大きくても許容できるが、他方「強い」コナーセンスの程度が大きいと、修正箇所が多いだけでなく壊さぬように神経を使わなければいけないので、修正が難しくなる。
特にユーティリティ系のモジュールはさまざまなコンポーネントから使用されるためコナーセンスが大きくなる傾向にある。そういうモジュールは「弱い」コナーセンスに保つよう最新の注意を払っていきたい。
コナーセンスの局所性
コナーセンスの局所性とは、コナーセンスのある2つのコンポーネントが互いに近い場所にあるか遠い場所にあるかである。コナーセンスのある2箇所のコードが同じ関数内にあれば局所性は小さい。互いに別のモジュールにあったり、別のサービスにあったりすると、遠い場所にあるため局所性は大きい。
変更容易性の高いコードベースを保つには、「強い」コナーセンスは同じモジュール内にとどめておいて局所性を小さくするとよい。さもなくばモジュール間に暗黙の依存関係が生まれて影響範囲が予測しにくくなる。言い方を変えると、強いコナーセンスを互いに近い場所にとどめておくことは、凝集度を上げることにもなる。
強度、程度、局所性のバランス
コナーセンスは弱ければ良い、強ければまずいと単純には言えない。考慮すべきはバランスである。
コナーセンスを測る3つの尺度をまとめると以下のようになる。
- 強度 - 発見・変更の難しさ。変更が難しくなるほど「強い」
- 程度 - 関連するコンポーネントの多さ。多いほど程度が「大きい」
- 局所性 - コンポーネントが互いにどれだけ離れた場所にあるか。離れた場所にあるほど局所性が「大きい」
強いコナーセンスは程度を小さく、局所性を小さく保っておく。そうすると変更の影響範囲をコントロールできる。弱いコナーセンスは程度を大きくしてもよいが、あまりにも大きいと問題になる。
マイクロサービスアーキテクチャの文脈で言うと、「APIファースト」な開発が良いと言われるのは、マイクロサービス間のコナーセンスを弱く保つためである。マイクロサービス間のコナーセンスは当然、局所性が大きい。そこで変更の影響範囲を予測可能にするために名前と型のような弱いコナーセンスに限定していこうという話である。
共通の語彙としてのコナーセンス
ちゃんと調べられていないが、おそらくコナーセンスは定量的なメトリクスではない。もしかすると定量化できるのかもしれないが、実行時の挙動を知る必要がある動的コナーセンスという概念があるため、ソースコードの静的解析から単純に計算できるものではない。
コナーセンスはむしろ定性的なメトリクスであると思う。つまり、結合度の低い、変更容易性の高いコードを書くために開発者の共通の語彙となるような基準である。一口に「結合度の低い、凝集性の高いコードを書きましょう」といっても、それだけでは抽象的で人によって理解がまちまちかもしれない。コナーセンスは結合度の概念を具体的に説明しているので、コードレビューやリファクタリングにおいて「このコードだとなぜ結合度が高いと言えるか」のような話を開発者間で共有できそうである。