依存関係逆転の法則(Dependency Inversion Principle)とは
依存関係逆転の法則とは、ソフトウェア設計において「上位モジュール(ビジネスロジックなど)が下位モジュール(詳細な実装、たとえばデータベースや外部サービスなど)に依存しないようにし、両者とも“抽象(インターフェースや抽象クラス)”に依存するべきだ」という原則です。これにより、システムは柔軟性・拡張性・保守性が高くなり、変更に強くなります。
2 つのポイント
- 上位モジュールも下位モジュールも、具体的な実装ではなく抽象(インターフェースなど)に依存する
- 実装の詳細(下位モジュール)が抽象に従うようにする(=依存の向きを逆転させる)
具体例で理解する
1. 具象に依存している悪い例
たとえば「メールを送る」機能を考えます。
// EmailSender はメール送信を担当する構造体
type EmailSender struct{}
// SendEmail はメールを送信するメソッド
func (e *EmailSender) SendEmail(message string) {
// メール送信処理
fmt.Printf("メールを送信中: %s\n", message)
}
// NotificationService は通知サービスを提供する構造体
type NotificationService struct {
emailSender *EmailSender
}
// NewNotificationService はNotificationServiceの新しいインスタンスを作成
func NewNotificationService() *NotificationService {
return &NotificationService{
emailSender: &EmailSender{},
}
}
// Send はメッセージを送信するメソッド
func (n *NotificationService) Send(message string) {
n.emailSender.SendEmail(message)
}
// 使用例
func main() {
service := NewNotificationService()
service.Send("テストメッセージです")
}
この例では、NotificationService
がEmailSender
という具体的な実装に直接依存しています。もし今後、SMS 送信や他の通知手段を追加したい場合、NotificationService
のコードを修正しなければなりません。
2. 抽象に依存させる良い例(依存関係逆転の法則の実践)
// MessageSender はメッセージ送信のインターフェース
type MessageSender interface {
Send(message string)
}
// EmailSender はメール送信を実装する構造体
type EmailSender struct{}
// Send はMessageSenderインターフェースを実装
func (e *EmailSender) Send(message string) {
// メール送信処理
fmt.Printf("メールを送信中: %s\n", message)
}
// NotificationService は通知サービスを提供する構造体
type NotificationService struct {
messageSender MessageSender
}
// NewNotificationService はNotificationServiceの新しいインスタンスを作成
func NewNotificationService(messageSender MessageSender) *NotificationService {
return &NotificationService{
messageSender: messageSender,
}
}
// Send はメッセージを送信するメソッド
func (n *NotificationService) Send(message string) {
n.messageSender.Send(message)
}
// 使用例
func main() {
// EmailSenderを作成
emailSender := &EmailSender{}
// NotificationServiceに依存性を注入
service := NewNotificationService(emailSender)
// メッセージを送信
service.Send("テストメッセージです")
}
このように、NotificationService
はMessageSender
という抽象(インターフェース)に依存し、EmailSender
はその実装を担います。今後、SmsSender
など新しい通知手段を追加しても、NotificationService
のコードを変更せずに済みます。
以下は、具体的に、SmsSender
メソッドを追加した場合のコードです。
// MessageSender はメッセージ送信のインターフェース
type MessageSender interface {
Send(message string)
}
// ----------------------- 既存の実装 -----------------------
type EmailSender struct{}
func (e *EmailSender) Send(message string) {
fmt.Printf("メールを送信中: %s\n", message)
}
// ----------------------- 追加する実装 ----------------------
type SmsSender struct{}
func (s *SmsSender) Send(message string) {
// SMS 送信処理(ここではダミー)
fmt.Printf("SMS を送信中: %s\n", message)
}
// -------------------- 共通サービス層 -----------------------
type NotificationService struct {
messageSender MessageSender
}
func NewNotificationService(sender MessageSender) *NotificationService {
return &NotificationService{messageSender: sender}
}
func (n *NotificationService) Send(message string) {
n.messageSender.Send(message)
}
// ------------------------- 使用例 --------------------------
func main() {
// 1) Email で通知
emailSender := &EmailSender{}
emailService := NewNotificationService(emailSender)
emailService.Send("メールのテストメッセージです")
// 2) SMS で通知
smsSender := &SmsSender{}
smsService := NewNotificationService(smsSender)
smsService.Send("SMS のテストメッセージです")
}
日常的なたとえ
1️⃣ 具象(具体的なモノそのもの)に依存している例
イメージとしては、 “その道具・方法しか使えない” 状態です。
シーン | 説明 | 困るポイント |
---|---|---|
➊ 乾電池式のおもちゃが “単三電池” 専用 | 取扱説明書には「このおもちゃは単三電池 2 本のみ対応」と書いてある。単四や充電池は入らない。 | 単三が切れたら遊べない。別規格が増えるたびに製品を買い替え or 改造が必要。 |
➋ コーヒーメーカーが “専用カプセル” だけを認識 | 機械内部のセンサーはメーカー純正カプセルの形状だけを前提に作られている。 | 新しい味のカプセルが出ても、形が少し違うとエラーになり抽出できない。 |
➌ スマートフォン用アクセサリが “Lightning 端子” 固定 | ケーブル・ドック・マイク…すべて Lightning でしか給電・通信できない設計。 | 端末が USB-C に変わった瞬間、全部使えなくなる。 |
「特定の電池・カプセル・コネクタ = 具体的実装」にロックインされていると 道具が増える or 仕様が変わるたびに本体側を作り直さなければならない。
これが、具象に依存している例です。
2️⃣ 抽象(共通ルール・インターフェース)に依存している例
イメージとしては、 “ルールさえ守れば何でも OK” な状態です。
シーン | 説明 | うれしいポイント |
---|---|---|
➊ コンセント(AC100 V 50/60 Hz)を使う家電 | ドライヤー・炊飯器・PC などは「家庭用 100 V の壁コンセント」というルールだけを前提に作る。 | メーカーも形も違う家電を自由に差し替えられる。将来、AI 搭載炊飯器が出てもコンセントはそのまま。 |
➋ “交通系 IC カード” 対応改札機 | Suica、PASMO、ICOCA… すべて “FeliCa タッチで残高を引く” というインターフェースを共有。 | 新しい地域カードが増えても改札機はソフト追加だけで対応。ハードを一から作り替えなくて済む。 |
➌ 汎用サイズのネジ穴(1/4-20 UNC)の三脚 | カメラでもスマホホルダーでも、同じネジ規格を守れば載せ替え可能。 | 新しいカメラが発売されても三脚本体は継続利用。周辺機器のバリエーションが豊富に。 |
「壁コンセント・IC 乗車券・ネジ規格 = 抽象インターフェース」にロックインされていると 新しい製品が増えても “共通ルール” さえ守れば双方そのまま使える。
これが、抽象に依存している例です。
違いを一言で
観点 | 具象依存 | 抽象依存 |
---|---|---|
依存先 | “単三電池” のように 特定品そのもの | “乾電池 1.5 V” のような 共通仕様 |
変化への強さ | 容易に壊れる・作り直しが必要 | 追加・交換が容易 |
開発者/利用者の自由度 | 限定的(ロックイン) | 拡張しやすくイノベーションが起きやすい |
DIP を意識すると日常でもソフトウェアでも「長く使える・後で組み替えられる」設計ができ、“選択肢が増えても主役(高レベル)は迷わない” 世界が実現します。
なぜ「逆転」なのか
項目 | ふつう(逆転前) | 依存関係“逆転”後 |
---|---|---|
コードの層 | 高レベル(ビジネスロジック)が ↓ 低レベル(具体的処理)を直接呼ぶ | 低レベルが ↑ 共通の“ルール(抽象)”に合わせる |
物理的な依存(import / include) | 高レベル → 低レベル | 低レベル → 抽象 ← 高レベル |
つまり、本来“上”が“下”を見に行っていた依存の向きを、“下”が“上のルール”に合わせに行く形へひっくり返すからです。
まとめ
- 依存関係逆転の法則とは、「具体的な実装」ではなく「抽象(インターフェース)」に依存することで、柔軟で変更に強い設計を実現する原則です。
- これにより、上位モジュールも下位モジュールも抽象に依存し、実装の入れ替えや拡張が容易になります。
参考文献
1. https://qiita.com/k2491p/items/686ee5dd72b4baf9a81a
2. https://ja.wikibooks.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87
3. https://qiita.com/hirodragon/items/090467284d19a4cbafa0
4. https://qiita.com/phenan/items/a8030b164d95a035a5dc
5. https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87
6. https://zenn.dev/yoshinani_dev/articles/c743a3d046fa78
7. https://emb-sw-eng.com/solid_d/
8. https://zenn.dev/chida/articles/e46a66cd9d89d1
9. https://blog.openreplay.com/ja/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87-%E8%AA%AC%E6%98%8E/
10. https://rookie-programmer.jp/?p=75
11. https://qiita.com/marienplatz/items/1e3fe7597afc75b22399
12. https://qiita.com/zizynonno/items/6bf71d73d790e27fb9ee
13. https://yuru-uni.com/2023/04/23/solid_principle_d/
14. https://blog.chiyuu.co.jp/2024/06/06/dependency-inversion-principle/
15. https://plainprogram.com/dependency-inversion-principle/