「修正したら別の場所でバグが出た!」
「コードが複雑すぎてどこを直せばいいのかわからない!」
ソフトウェアを開発するうえで、そのような経験は誰しもあるはずです。
その原因の多くは結合度が高い、または凝集度が低い、またその両方がソフトウェア内部に存在していることにあります。これらは古くから設計品質を測る普遍的な指標であり、現実的に解決する最適解のひとつがオブジェクト指向です。
本記事では、結合度と凝集度の基礎から始め、オブジェクト指向の基本的な考え方であるカプセル化・情報隠蔽・継承・ポリモーフィズムが品質にどう効くのか、さらにComparatorを例に仕様と実装の分離を解説します。
目次
1. 結合度と凝集度の基礎を理解する
結合度(Coupling)とは
結合度はモジュール同士の依存の強さを示す指標です。あるモジュールが他のモジュールの内部実装に強く依存している場合、それらは「結合度が高い」ということです。
- 高結合:1つの変更が他のモジュールに波及しやすい
- 低結合:変更の影響範囲が局所化される
理想的には、モジュール間の依存関係を少なく、可能ならばゼロにすることです。結合度を少なくすることで保守性・拡張性・テスト容易性が高まります。
凝集度(Cohesion)とは
凝集度はモジュール内部の要素の関連の強さです。
- 低凝集:1つのモジュールに関連性が低い複数の情報が混在している状態
- 高凝集:1つのモジュールに関連性が高い情報だけが集まっている状態
単一責任の原則(SRP)とも直結し、理想は凝集度が高い状態です。
結合度の最悪例:グローバル変数という“全方位依存”
最悪の例としてはグローバル変数が挙げられます。どこからでも無制限にアクセスできるので、すべてのモジュールが依存している状態になります。
public class Globals {
public static int sortMode = 0; // 0: name, 1: date, 2: size
}
複数のクラスが Globals.sortMode に依存すると、どこかで値を変えた瞬間に、それを参照している箇所の挙動が変わります。修正の波及、テストの不安定化、グローバル変数が存在することにより、責任の所在不明などの多数の問題が発生します。グローバル変数は「高結合・低凝集」の象徴です。
結合度を低くする第一歩はグローバル変数を排除することです。
2. オブジェクト指向の原則が導く低結合・高凝集
カプセル化(Encapsulation)
データとそれを操作するメソッドをひとまとめにし、外部から直接状態を触らせないことで、外部依存を減らし(低結合)、内部整合性を守ります(高凝集)。
例えば、口座(Account)というクラスでは、外部から残高(balance)に対する直接アクセスすることを不可能にし、メソッド経由でのみ変更できるようにすることで、残高を不正に変更できないようにします。
public class Account {
private int balance;
public void deposit(int amount) {
this.balance += amount;
}
public void withdraw(int amount) {
if (amount <= balance) balance -= amount;
}
public void getBalance() {
return balance;
}
}
情報隠蔽(Information Hiding)
Accountクラスはbalanceをクラスの内部に隠蔽することで、不正な変更を防止します。
外部は「何ができるか」だけを知り、「どう実現しているか」は知らない。実装を変えても呼び出し側に影響しないため、低結合を保てます。
継承(Inheritance)
再利用の手段ですが、親クラスへの強い依存を生むため慎重に考慮する必要があります。変更の影響の波及を避けるため、近年は継承より委譲(Composition over Inheritance)が推奨されています。
ポリモーフィズム(Polymorphism)
同じインターフェースで異なる実装を扱う仕組み。呼び出し側は具象を知らずに利用でき、実装側は責務に集中できます。低結合・高凝集の中核です。
3. Comparatorに学ぶ「仕様と実装の分離」
仕様はインターフェースで、実装はクラスで
Comparator<T> は「2つのオブジェクトをどう比較するか」という仕様のみを定義し、実際のロジックは個別のクラスに切り出せます。
public interface Comparator<T> {
int compare(T a, T b);
}
java.io.Fileをソートする具体例
java.io.File は名前・サイズ・更新日時を備え、Comparatorの教材に最適です。
ファイル名、ファイルサイズ、更新日時でソートするためのComparatorを用意します。
ファイル名で比較するComparatorとしてFilenameComparatorを作成します。
import java.io.File;
import java.util.Comparator;
public class FilenameComparator implements Comparator<File> {
public int compare(File a, File b) {
return a.getName().compareTo(b.getName());
}
}
ファイルサイズで比較するComparatorとしてFilesizeComparatorを作成します。
import java.io.File;
import java.util.Comparator;
public class FilesizeComparator implements Comparator<File> {
public int compare(File a, File b) {
return Long.compare(a.length(), b.length());
}
}
更新日時で比較するComparatorとしてUpdateTimeComparatorを作成します。
import java.io.File;
import java.util.Comparator;
public class UpdateTimeComparator implements Comparator<File> {
public int compare(File a, File b) {
return Long.compare(a.lastModified(), b.lastModified());
}
}
作成した各Comparatorを使用してソートするコードは以下のようになります。
sort()メソッドに渡すComparatorを変更するだけでソート順を変更できます。sort()メソッドは、渡されたComparatorの仕様だけを認識していて、実装には一切依存していないことがわかります。
import java.io.File;
import java.util.*;
List<File> files = List.of(new File("a.txt"), new File("b.txt"), new File("c.txt"));
files.sort(new FilenameComparator()); // ファイル名順
files.sort(new FilesizeComparator()); // サイズ順
files.sort(new UpdateTimeComparator()); // 更新日時順
Windowsエクスプローラの例で理解する
Windowsのエクスプローラの詳細表示で「名前順」「サイズ順」「更新日順」に並び替えられるのは、まさにこの発想です。
ソート機能自体(呼び出し側)は比較方法(実装)を知らず、各Comparatorは比較という責務に集中できます。
結果として、呼び出し側は仕様にのみ依存(低い結合度)、各実装は責務集中(高い凝集度)を達成します。
さらに、エクスプローラでは大きいアイコンや小さいアイコンなどの表示方式を切り替える機能もあります。
これは、ソートと同様のやりかたで、表示するアルゴリズムを切り替えるように作られています。
4. 結合度・凝集度から見たオブジェクト指向の本質
グローバル変数は“全方位依存”。対してオブジェクト指向は「依存を制御し、責務を閉じ込める」仕組みです。
比較という共通仕様をインターフェースとして定義し、各戦略を独立実装に分離すれば、呼び出し側を変えずに拡張できます。
これこそ低結合・高凝集の実践です。
5. まとめ
| 観点 | 悪い例 | 良い例 |
|---|---|---|
| 結合度 | グローバル変数に依存 | インターフェースに依存(Comparator) |
| 凝集度 | 責務が曖昧なクラス | 明確な責務を持つクラス |
| 拡張性 | コードのあちこちで修正が必要 | 新しいComparatorを追加するだけ |
| 設計思想 | 状態と実装が混在 | 仕様と実装が分離 |
オブジェクト指向は目的ではなく手段です。
オブジェクト指向でソフトウェアを設計することで、結合度を下げ、凝集度を上げることが容易になります。