凝集度と結合度 — 変更したとき何ファイル触るか
良い設計は「動くか」では測れない。測れるのは、仕様変更が来たときの影響範囲。それを制御する2つの語彙。
この記事の進み方
What — 2つの軸で構造を測る
1つのモジュールの中身が、どれだけ1つの目的に向かっているか。高いほどよい。
// 凝集度が低い:関係ないものが同居している
class UserService {
createUser() {}
sendEmail() {} // ← メール送信は別の関心事
generateCsvReport() {} // ← レポートも別
validateCreditCard() {} // ← 決済も別
}
// 凝集度が高い:1つの目的に向かっている
class UserRegistration {
register() {}
validateEmail() {}
checkDuplicate() {}
}- 測り方
- この中身は1文で説明できるか
- 低いと
- 無関係な変更で同じファイルを触る
- 高いと
- 変更の影響が中に閉じる
- –「〜Manager」「〜Util」「〜Helper」という名前は、凝集度が低いサインになりやすい。何をするか説明できないから曖昧な名前になる。
- –クラス名を1文で説明できないなら、2つ以上の責務が混ざっている。
「凝集度を高く、結合度を低く」が設計の基本方針。この2つは独立ではなく、責務を正しく分けると両方が改善する。
結合度には段階がある
上ほど弱く、望ましい。下に行くほど、相手の変更が自分に波及しやすくなる。
calculateAmount(items, isMember, hasCoupon, forExport) のようなフラグ引数が危ういのはなぜですか?
Why — なぜこの2つで測るのか
ソフトウェアの品質は「正しく動くか」だけでは測れません。動くコードは、良いコードとは限らないからです。
では何で測るか。仕様変更が来たときに、何ファイル触るかです。
- 凝集度が高い → 1つの変更が1つのモジュールに収まる
- 結合度が低い → その変更が他へ波及しない
この2つが揃うと、変更の影響範囲が予測可能になります。予測できれば、見積もりができ、テスト範囲が決まり、レビューで見るべき場所が分かります。
逆に、凝集度が低く結合度が高いコードは、どこを触ると何が壊れるか誰にも分からない状態になります。この状態のコードベースでは、小さな修正にも大きな見積もりがつきます。
「共通化」が失敗する理由
冒頭の事故は、DRY 原則の誤用です。DRY(Don't Repeat Yourself)が言っているのは「同じ知識を重複させるな」であって、「似ているコードをまとめろ」ではありません。
判断基準は変わる理由です。
// 一見同じだが、変わる理由が違う
function formatUserName(user) { // 表示のルールで変わる
return `${user.lastName} ${user.firstName}`;
}
function formatInvoiceName(user) { // 請求書の法的要件で変わる
return `${user.lastName} ${user.firstName}`;
}
この2つを共通化すると、表示ルールを変えたときに請求書まで変わります。現時点で同じでも、変わる理由が違うなら分けておくのが正しい。
逆もあります。
// 見た目は違うが、同じ知識
const TAX_RATE = 0.10; // A ファイル
const taxAmount = price * 0.1; // B ファイル(直書き)
こちらは同じ知識が2か所にあります。税率が変わったとき、片方だけ直して事故になる。これこそが DRY が防ごうとしているものです。
フラグ引数が危険な理由
process(data, true); // ← この true は何?
呼び出し側を読んでも意味が分かりません。そして、フラグで分岐する関数は実質的に2つの関数です。
function process(data, isDryRun) {
if (isDryRun) { /* 検証だけ */ }
else { /* 実際に実行 */ }
}
この2つの経路は、共通部分より違う部分のほうが多いことがよくあります。分ければ、それぞれの凝集度が上がり、呼び出し側も読みやすくなります。
processDryRun(data);
processAndCommit(data);
似たコードを見つけたとき、共通化してよいかの判断軸はどれですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
このクラスをどう分割しますか?
- EC サイトの OrderService クラス。現在 1200 行あり、以下のメソッドを持つ
- createOrder / cancelOrder / calculateTotal / applyCoupon
- chargeCreditCard / refund(決済代行 API を呼ぶ)
- sendOrderConfirmationEmail / sendShippingNotice(メール送信)
- exportOrdersToCsv(経理部向けの日次エクスポート)
- updateInventory(在庫システムの API を呼ぶ)
- 直近の変更履歴: メールの文面変更が5回、決済代行の仕様変更が2回、注文ロジックの変更が1回
- テストを書こうとすると、決済 API・メール送信・在庫 API・S3 の4つをモックする必要がある
- · 変更履歴から、何が別々の理由で変わっているかが読めるか
- · モックが4つ必要という事実は、何を示しているか
- · 分割したあと、それぞれを1文で説明できるか
「共通化したのに、なぜか変更が怖くなった」という相談に、何が起きたかを説明してください
- 3画面の似た処理を1関数にまとめた経緯がある
- 現在はフラグ引数が4つある
- 分割の指針まで示したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。