凝集度(コヒージョン)は、単一のモジュールやクラス内の要素がどれだけ密接に関連しているかを示すメトリクスです。Wikipediaによると、高凝集は適切に設計されたモジュールの特徴であり、すべてのメソッドとフィールドが単一のタスクに取り組みます。凝集度はコードの保守性に直接影響し、結合度(カップリング)——モジュール間の相互接続性——と対比されます。
重要なポイント
凝集度は、単一のクラスやモジュール内のメソッド、フィールド、プロパティが論理的にどれだけ結びついているかを評価するメトリクスです。凝集度の高いモジュールは1つのタスクを実行し、その達成に必要な要素のみを含みます。凝集度の低いモジュールは一度に複数のことを行おうとし——メソッド間の意味的な関連性が弱くなります。
オブジェクト指向プログラミングの文脈では、凝集度は単一責任の原則(S)と密接に関連しています。クラスに明確な責任が1つある場合、その凝集度は一般的に高くなります。クラスがUI、ビジネスロジック、ネットワーキングを同時に扱う場合——凝集度は低く、そのようなクラスはより狭い責任を持つ複数の別々のクラスに分割すべきです。
凝集度を理解することは、開発者がリファクタリングの判断をするのに役立ちます。クラス内にそのクラスのフィールドを一切使用しないメソッドがある場合、それは低凝集の兆候です。そのようなメソッドはクラス内に置く場所が間違っているか、クラスの設計が不適切です。高凝集を目指すことは、コードのあらゆるレベルでアーキテクチャを改善し続ける継続的な取り組みです。
ソフトウェア工学では、凝集度の7つのレベルが、最低から最高まで区別されています。この尺度を理解することで、モジュールの品質を客観的に評価し、リファクタリングの方向性を決定できます。レベルが高いほど、コードの保守性と理解しやすさが向上します。
偶発的——最低レベルで、モジュール内の要素が論理的な関連性なしにランダムにグループ化されています。例:日付の書式設定、メール送信、割引計算のメソッドを持つUtilitiesクラス。このようなクラスはすべてのメソッドを読まなければ理解できず、1つのメソッドを変更するだけで、単に同じ場所にあるという理由で他のメソッドが壊れる可能性があります。
論理的——要素は論理的に関連しているが、根本的に異なるタスクを実行します。parseJSON、parseXML、parseCSVのメソッドを持つクラスは「パース」というトピックで論理的につながっていますが、各メソッドは根本的に異なる作業を行います。問題点:新しい形式(YAML)が追加されるとクラスが肥大化し、インターフェースが膨張します。
時間的——要素が実行時間でグループ化されています。データベースをセットアップし、設定をロードし、分析を初期化するAppInitializerクラス——これらはすべてアプリ起動時に行われますが、タスク自体は無関係です。責任領域ごとに別々のInitializerに分割する方がよいでしょう。
手続き的——要素が実行の順序で結びつけられている場合に発生します。「注文処理」モジュールにはvalidateCart、processPayment、sendConfirmationのメソッドが含まれ——各メソッドは前のメソッドの後に厳密に呼び出されます。これは偶発的や論理的凝集よりは優れていますが、まだ理想的ではありません。各ステップを別々のモジュールに抽出できます。
通信的——要素が同じデータを扱います。getUser、updateUser、deleteUserのメソッドを持つUserServiceクラスは共通のUserエンティティで結びついています。これは手続き的凝集より大幅に優れており、クラスには明確なドメインがあります。モバイルプロジェクトのほとんどのRepositoryクラスは通信的凝集を持っています。
機能的——最高レベルで、モジュールのすべての要素が単一のタスクの実行に参加します。長さ、文字の有無、パスワードの複雑さをチェックする単一のvalidateメソッドを持つPasswordValidatorクラスは、機能的凝集の例です。このようなクラスが変更されるのは、パスワード検証ルールが変更された場合のみです。
機能的凝集の達成は、アーキテクチャリファクタリングの主要な目標です。各クラスは変更理由を正確に1つ持つべきです。モバイル開発では、機能的凝集は個別のUse Cases、カスタムView、フォーマッター、バリデーターを抽出することで達成されます。そのような各クラスは、明確な責任範囲を持つ完全な構成要素です。
凝集度と結合度は同じ品質の両面です。モジュール内の凝集度が高いほど、モジュール間の結合度は低くなる傾向があります。適切に設計されたシステムは、高い内部凝集度と疎な外部結合度を同時に目指します。この原則は1970年代からソフトウェア工学の基本として認識されています。
凝集度-結合度の関係はバランスとして考えることができます。開発者が複数のタスクを1つのクラスにまとめて凝集度を犠牲にすると、隣接するモジュールはより多くの依存関係を得ます——さまざまな目的でこの過負荷なクラスにアクセスする必要が生じ、結合度が高まります。逆に、小さな高凝集クラスへの分割は、モジュール間の相互作用ポイントを減らします。
実際には、これは次のことを意味します:機能的凝集を持つ新しいクラスを抽出すると、同時に他のモジュールがその実装の詳細を知る必要がなくなります。例えば、EncryptionManagerを機能的凝集を持つ別のクラスに抽出することで、他のモジュールは暗号化アルゴリズムの詳細を理解する必要なく、シンプルなencrypt/decryptインターフェースを得られます。
// 低凝集 — クラスが一度にすべてを行う
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// 高凝集 — 各クラスが1つのタスクを解決する
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
この例は違いを示しています:UserManagerは論理的凝集を持っています——すべてのメソッドがユーザーに関するものですが、それぞれが根本的に異なる作業を行います。リファクタリング後、各クラスは機能的凝集を持ち、他のモジュールは必要なクラスにのみ依存するため、結合度が低下します。
LCOM(メソッドの凝集度の欠如)は、クラスの凝集度を測定する最もよく知られたメトリクスです。LCOMは共通のフィールドを共有しないメソッドのペアの数をカウントします。値0は理想的な凝集度(すべてのメソッドが同じフィールドで動作)を意味し、高い値は低凝集を示します。LCOM4(改良版)は他のメソッドを介した推移的な接続も考慮します。
Android開発では、DetektのTooManyFunctionsルールを通じて凝集度メトリクスを取得できます。異なるフィールドグループを使用する多数のメソッドを持つクラスは、おそらく低凝集です。iOSでは、SwiftLintにfile_lengthとfunction_body_lengthのルールがあります——間接的な指標:長いファイルやメソッドはしばしば低凝集を示します。
手動の評価方法:「このクラスは1つの理由で変わりますか、それとも複数の理由で変わりますか?」と質問します。複数の独立した理由を挙げられる場合——そのクラスは低凝集です。2つ目のテスト:「このクラスを2つの独立したクラスに分割できますか?」はいの場合——実行しましょう。コードレビューで定期的に凝集度をチェックすることで、神クラスを防ぎ、技術的負債を減らせます。
最初のステップ——単一責任の原則を適用します。各クラスは1つの明確な責任を持つべきです。クラスに主要タスクに関係しないメソッドがある場合、それを別のクラスに抽出します。IDEのExtract ClassやExtract Delegateテクニックがこのプロセスを自動化します。抽出後、元のクラスがより集中したものになったか確認します。
2番目のステップ——Facadeパターンを使用してインターフェースを簡素化します。クラスが20のメソッドを提供しているが、クライアントが3〜4しか使用しない場合、そのクラスは低凝集かもしれません——多様すぎる機能を提供しています。メソッドをトピックごとにグループ化し、各グループに別々のクラスを抽出し、元のクラスをファサードにするか削除します。
3番目のステップ——フィールドグループに注意を払います。クラスにメソッドのサブセットのみで使用されるフィールドがある場合——それは低凝集の指標です。フィールドグループごとにクラスを分割します。例えば、クラスにuserRepository、networkClient、analyticsTrackerのフィールドがあるが、最初のメソッドグループはuserRepositoryのみを使用し、2番目はnetworkClientを使用する場合——これらは2つの異なるクラスです。
4番目のステップ——任意のstaticメソッドを持つ「ユーティリティ」クラスの作成を避けることです。UtilsやHelpersクラスにあるすべてのstaticメソッドは、専門クラスに抽出する候補です。FormatUtils.dateToStringはDateFormatterに、ValidationUtils.isValidEmailはEmailValidatorに移動する方がよいでしょう。これにより各クラスの凝集度が高まり、コードが自己文書化されます。
よくある質問
ほぼ常に良いです。機能的凝集はコードを明確で予測可能にします。ただし、極端に進めると過度な断片化を引き起こす可能性があります——すべての操作に別々のクラスを作成し、アーキテクチャが過度に複雑になります。バランスは、機能ごとに数個のクラスを設け、それぞれが機能的凝集を持つことです。
凝集度は単一のモジュールやクラス内の内部一貫性のメトリクスです。モジュール性は、アプリケーションを物理モジュールに分割するアーキテクチャ原則です。高凝集は個々のクラスとモジュール全体の両方を設計する際の目標です。
Android用のDetektとiOS用のXcode Analyzerは、メソッドやフィールドの数が疑わしく多いクラスを強調表示します。IntelliJ IDEAとAppCodeには依存関係の可視化機能があり——接続グラフを見て低凝集のクラスを発見できます。SonarQubeはLCOMメトリクスを自動的に計算します。
はい。connect、disconnect、isConnectedのメソッドを持つインターフェースは高凝集です——すべてのメソッドが接続管理に関連しています。connect、parseData、renderUIのメソッドを持つインターフェースは低凝集です。インターフェース分離の原則(SOLID)は、高凝集の狭く焦点を絞ったインターフェースを作成することを要求します。
3つの質問をします:クラスの目的を1文で説明できますか?すべてのメソッドがこの目的をサポートしていますか?一部のメソッドで使用されていないフィールドはありますか?いずれかの質問に対する答えが「いいえ」の場合——凝集度は低く、クラスを分割すべきです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。