イントロダクション:疎結合を一言で言うと?🔗
疎結合(そけつごう)とは、「お互いに強く依存しすぎないように部品同士をつなぐこと」です。
あるクラスやモジュールを変更しても、他の場所への影響ができるだけ小さくなるようにする考え方で、
「変更に強い設計」を支える、とても大事な基礎用語です。
1. なぜ疎結合が大事なの?(現場でよく効く理由)
疎結合になっていると、システムをあとから変更したり、部品を差し替えたりするときに、とても動きやすくなります。
逆に、部品同士がベッタリつながった「密結合」のままだと、次のような困りごとが起きやすくなります。
-
ちょっとした修正で壊れやすい
あるクラスを直しただけなのに、思わぬ別の機能まで一緒に壊れてしまう。
-
テストしにくい
1つのクラスをテストしたいだけなのに、他のクラスや外部サービスが一緒に動いてしまい、単体テストが書きづらい。
-
置き換え・差し替えが難しい
DBや外部API、UIなどを変えたいときに、あちこちのコードを書き換えないといけなくなる。
疎結合を意識して設計しておくと、
「変更しやすい」「テストしやすい」「長く育てやすい」
システムに近づいていきます。
2. 疎結合と密結合の違い
ざっくりとしたイメージとして、次のように考えると分かりやすくなります。
-
密結合:
クラスAがクラスBの「具体的な実装」にベッタリ依存している状態。
Bの中身や使い方が変わると、すぐAも壊れてしまう。
-
疎結合:
AはBの「インターフェース(外から見える約束ごと)」だけを知っていて、
中身の細かい実装にはなるべく依存しないようにしている状態。
現場では、
「この2つ、ちょっと密結合すぎない?」
といった会話で使われることが多いです。
「お互いを知らなさすぎて困る」のではなく、
「ほどよい距離感でつながっている」
ことがポイントです。
3. 疎結合に近づけるための工夫 🧱
疎結合にするためのテクニックはいくつかありますが、代表的なものをいくつか挙げます。
インターフェースや抽象クラスをはさむ
具体的なクラスどうしを直接つなぐのではなく、
「このメソッドが呼べればOK」という抽象的な窓口
を用意してつなぐ方法です。実装を差し替えたいときも、そのインターフェースを満たすクラスを用意するだけで済みます。
依存の向きをそろえる(依存方向を意識する)
ビジネスロジックが、UIやDBなどの「外側の都合」に引きずられないように、
中心にあるルールほど、外側の詳しい仕組みに依存しない ようにします。
依存の向きをそろえておくと、設計が整理されて見通しも良くなります。
コンストラクタやDIコンテナで依存を渡す
クラスの中で直接 new してしまうのではなく、
必要な相手は外から注入してもらう(依存性の注入:DI)
ようにすると、テスト用のダミーや別実装に差し替えやすくなります。
どのテクニックも共通して、
「片方を変えても、もう片方への影響は最小限にしたい」
という目的に向かっています。
図解で見る「密結合」と「疎結合」
疎結合と密結合の違いを、UI・実装A / 実装B・DB の4つのモジュールで図解します。
同じ登場人物でも、結合のさせ方によって変更のしやすさ / 壊れやすさが大きく変わります。
疎結合は、モジュール同士がお互いの詳細をあまり知らず、
必要最小限の情報だけでゆるくつながっている状態です。
一方で密結合は、モジュール同士が直接ベタベタにつながり、
どこか1か所を変えると周りも巻き込んで壊れやすい状態を指します。
UI・実装A・実装B・DB が互いに直接参照し合い、矢印だらけでベタベタにつながった状態。
どこか1つを変えると、他のモジュールも連鎖的に修正が必要になりやすい構成です。
UI はインターフェースにだけ依存し、実装A/B と DB はインターフェース経由でゆるくつながる構成。
誰か1つの詳細を変えても、他のモジュールへの影響を小さく保ちやすくなります。
JavaScriptで見る「密結合」と「疎結合」の例
ユーザー登録時の「お知らせ送信」を題材に、フォームやメール送信の具体実装にベッタリ依存した密結合の例と、
インターフェースで責務を分離した疎結合の例を比較できるコードスニペットです。※コードのコメントを読むだけでも理解できるかと思います。
密結合の例:具体的なメール送信処理にベッタリ依存するコード
フォームDOMの扱い・ユーザー作成・メール文面の組み立て・送信APIの呼び出しまで、
1つの関数&特定のモジュールにくっついている例です。
通知方法を変えたいだけでも大きな修正が必要になります。
// 密結合の例:登録とメール送信がベッタリくっついている
// メール送信モジュール(具体的なAPIに直結している)
const emailSender = {
send(to, subject, body) {
// 実際には fetch や外部サービスSDKなどを叩く想定
console.log('メール送信:', { to, subject, body });
// fetch('/api/send-email', { ... }) のようなコードがここに来る
}
};
// フォーム送信イベントから直接呼ばれるハンドラ
function registerUserTightlyCoupled(formElement) {
// 1. フォームから直接DOM操作で値を取得
const name = formElement.querySelector('[name="name"]').value;
const email = formElement.querySelector('[name="email"]').value;
// 2. ユーザー作成ロジック(ここではローカルストレージに保存)
const users = JSON.parse(localStorage.getItem('users') || '[]');
const newUser = {
id: users.length + 1,
name,
email,
createdAt: new Date().toISOString()
};
users.push(newUser);
localStorage.setItem('users', JSON.stringify(users));
// 3. メール文面の組み立てもここで実施
const subject = 'ようこそ!' ;
const body =
newUser.name + ' さん、登録ありがとうございます!\n' +
'あなたの会員IDは ' + newUser.id + ' です。';
// 4. 特定のメール送信モジュールにベッタリ依存
emailSender.send(newUser.email, subject, body);
// 5. UI 更新もここでやってしまう
const message = document.getElementById('tightlyCoupledMessage');
message.textContent = '登録とメール送信が完了しました(密結合の例)';
}
疎結合の例:登録処理と通知処理をインターフェースで分離
「ユーザーを登録する処理」と「登録されたことを通知する処理」を切り離し、
通知側はインターフェース(契約)で受け取るようにした例です。
メール以外の通知方法に差し替えるのも簡単になります。
// 疎結合の例:登録処理と通知処理をインターフェースで分離
// ユーザーリポジトリ:保存だけに責務を限定
const userRepository = {
save(userInput) {
const users = JSON.parse(localStorage.getItem('users') || '[]');
const newUser = {
id: users.length + 1,
name: userInput.name,
email: userInput.email,
createdAt: new Date().toISOString()
};
users.push(newUser);
localStorage.setItem('users', JSON.stringify(users));
return newUser;
}
};
// 通知インターフェースを「満たす」オブジェクトの例1(メール通知)
const emailNotifier = {
notifyUserRegistered(user) {
const subject = 'ようこそ!';
const body =
user.name + ' さん、登録ありがとうございます!\n' +
'あなたの会員IDは ' + user.id + ' です。';
console.log('メール通知:', { to: user.email, subject, body });
}
};
// 通知インターフェースを「満たす」オブジェクトの例2(コンソール通知)
const consoleNotifier = {
notifyUserRegistered(user) {
console.log('コンソール通知: 新規ユーザー登録', user);
}
};
// 疎結合なユースケース:
// 「ユーザーを登録して、通知を飛ばす」という振る舞いだけを担当
function registerUser(userRepo, notifier, userInput) {
const user = userRepo.save(userInput);
// notifier が notifyUserRegistered(user) を持っていることだけを契約とする
notifier.notifyUserRegistered(user);
return user;
}
// UI 層:フォームとユースケースをつなぐ薄い部分
function handleRegisterUserLooselyCoupled(formElement) {
// フォームから値を取得
const userInput = {
name: formElement.querySelector('[name="name"]').value,
email: formElement.querySelector('[name="email"]').value
};
// ユーザーを登録して、通知を飛ばす ※ここで自由に通知機能を差し替え可能に
registerUser(userRepository, emailNotifier, userInput);
// 通知
const message = document.getElementById('looselyCoupledMessage');
message.textContent = '登録と通知が完了しました(疎結合の例)';
}
まとめ:疎結合を意識するときのポイント
-
疎結合とは、部品どうしが強く結びつきすぎないように設計する考え方です。
-
疎結合になっていると、変更・テスト・差し替えがしやすいシステムに近づきます。
-
インターフェースをはさむ、依存の向きをそろえる、DIで依存を注入する…といったテクニックは、
疎結合に近づけるための道具として使われます。
まずは「このコード、もう少し疎結合にできないかな?」と考えてみることが、
設計の質を一歩ずつ上げていくための練習になります。