依存性逆転の原則(DIP)と依存性注入(DI)についての理解
正しく理解できていなかったと思うので、ちゃんと言語化してみたいと思い書きます。
依存性逆転の原則(DIP)とは
依存性逆転の原則または依存関係逆転の原則(dependency inversion principle)とは[1]、オブジェクト指向設計の用語であり、ソフトウェアモジュールの疎結合を確立する特別な形態を表現したコンセプトである。SOLIDの五原則の一つとして知られる。
オブジェクト指向における従来の依存関係とは、上位モジュールから下位モジュールへの方向性であり、仕様定義を担う上位モジュールを、詳細実装を担う下位モジュールから独立させて、各下位モジュールを別個保存するというものだったが、それに対して依存性逆転原則は以下二点を提唱している[2]。
1.上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。
2.抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。
wikipediaより
すごいざっくりですが、要は重要なプログラムが重要でないプログラムに依存しちゃダメよ、という設計手法だと理解しています。
依存性の注入とは
そもそも語源はDependency Injection
の直訳から来ているそうです。
具象に依存しているプログラムに対して、抽象を依存させることでテストをしやすくさせる、みたいなイメージです。
まあ上の依存性逆転の原則を実現する手法のような感じですかね。
実装について
今回は簡単な実装でイメージを具体化します。使用する言語はGo言語です。
りんごデータ(Apple)を受け取って、果物(fruits)として返却する処理を作るとします。
※あくまでイメージなので、動作確認等はしていません
依存性注入しないパターン
package main
import "fmt"
// AppleRepository は Apple データの取得
type AppleRepository struct{}
func (r AppleRepository) GetApple(product string) Apple {
return Apple{Name: product}
}
type Apple struct {
Name string
}
type Fruits struct {
Name string
}
type FruitService struct{}
// GetFruitsByApple は特定のAppleを取得してFruitsとして返却
func (s *FruitService) GetFruitsByApple(product string) Fruits {
repo := AppleRepository{}
apple := repo.GetApple(product)
return Fruits{Name: apple.Name}
}
func main() {
service := FruitService{}
fruits := service.GetFruitsByApple("Golden Delicious")
fmt.Println("果物 :: ", fruits.Name)
}
これでも動きますが、依存性注入をやってみたパターンも書いてみます。
(以下、依存性注入=DIと記載します)
依存性注入するパターン
package main
import "fmt"
// FruitRepository はフルーツデータの取得を抽象化したインターフェース
type FruitRepository interface {
GetApple(product string) Apple
}
// AppleRepository は FruitRepository の具体的な実装
type AppleRepository struct{}
func (r AppleRepository) GetApple(product string) Apple {
return Apple{Name: product}
}
type Apple struct {
Name string
}
type Fruits struct {
Name string
}
type FruitService struct {
repo FruitRepository
}
// NewFruitService は FruitService の新しいインスタンスを生成
func NewFruitService(repo FruitRepository) *FruitService {
return &FruitService{repo: repo}
}
// GetFruitsByApple は特定のAppleを取得してFruitsとして返却
func (s *FruitService) GetFruitsByApple(product string) Fruits {
apple := s.repo.GetApple(product)
return Fruits{Name: apple.Name}
}
func main() {
repo := AppleRepository{}
service := NewFruitService(repo)
fruits := service.GetFruitsByApple("Golden Delicious")
fmt.Println("果物 :: ", fruits.Name)
}
この処理結果自体は同じ内容が出力されます。
メリットは何か?
結論として、テストがやりやすくなるということと、変更が容易になるという点だと思います。
テストがやりやすくなる
DIをしていないコードでテストコードを書こうとすると、GetApple
での処理がどうしてもネックになってしまいます。
GetAppleが具象クラスに依存しているため、実装の本体があり、テスト上で振る舞いを任意に変えるのが難しいです。
テストできるようにするためには関数やインターフェースをオーバーライドしたり、複雑なリファクタをやらないといけないと思います。この例ではロジックが単純なため可能ですが、複雑なロジックだとかなりの労力を使ってしまいます。
そこでDIです。
DIしているコードはGetApple
は抽象になっています。なので実装はインターフェースごとに可能です。
つまり、GetApple
はテストでモックが作成できます。テストコード上でテストケースに合わせたモックを作ることができるので、GetFruitsByApple
全体をテストすることができます。
以下、テストコードのサンプルです。
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
type MockFruitRepository struct{}
// GetApple テストに必要な固定の値を返却
func (m *MockFruitRepository) GetApple(product string) Apple {
// このテストでは固定で"Test Apple"を返却
return Apple{Name: "Test Apple"}
}
// TestGetFruitsByApple は GetFruitsByApple
func TestGetFruitsByApple(t *testing.T) {
// モックの FruitRepositoryを作成
mockRepo := &MockFruitRepository{}
// モックを注入して FruitServiceインスタンスを作成
service := NewFruitService(mockRepo)
// テストを実行
result := service.GetFruitsByApple("Test Apple")
// テスト結果の確認
expected := Fruits{Name: "Test Apple"}
assert.Equal(t, expected, result, "The returned fruit should match the expected result.")
}
関数内に含まれる関数が抽象になっているので、その振る舞いをコントロールできるからテストができる、ということになります。
変更が容易になる
ビジネスロジックの核となる処理やモジュールが、様々な機能から参照されているとします。
その核となるロジックを変更した場合、それを参照している様々な機能が影響を受けてしまいます。
DIを用いることで、これらの影響を最小限に抑え、各機能をより独立して開発しやすくなります。
参考
Discussion