依存性逆転の原則(DIP)と依存性注入(DI)の活用
ソフトウェア開発において、依存性逆転の原則(Dependency Inversion Principle, DIP)と依存性注入(Dependency Injection, DI)は、コードの品質を向上させるための重要な概念です。これらの原則は、以下の利点をもたらします:
- コードの保守性の向上
- システムの拡張性の強化
- クラス間の依存関係の柔軟な管理
本セクションでは、DIPとDIの基本概念を説明し、これらの原則をどのように適用してクラス間の結合度を低減し、より柔軟なソフトウェア設計を実現できるかを探ります。
シンプルなシナリオ
まずはシンプルなシナリオを考えてみます。二つの異なるパッケージ(通知サービスと電子メール送信)があり、それぞれにいくつかの構造体が存在しています。通知サービスの構造体は電子メール送信の構造体に依存しているという状況を考えてみます。
具体的には、通知サービスパッケージのNotificationService
が電子メール送信パッケージのEmailSender
に依存しているとしましょう。この依存関係を依存性逆転の原則を用いて解決し、依存性注入によって柔軟な設計を実現します。
package notification
import "email" // emailsenderパッケージに依存している
// 通知サービスパッケージに含まれる構造体
type Service struct {
Sender email.Sender // emailsenderパッケージの構造体に依存している
}
func NewService(sender email.Sender) *Service {
return &Service{Sender: sender}
}
func (n *Service) Notify(message string) {
// 依存対象のメソッドを使って処理
n.Sender.Send(message)
}
package email
import "fmt"
// EmailSender構造体
type Sender struct{}
func (e *Sender) Send(message string) {
// 実装内容
fmt.Println("Sending email:", message)
}
package main
import (
"fmt"
"notification"
"email"
)
func main() {
// 依存性注入を使用してnotification.Serviceを生成
es := &email.Sender{}
notificationService := notification.NewService(es)
// notification.Serviceを使って処理を実行
notificationService.Notify("Hello, World!")
// 出力: Sending email: Hello, World!
}
mainの中でnotification.Service
のコンストラクタを呼ぶ際に依存するemail.Sender
を渡しています。このようにあるモジュールが依存するモジュールを埋め込むことを依存性の注入 (Dependency Injection, DI) といいます。今回のようにコンストラクタを使って依存性を注入するパターンはコンストラクタインジェクションと言います。
依存性逆転の原則の活用
依存性逆転の原則とはオブジェクト指向設計におけるSOLID原則というものの一つで、ロバート・C・マーティンが主要な提唱者の一人です。
依存性逆転の原則は、高レベルのモジュールは低レベルのモジュールに依存してはならず、両者は共に抽象に依存すべきであると述べています。これについて以下のコード例を用いて説明します。
この原則を適用するために、まずインターフェースを定義してみます。
package notification
// emailsenderパッケージのimportが無くなる
// 代わりにMessageSenderインターフェースを定義する
type MessageSender interface {
Send(message string)
}
// 通知サービスの構造体
// 実装ではなくて抽象(インターフェース)に依存させる
type Service struct {
Sender MessageSender
}
func NewService(sender MessageSender) *Service {
return &Service{Sender: sender}
}
func (n *Service) Notify(message string) {
// 依存対象のメソッドを使って処理
n.Sender.Send(message)
}
package email
import "fmt"
// EmailSender構造体
type Sender struct{}
func (e *Sender) Send(message string) {
// 実装の詳細
fmt.Println("Sending email:", message)
}
package main
import (
"fmt"
"notification"
"email"
)
func main() {
// 依存性注入を使用してnotification.Serviceを生成
es := &email.Sender{}
notificationService := notification.NewService(es)
// notification.Serviceを使って処理を実行
notificationService.Notify("Hello, World!")
// 出力: Sending email: Hello, World!
}
上記のコードにおいてnotification.Service
は、emainl.Sender
の具体的な実装ではなく、MessageSender
というインターフェースをメンバとしてもち、それに依存しています。
また逆にemail.Sender
はMessageSender
インターフェースを実装することでインターフェースの契約を満たすことになります。
つまり二つのモジュールがそれぞれインターフェースという抽象的な機能に依存するように実装することができました。このような、具体的な実装ではなく抽象的なインターフェースにのみ依存するように設計するテクニックを依存性逆転の原則 (Dependency Inversion Principle, DIP) といいます。
これによって、notification
パッケージは通知処理の実装の詳細を知る必要がなくなり、パッケージ間の疎結合性が実現されます。実際notification
パッケージからはemail
パッケージのimportが排除され、パッケージ間の依存がなくなりました。
DIPのメリット、デメリット
DIPを採用することによるメリットは以下のようになります。
-
変更の容易性: 依存されるオブジェクトの内部の実装を変更した際に、依存する側のコードに大きな影響を与えることなく変更が行えます。具体的な実装の詳細が隠蔽されているため、変更に伴う修正範囲が限定され、バグの発生リスクが低減します
-
ユニットテストの容易性: テスト時には、依存オブジェクトの代わりにテストダブル(モック)を使用してテスト対象のテストを行うことができます。これにより、パッケージBの実装に依存することなく、パッケージAの振る舞いを確認するテストが行えます
-
拡張性の向上: 新しい実装を追加したり、既存の実装を置き換えたりする際に、新しい構造体をインターフェースを実装する形で追加するだけで済みます。既存のクラスへの影響が最小限に抑えられるため、システム全体の拡張性が向上します
以上のように、DIPに従うことによって、システムの保守性、拡張性、テスト容易性、再利用性などが向上します。上位モジュールは抽象に依存しているため、モジュールの外部から依存する実態を切り替えてDIすることが可能になるからです。したがって依存性逆転の原則と依存性注入の活用によって、これらのメリットを実現し、高品質なソフトウェアを開発することが可能になります。めでたい。
依存性逆転の原則に基づいた設計には上に書いたようなメリットがありますが、モジュールが抽象に依存するのでそのモジュールが実際に依存先の機能を使用するためには、外部からその依存を注入する必要があります。
そのためにはコンストラクタや他のメソッドのシグニチャに依存を注入するための注入口を用意する必要が生じます。これによって、コードの複雑性が上がる場合もあるため注意が必要です。
とくに変更の容易性や拡張性の向上を意識しすぎて、インターフェースを追加しすぎると簡潔性が損なわれGoらしいコードでは無くなってしまうかもしれません。Goのインターフェースについては、他の言語と決定的に違う部分があるので注意が必要です。詳しくは『Go 100 mistakes』等のサイトを参照してください。
GoらしいコードについてはGoogleのStyle Guideを参照するとよいでしょう。
おまけ(DIP絵描き歌)
では最後はDIP絵描き歌を歌ってお別れです。
...
♪サービス二つありました(あ、よいしょ)
♪依存が一つありまして(あ、どした)
♪インターフェース依存元に寄せまして(それで?)
♪email.SenderはMessageSenderの実装なので、依存性逆転の出来上がり(わーい)
...最後までお付き合いいただきありがとうございました😊
参考URL