この記事は、保守性の高いコード設計シリーズの一部です。これまでの記事では、クラスの内部構造を堅牢にする「不変性」などについて解説しました。今回は、クラスとクラスの関係性に目を向け、ソフトウェアの変更を困難にする「低凝集」の問題とその解決策を探ります。
こんにちは。皆さんのプロジェクトには、Common
やUtil
といった名前のクラスはないでしょうか?様々な便利メソッドが詰め込まれたこれらのクラスは、多くの場合**「低凝集」**のサインです。
凝集度とは、クラスやモジュール内部のデータとロジックが、どれだけ強く関連し合っているかを示す指標です。「高凝集」なクラスは、一つの関心事に集中しており、変更に強く、再利用しやすい理想的な状態です。
今回は、低凝集を引き起こす代表的な原因であるstatic
メソッドの誤用と、初期化ロジックの分散について、具体的な改善策と共に見ていきましょう。
static
メソッドが凝集度を破壊する仕組み
static
メソッドは、インスタンスを生成せずに呼び出せるため、一見便利に思えます。しかし、それは多くの場合、本来あるべきクラスからロジックが引き剥がされていることを意味します。
【Before】データとロジックが分離した、低凝集な設計
// 金額データを保持するだけのクラス (データクラス)
class PriceData {
int amount;
PriceData(int amount) { this.amount = amount; }
}
// 金額の計算ロジックを持つ、全く別のクラス
class PriceCalculator {
// staticな加算メソッド
static int add(int amount1, int amount2) {
return amount1 + amount2;
}
}
// 実際の利用コード
PriceData price1 = new PriceData(100);
PriceData price2 = new PriceData(200);
// 計算のために、わざわざ外部のstaticメソッドを呼び出す必要がある
int addedAmount = PriceCalculator.add(price1.amount, price2.amount);
PriceData result = new PriceData(addedAmount);
このコードでは、「金額」という概念がPriceData
(データ)とPriceCalculator
(ロジック)に分離してしまっています。add
メソッドは、本質的には「金額」に関するロジックであり、Price
クラス自身が持つべきです。
static
メソッドはインスタンス変数(this
)にアクセスできないため、このようにデータとロジックを分離させる構造を生み出しやすいのです。
いつstatic
メソッドを使うべきか?
では、static
メソッドは悪なのでしょうか?いいえ、適切な使い所があります。
- 真のユーティリティ:
Math.max()
のように、特定のインスタンスの状態に全く依存しない、純粋な計算処理。 - 横断的関心事:
Logger.log()
のように、システムの様々な場所から呼び出される、特定のビジネスロジックとは無関係な機能。 - ファクトリメソッド: 次に解説する、特定の目的を持ったインスタンスを生成する場合。
初期化ロジックの分散とファクトリメソッド
低凝集は、オブジェクトの生成方法においても現れます。例えば、顧客への付与ポイントを管理するCustomerPoint
クラスがあるとします。
【Before】生成ロジックが、利用する側のコードに分散している
// 新規会員の登録特典ポイント
CustomerPoint welcomePoint = new CustomerPoint(500);
// プレミアム会員へのアップグレード特典ポイント
CustomerPoint premiumUpgradePoint = new CustomerPoint(2000);
このコードの問題点は、500
や2000
という数値(マジックナンバー)が何を意味するのか、CustomerPoint
クラス自身は知らないことです。「新規会員特典」や「プレミアム特典」といったビジネスルールが、クラスの外側、つまり利用する側のコードに漏れ出してしまっています。
この初期化ロジックをクラス内に集約させるのがファクトリメソッドです。
【After】ファクトリメソッドで、生成目的を明確にする
class CustomerPoint {
private static final int WELCOME_POINT = 500;
private static final int PREMIUM_UPGRADE_POINT = 2000;
final int value;
// コンストラクタはprivateにし、外部から自由な値で生成させない
private CustomerPoint(final int point) {
// ... バリデーション ...
this.value = point;
}
// 「新規会員向け」という目的でインスタンスを生成するstaticファクトリメソッド
static CustomerPoint forNewWelcome() {
return new CustomerPoint(WELCOME_POINT);
}
// 「プレミアム会員向け」という目的でインスタンスを生成するstaticファクトリメソッド
static CustomerPoint forPremiumUpgrade() {
return new CustomerPoint(PREMIUM_UPGRADE_POINT);
}
// ...
}
// 利用側のコードは、目的を伝えるだけで良くなる
CustomerPoint welcomePoint = CustomerPoint.forNewWelcome();
CustomerPoint premiumUpgradePoint = CustomerPoint.forPremiumUpgrade();
コンストラクタをprivate
にし、目的別のstatic
なファクトリメソッドを公開することで、**インスタンスの生成方法に関する知識をクラス自身に集約(高凝集化)**できます。これにより、利用側は詳細を知る必要がなくなり、コードはより意図が明確で、変更に強いものになります。
まとめ
凝集度を高める基本は、**「データと、そのデータを操作するロジックは、できるだけ近くに置く」**という原則です。
static
メソッドの多用は、ロジックが本来あるべき場所から離れているサインかもしれません。- ファクトリメソッドを活用し、インスタンスの生成ロジックもクラス自身に責任を持たせましょう。
▼次の記事 次回は、メソッドの「顔」である引数に焦点を当てます。多すぎる引数や、副作用を伴う引数の使い方が、いかにしてコードを複雑で危険なものにするか、そしてそれをどう解決するかを探ります。