はじめに
良いコードを考える上で重要と言われるのが
内部品質(可読性・保守性・柔軟性) です。
しかし、
「具体的にどのような観点で実装すれば内部品質が上がるのか?」
と言われると、言語化しづらい方も多いのではないでしょうか。
実は多くのデザインパターンや実装の考え方は
凝集度と結合度 を軸として整理できます。
(※あくまで私の体感ですが、かなり本質に近いです)
下記スライドでも紹介されているように、
凝集度・結合度は新人研修でも重要視されるレベルの基本概念です。
なので凝集度、結合度に対する理解を深め、
具体的にどのような実装が好ましいかまとめました!
あなたはこの実装をどう思いますか
まずは、よくある関数を見てください。
// 例1
function processUser(userId: string) {
const user = findUser(userId); // DB処理
clearCache(userId); // キャッシュ操作
writeAuditLog(user); // 監査ログ
sendWelcomeEmail(user.email); // メール送信
}
// 例2
function changeUser(user: User) {
user.status = "completed";
}
これを見てどう感じますか?
- 何も思わない
- 問題なし
- 何かまずいことしてるの?
もし「特に違和感なし」なら、
“とりあえず動く” 実装から抜け出せていない可能性があります。
(もしこの実装で自分にレビュー依頼が飛んできたら即突っぱね返します笑)
最近はAIがコードを書ける時代です。
求められるのは “コードを書ける人” ではなく
“どのような実装が好ましいか判断できるエンジニア” です
求められるもの
- AIが生成したコードをレビューし指摘できる
- 運用コストを最小限にする構造を考えられる
- 仕様変更に強い構造を作れる
- セキュリティ・脆弱性を考慮した設計ができる
- 誰でもすぐ理解できるシンプルな実装ができる
もし先ほどのコードを見て改善点が浮かばなかったなら、
まずは凝集度・結合度を理解することが重要です。
凝集度(Cohesion)
- ひとつのモジュール(関数/クラス)が どれだけ明確な目的に集中しているか
- 凝集度は高いほど良い
- 7段階で分類され、最終的に目指すのは 機能的凝集(★5)
// 偶発的凝集(最悪の凝集度)の例
// 1. processUserの役割が多いためそれぞれの処理は別の関数に分離すべき
// 2. それぞれの処理に関連性がなく、この関数の責任範囲や目的がわからない
// prosessUserなのになぜキャッシュクリアやメール送信を行う?役割多くない?
public async processUser(userId: string): Promise<void> {
// ① DBからユーザ取得
const user = await this.db.findUser(userId);
// ② ログ集計
this.logger.log(`Processing user ${userId}`);
// ③ キャッシュクリア
this.cache.clear(userId);
// ④ メール送信
await this.sendWelcomeEmail(user.email);
// ・・・とりあえず動くけど関連性薄い処理が混ざっている
}
要約
- 凝集度=「その関数はどれだけ単一目的か」の指標
- 高いほど良い(責務が明確)
- 低いと保守性が下がり、副作用まみれのコードになる
凝集度が低いことによる被害
- 可読性低下
- 何がしたい関数なのか分からなくなる
- 再利用不可
- ex) 関数A(A, B, C の処理を行う)が実装されているが、Bの処理は不要だからと関数B(A, C の処理だけを行う)を新規実装し保守が面倒になる
- 類似関数が量産される
- ex) DBからデータ取得する処理で関数AはSQLクエリにlimitあるのに、関数Bではデータ取得後、filter関数によって件数制限している
- どちらを使えばいいか分からない
- どちらがプロジェクトの推奨関数なのか分からない
- ex) DBからデータ取得する処理で関数AはSQLクエリにlimitあるのに、関数Bではデータ取得後、filter関数によって件数制限している
- バグ調査が困難
- 無関係な処理が多いことで副作用が大量発生し特定困難
- 軽微な変更で大量の実装修正が必要
- 1関数の責任が多く何かを変更すると別の責務に影響が出る
凝集度 一覧
凝集度は以下の7段階に分類されます!
機能的凝集 ★5 の構造に近い関数ほど良いとされます。
| 名称 | 度合い | 評価 | 説明 |
|---|---|---|---|
| 偶発的凝集 | ★1 | ❌ | ・無関係な処理がただ集まっている ・凝集度:一番低い |
| 論理的凝集 | ★2 | △ | ・条件分岐で似た処理をまとめている ・if / else 条件分岐で似た処理をまとめている |
| 時間的凝集 | ★2 | △ | ・同じタイミングで行う処理をまとめている ・初期化処理にファイル読込やログ初期化を実装 |
| 手続き的凝集 | ★3 | ○ | ・手順としては連続するが目的は多様 ・この順番でやらないといけない処理の塊 |
| 通信的凝集 | ★4 | ◎ | ・同じデータを扱う処理がまとまっている ・責務が整理されているため、理解しやすい |
| 逐次的凝集 | ★5 | ◎ | ・前の処理の出力が次の処理の入力になる ・一連のステップがまとまっている |
| 機能的凝集 | ★5 | ◎ | ・ひとつの明確な目的だけを実現している ・凝集度:一番高い |
それでは各凝集度の関数をどのように修正すべきか解説していきます!
before.ts, after.tsで実装をまとめているので
自分だったらどう修正するか、考えながら参考にしてください!
偶発的凝集(Coincidental cohesion)
度合い:★1 評価:❌(好ましくない)
- モジュール内に意味的なつながりのない処理が集まっている状態
- 「とりあえずここに置いておこう」という感じで詰め込まれた結果、関数名やクラス名から意図が読み取れない
- バグが出ても、どの処理がどんな影響を与えているのか追いづらい
- テスト時に「不要な副作用」まで毎回ついてくるため、単体テストもしにくい
実装修正例
// 偶発的凝集(★1)
function processUser(userId: string) {
const user = findUser(userId); // DB処理
clearCache(userId); // キャッシュ操作
writeAuditLog(user); // 監査ログ
}
// 機能的凝集(★5): 「ユーザーを1件読み込む」だけ
function loadUser(userId: string) {
return findUser(userId);
}
// 通信的〜手続き的凝集(★3〜4):
// userId に関係する複数処理がまとまっている(キャッシュ・監査ログ)
function cleanupForUser(userId: string) {
clearCache(userId);
writeAuditLog({ id: userId });
}
// 逐次的凝集(★5): 1つのユースケースの流れ
function handleUserRegistration(userId: string) {
const user = loadUser(userId);
cleanupForUser(userId);
}
論理的凝集(Logical cohesion)
度合い:★2 評価:△(あまり良くない)
- 「ログ出力」「通知」「レポート生成」など論理的に似た処理を1つにまとめている状態。ただし、内部ではフラグや引数で分岐してまったく違う処理を行う
- 呼び出し側が「どの分岐で何が起きるか」を知っていないといけない
- 責務があいまいになりやすい
- 新しい種類を追加すると if / else や switch がどんどん肥大化する
実装修正例
// 論理的凝集(★2)
// 引数によって内部処理が分岐してしまう
function handleOperation(type: "create" | "delete") {
if (type === "create") createUser();
else deleteUser();
}
// 手続き的凝集 ★3〜4
// コンストラクタで User と type を受け取る
class UserOperation {
private readonly user: User;
private readonly type: "create" | "delete";
// コンストラクタで受け取った type と user は final 的に保持される
// → operateUser() の凝集度が高くなる(外部依存が消える)
constructor(user: User, type: "create" | "delete") {
this.user = user;
this.type = type;
}
// 「保持している type に応じた操作を実行する」という単一責務の関数
// これにより論理的凝集(★2)から手続き的凝集(★3〜4)に改善
operateUser() {
if (this.type === "create") {
createUser(this.user); // 機能的凝集(★5)
} else if (this.type === "delete") {
deleteUser(this.user); // 機能的凝集(★5)
}
}
}
時間的凝集(Temporal cohesion)
度合い:★2 評価:△(微妙〜普通)
- 「起動時にやること」「終了時にやること」など、同じタイミングで実行したい処理を1つにまとめた状態
- それぞれの処理同士は、ビジネス的な意味では関連が薄い
- 初期化処理・シャットダウン処理などでよく見られる
- ある程度は許容されるが、増えすぎると「何をしている関数か分からない」状態になりがち
実装修正例
// 時間的凝集(★2)
// 起動時にやること処理がまとまっている
// 本来の目的が異なる処理が1つの関数に混在
function initApp() {
loadConfig(); // 設定のロード(環境構築)
setupLogger(); // ログ環境構築(観測系)
preloadCache(); // キャッシュ初期化(データ準備)
cleanupTempFiles(); // 一時ファイル削除(システムクリーン)
}
// 機能的凝集(★5): 「環境を整える」目的に集中
function initializeEnvironment() {
loadConfig();
setupLogger();
}
// 通信的凝集(★4): データ層の初期化に目的が統一
function initializeData() {
preloadCache();
}
// 機能的凝集(★5): システム後処理に特化
function initializeSystem() {
cleanupTempFiles();
}
// 逐次的凝集(★5): 起動時の目的別処理の流れ
function initApp() {
initializeEnvironment();
initializeData();
initializeSystem();
}
手続き的凝集(Procedural cohesion)
度合い:★3 評価:○(そこそこ良い)
- 決められた手順(シーケンス)を1つの関数にまとめた状態ただし、扱うデータや目的が少しバラけていることがある
- 「この順番でやらないといけない処理の塊」としては筋が通っているので、偶発的・論理的・時間的よりはかなりマシ
- さらに凝集度を高めるには、「同じデータ」や「同じ目的」でまとめる方向へ分割・リネームしていく
実装修正例
// 手続き的凝集(★3)
// validate → save → notify という “手順” でまとまっているが、
// 実際の責務は「検証」「永続化」「通知」と別々の目的。
function registerUserProcess(form: UserForm) {
validateForm(form); // 入力チェック
const userId = saveUser(form); // DB保存
sendConfirmMail(form.email); // メール送信
}
// 通信的凝集(★4): form を一貫して扱うまとまり
function validateAndSaveUser(form: UserForm) {
validateForm(form);
return saveUser(form);
}
// 機能的凝集(★5): 「通知を送る」目的に集中
function notifyUser(email: string) {
sendConfirmMail(email);
}
// 手続き的凝集(★3): 登録フローとしてまとめる
function registerUser(form: UserForm) {
const userId = validateAndSaveUser(form);
notifyUser(form.email);
return userId;
}
通信的凝集(Communicational cohesion)
度合い:★4 評価:◎(かなり良い)
- 同じデータ(引数・オブジェクト)を共通して扱う処理が1つの関数やクラスにまとまっている状態
- 例:ユーザ情報 user を、検証・整形・保存する処理が1まとまり
- データのまとまり単位で責務が整理されているため、理解しやすく変更もしやすい
- まださらに分割できる場合もあるが、実務的には十分「ちゃんとしている」レベル
実装修正例
// 通信的凝集(★4)
// user を中心にまとまっているが、ログ出力という別責務が混入しており純度が低い。
function updateUserProfile(user: User) {
validateUser(user); // 検証
user.displayName = formatName(user.displayName); // データ更新
saveUser(user); // 保存
logUpdate(user.id); // ログ出力(別責務)
}
// 機能的凝集(★5): 「ユーザーを正規化する」単一目的
function normalizeUser(user: User) {
return { ...user, displayName: formatName(user.displayName) };
}
// 通信的凝集(★4): userデータを扱う一連の処理
function updateUserProfile(user: User) {
validateUser(user);
const normalized = normalizeUser(user);
saveUser(normalized);
}
逐次的凝集(Sequential cohesion)
度合い:★5 評価:◎(かなり良い)
- 前の処理の出力が、次の処理の入力になるような、一連のステップがまとまっている状態
- パイプライン処理やETL(Extract → Transform → Load)的な流れと相性が良い
- 「流れ」と「データの変化」の両方が読み取りやすく、処理の筋が非常に通っている
- 1つの明確な目的に向かう流れとなっていれば、ほぼ理想に近い
実装修正例
// 逐次的凝集(★5)に近いが、通知処理が混ざることで凝集度が低下
function importCsv(path: string) {
const raw = readFile(path); // 読み込み
const parsed = parseCsv(raw); // パース
const valid = validateRows(parsed); // 検証
saveRows(valid); // 保存
notify("done"); // ← 別責務
}
// 逐次的凝集(★5): 出力 → 入力 が自然につながる
function importCsv(path: string) {
const csv = readFile(path);
const rows = parseCsv(csv);
const validated = validateRows(rows);
saveRows(validated);
}
機能的凝集(Functional cohesion)
度合い:★5 評価:◎(最も望ましい)
- 関数・モジュールが1つの明確な目的(機能)だけを持つ状態
- 入力と出力がはっきりしており、「この関数は何をするのか?」が名前から即座にわかる
- 副作用が少ない(またはない)ほど、テスト再利用も容易
- 凝集度の観点では最も理想的な形であり、他の凝集度レベルからのリファクタリングのゴールになりやすい
実装修正例
// 機能的凝集(本来★5)だが、ログという不要な副作用が混入している
function sum(a: number, b: number) {
console.log("calculating"); // ← 不要な副作用
return a + b;
}
// 機能的凝集(★5): 「合計値を返す」だけに集中
function sum(a: number, b: number): number {
return a + b;
}
凝集度 まとめ
関数内の処理を別関数で切り出し、「責務を最小限」にすること!
処理が多い関数 = 90%は凝集度が低い実装
分割できないか常に考えること!
以下3つの関数のような構造で実装するように意識する!
可能な限り 3. 機能的凝集 の構造にできないか検討して実装すること!
// 1. 通信的凝集(★4): 同じデータを共通して扱う構造
function updateUserProfile(user: User) {
validateUser(user);
const normalized = normalizeUser(user);
saveUser(normalized);
}
// 2. 逐次的凝集(★5): 一連のステップとしてまとめる構造
function updateUserProfile(path: string) {
const raw = readFile(path); // 読み込み
const parsed = parseCsv(raw); // パース
const valid = validateRows(parsed); // 検証
saveRows(valid); // 保存
}
// 3. 機能的凝集(★5): 1つの明確な目的だけをまとめる構造
function sum(a: number, b: number): number {
return a + b;
}
結合度(Coupling)
- モジュール間の依存の度合い
- どれだけ他モジュールに影響を受けているか / 与えているか
-
結合度は低いほど良い!
- 他モジュールへの依存度合いが小さい!
- 他モジュールの変更に巻き込まれにくくするための考え方
- ex) 引数 { User user }ではなく{ String userId }の方が安全
- Userの構造変更されても影響がないため
- ex) 処理内でUser.id = userIdで直接変更しているが危険では?
- ex) 引数 { User user }ではなく{ String userId }の方が安全
- 引数の構造や参照方法によって結合度は大きく変わる
// 内部結合(最悪の結合度)の例
// 1. 他モジュールであるuserData.userIdをProfileクラスの関数によって変更しているため最悪
// 2. userDataファイルの関数が処理ロジックを持つべき
// 3. ProfileではUserDataを直接更新せず、UserDataクラスの関数を呼ぶべき
import { userData } from "./userData";
export class Profile {
public updateProfile(userId: string) {
// 更新処理
userData.userId = userId;
}
}
要約
- 結合度 = どれだけ他のモジュールに依存しているか
- 低いほど良い(影響範囲が狭い)
- 関数・クラスはなるべく独立させる
結合度が高いことによる被害
- 予期せぬバグ発生率増加
- ex) 関数Aでのみテーブル1の更新をしているはずが、関数Bでもテーブル1の更新がされていた
- ex) UserAを一度インスタンス化し中身が変わることを想定していなかったのに、終端処理でユーザ名が書き換わっていた(関係ない関数でUserAを直接変更していた)
- UT複雑化
- 他モジュールに依存しているためMock化が大量に必要となる
- 再利用不可
- 関数引数がオブジェクト型のため対象関数が利用できない
- ex) getAccount(User user)でUserオブジェクトが必要
- getAccount(String userId)なら使えたのに。。
- 仕様変更に弱くなる
- 1つの修正で10ファイルの修正が発生
- スパゲッティ化しやすい
- A → B → C → A と循環依存が起こる
- 並行開発困難
- 複数の開発者が同時に変更するとお互いの変更が衝突する
結合度 一覧
結合度は以下の7段階に分類されます!
メッセージ結合 ★5 の構造に近い関数ほど良いとされます。
| 名称 | 度合い | 評価 | 説明 |
|---|---|---|---|
| 内容結合 | ★1 | ❌ | ・他モジュールの内部実装に直接依存している ・最悪の結合度で変更に非常に弱い ・fn(user) { user.name = 'タロウ' } |
| 共通結合 | ★2 | ❌ | ・複数モジュールが同じグローバルデータを共有 ・ex) グローバル変数Log logを直接変更 ・fn() { log.level = 'info' } |
| 外部結合 | ★2 | △ | ・複数モジュールが同じ外部リソースに依存 ・環境変化に弱い ・fn() { api.updateUser(user) } ・fn() { return process.env.APP_MODE } |
| 制御結合 | ★3 | △ | ・呼び出し側がフラグで処理を指示 ・fn(isUpdate) |
| スタンプ結合 | ★4 | ○ | ・必要以上に大きなデータ構造をやり取り ・ex) 引数がUserオブジェクト ・fn(User) |
| データ結合 | ★5 | ◎ | ・必要なデータだけを引数として渡す ・モジュール間が最小限の情報でつながっている ・fn(1, 'userName') |
| メッセージ結合 | ★5 | ◎ | ・引数なし ・最も望ましい結合度 ・fn() |
それでは各結合度の関数をどのように修正すべきか解説していきます!
内容結合(Content Coupling)
度合い:★1 評価:❌(最悪の結合)
- 他モジュールの 内部実装に直接アクセス している状態
- 例:他クラスの private 変数に直接アクセス、内部処理を強制的に呼ぶなど
- モジュール間の独立性が完全に失われ、変更の影響が即連鎖する
- バグの温床であり、リファクタもしにくい
- テストが困難(内部実装に依存するためモック不可・隠蔽不可)
現場で一番見る最悪の実装です
ex) 受け取った引数を直接変更する関数
実装修正例
// 内容結合(★1)
// 他クラスの内部状態を直接書き換える最悪パターン
function changeUser(user: User) {
user.status = "completed";
}
function operate(userId: string) {
const user = userAPi.getUser(userId) // ユーザー取得. User(status = process)
changeUser(user) // ここで勝手に引数で渡したuserが書き換えられる
if (user.status = 'process') { // 元のuserはprocessだったのに変更され想定外
...
}
}
// スタンプ結合(★4): 公開APIを通じて明示的に依頼する
function getChangedUser(user: User) {
const fixedUser = {...user} // 新規オブジェクト生成し操作を行う
fixedUser.status = "completed";
return fixedUser
}
function operate(userId: string) {
const user = userAPi.getUser(userId)
const fixedUser = getChangedUser(user)
if (user.status = 'process') { // 渡した引数が改ざんされていない!
...
}
}
共通結合(Common Coupling)
度合い:★2 評価:❌(避けるべき)
- 複数モジュールが 同一のグローバル変数 に依存
- どこで値が書き換わるかわからず、副作用の発生源が特定しづらい
- テストが不安定になりやすく、予期せぬ競合や状態破壊を招く
実装修正例
// 共通結合(★2)
let GLOBAL_CONFIG = { logLevel: "debug" };
function updateLevel() {
GLOBAL_CONFIG.logLevel = "info"; // どこからでも変更できてしまう
}
// データ結合(★5): 必要な部分だけ明示的に渡す
function updateLevel(config: { logLevel: string }) {
return { ...config, logLevel: "info" };
}
外部結合(External Coupling)
度合い:★2 評価:△(微妙)
- 複数モジュールが OS・環境変数・外部設定などの外部リソース に依存
- テストではモックが必要となり、環境依存のバグが発生しやすい
実装修正例
// 外部結合(★2)
function loadConfig() {
return process.env.APP_MODE; // 外部環境に直接依存
}
// データ結合(★5): 必要な値を渡す形に変更
function loadConfig(appMode: string) {
return appMode;
}
制御結合(Control Coupling)
度合い:★3 評価:△(望ましくない)
- 呼び出し元が フラグで挙動を指示 する状態
- “どう処理すべきか” が呼び出し元に依存するため結合が強い
- 関数内に「フラグによって処理を切り替える if 文」が増えて複雑化
実装修正例
// 制御結合(★3)
function process(type: "simple" | "detail") {
if (type === "simple") simplify();
else detailProcess();
}
// メッセージ結合(★5): 処理内で分岐要素を取得。引数による処理分岐を発生させない
function process() {
const dataType = getDataType() // "simple" | "detail"
if (type === "simple") {
simplify();
} else {
detailProcess();
}
}
スタンプ結合(Stamp Coupling / Data-Structured Coupling)
度合い:★4 評価:○(悪くない)
- モジュール間で 必要以上に大きなデータ構造 をやり取りする
- そのデータ構造のどの部分を使っているかが曖昧
- 意図しない依存関係が生まれやすい
この実装も現場で良く見られる
引数でオブジェクトそのまま渡している関数は要注意!
本当に全部のフィールドが関数処理に必要ですか?
実装修正例
// スタンプ結合(★4)
function printUser(user: User) {
console.log(user.profile.name); // user全体を渡す必要はない
}
// データ結合(★5): 必要なデータだけを渡す
function printUser(name: string) {
console.log(name);
}
データ結合(Data Coupling)
度合い:★5 評価:◎(望ましい)
- 必要なデータだけを引数として渡す
- 不要な依存がなく、関数が「単独でテストしやすい」
- 結合度としては理想的な状態
実装例
// データ結合(★5)
function updateName(userId: string, newName: string) {
updateUserName(userId, newName);
}
メッセージ結合(Message Coupling / No Coupling)
度合い:★5 評価:◎(最良)
- オブジェクト間のやり取りを メッセージ(メソッド呼び出し) のみに限定
- 内部構造を全く公開せず、変更に強く疎結合
- モジュール間の独立性が最も高い
実装例
// メッセージ結合(★5)
function initLogger() {
this.logger.init()
}
結合度 まとめ
依存関係は可能な限り減らす!!
余計な共有・フラグ制御・内部参照などが増えると、結合度が跳ね上がり保守不能になる!
結合度が高い実装は「どこを変えると何に影響するのか」把握できないので、
常に「依存を減らせないか?」「公開APIだけで完結できないか?」を意識すること!
以下3つの実装構造を参考に「結合度の低さ」を意識する!
可能な限り 3. メッセージ結合 の構造にできないか検討して実装すること!
// 1. スタンプ結合(★4 / ○): データ構造をまとめて渡す方式
// → やや重いが、まだ制御結合よりも保守しやすい
function printUserInfo(user: User) {
console.log(user.profile.name);
}
// 2. データ結合(★5 / ◎): 必要なデータだけを渡す疎結合
// → モジュール間の依存が最小限になる理想的な形
function updateUserName(userId: string, newName: string) {
updateUserNameInDB(userId, newName);
}
// 3. メッセージ結合(★5 / ◎◎ 最良):
// → オブジェクトに「何をしてほしいか」を伝えるだけ。内部実装を知らない。
// → 真の意味での疎結合。変更に強く、拡張性も高い。
user.changeName("newName");
まとめ
長文となってしまいすみません。。
凝集度 と 結合度 を意識するだけでコード品質は劇的に向上します。
- 仕様変更に強い
- 再利用しやすい
- テストが書きやすい
- レビューで指摘しやすい
- 初学者でも理解しやすい
という良い循環が生まれます。
ぜひ、明日からのコーディングに役立ててください!