11
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

ドラ🔵もんで依存性の注入と依存性逆転の原則を理解する!!

Last updated at Posted at 2024-06-23

はじめに

依存性の注入(Dependency Injection, DI)は、習得するのは難しいですが、理解すると簡単にできるようになります。また、依存性の注入と間違えやすい概念として、SOLID原則の一つである依存性逆転の原則(Dependency Inversion Principle, DIP)があります。

今回は、依存性の注入と依存性逆転の原則という概念をドラ🔵もんで分かりやすく解説したいと思います。

ちなみにスライドもあります。(スライドにはコード例は載ってません)

依存性の注入(DI)

DIをドラ🔵もんで解説する

の○太はドラ🔵もんに依存しています。ドラ🔵もんを他のロボットに変えるとの🟡太は死にます。生死がドラ🔵もんにかかっていますが、の🟡太は誰にも相談しないので、誰もドラ🔵もんがの🟡太と関わっていることを知りません。この状態がDIしていない状態です。

image.png

上記のままだと、誰もの🟡太を助けようがありません。
そこで、の🟡太が周りに相談します。の🟡太がドラ🔵もんに依存しているということが周知となりました。

image.png

この状態が依存性を注入した状態です。

依存性の注入の誤解

  • DIをしたら、依存関係がなくなる → 依存関係は無くなりません
  • DIをしたので、テストや切り替えが可能となる → できる場合とできない場合がある

テストできる場合

抽象なものに依存しているときはテストできます。

例えば、

func main() {
    f()
}
func f() {
    hoge()
    foo()
}
func hoge() {
    return 1
}
func foo() {
    return 9
}

上記のコードをDIすると、

func main() {
 result := sum(hoge() + foo())
 fmt.Println(result)
}

func sum(a int, b int) int {
 return a + b
}

func hoge() int {
 return 1
}

func foo() int {
 return 9
}

となりますが、以下のようにテストができます。注入しているものがintなので、他のものに置換可能です。

func Test_sum(t *testing.T) {
 expected := 10
 actual := sum(2, 8)
 if actual != expected {
  t.Errorf("sum() = %v; want %v", actual, expected)
 }
}

テストできない場合

具体に依存していたら、モックができないのでテストできません。
例えば、先ほどのドラ🔵もんの例は置換ができません。(ドラ🔵もんの代わりになるものなんていない!)

結局DIとは何なのか?

DIとは、色々な意味で使用されることがありますが、以下の2つだと考えております。

狭義のDI

明示的に周りに何を必要とするかを宣言すること

広義のDI

単一責任の原則、リスコフの置換原則、Adapterパターン、Compositeパターンなど、様々な原則とパターンを集めたもの。

DIの目的

コード疎結合にして、保守容易性を向上させる。

依存性逆転の原則(DIP)

依存性逆転の原則をドラ🔵もんで解説する

の○太はドラ○もんに依存しています。ドラ🔵もんを他のロボットに変えるとの🟡太は死にます。

image.png

ドラ🔵もんは所詮四次⚪️ポケットであることに気づきます。

image.png

四次⚪️ポケットさえあればの○太は生きていけます。

image.png

ドラ○もんじゃなくても、四次⚪️ポケットを持っていれば、ぶっちゃけド🟠ミちゃんでも誰でもいいです。

image.png

つまり、ドラ🔵もんを代わりのものに置き換えることが可能となります。この状態が、依存関係の逆転した状態です。

依存性逆転の原則とは

具象から抽象へ依存させることで、依存関係を逆転すること。
つまり、interfaceや抽象クラスに依存させることによって、依存関係を逆転させ、実装の切り替えや置換を可能とします。

四次⚪️ポケットは具象ではないのか?

ここでは、ドラ🔵もん、ド🟠ミちゃんなどの「秘密道具を出す」という振る舞いを抽象化しているので、四次⚪️ポケットにしています。

上記の例で、より抽象的なポケットにしてしまうと、ズボンのポケットを持っている者だったら誰でもドラ🔵もんの代わりができてしまいます。

次に、ドラ🔵もんやド🟠ミちゃんを抽象化しものが、ロボットであった場合、ドラ🔵もんの代わりを鉄腕ア⚪︎ムやガ⚪︎ダムができるようになってしまいます。

つまり、「秘密道具を出す」という振る舞いに限定したいので、その 振る舞いに適した抽象度 である四次⚪️ポケットとしております。(四次⚪️ポケット以外でも SecretToolsSupplier とかでもいい)

実装例

実際に上記の例をGoで実装してみます。

DIしていない例

の🟡太がドラ🔵もんに依存しているのを周り(main)から見えていない状態です。

Go Playground

type Doraemon struct{}

func (d Doraemon) GetItem() (string, error) {
 return "暗記パン🍞", nil
}

func NewDoraemon() Doraemon {
 return Doraemon{}
}

// の🟡太がドラ🔵もんに依存している
type Nobita struct {
 doraemon Doraemon
}

func NewNobita() Nobita {
 doraemon := NewDoraemon()
 return Nobita{
  doraemon: doraemon,
 }
}

func (n Nobita) study() string {
 item, err := n.doraemon.GetItem()
 if err != nil {
  return "勉強できないよ〜😭"
 }
 return fmt.Sprintf("%sを使って勉強できた!", item)
}

func main() {
    // ドラ🔵もんがmainから見えていない
 nobita := NewNobita()
 result := nobita.study()
 fmt.Println(result)
}

時間があれば上記の一度テストをしてみてください。失敗テストを実装するのが困難だと思います。

DIした例

の🟡太がドラ🔵もんに依存していることを、周り(main)に明示的に宣言します。(NewNobita)

Go Playground

type Doraemon struct{}

func (d Doraemon) GetItem() (string, error) {
 return "暗記パン🍞", nil
}

func NewDoraemon() Doraemon {
 return Doraemon{}
}

// の🟡太はドラ🔵もんに依存している
type Nobita struct {
 doraemon Doraemon
}

// 外部からドラ🔵もん注入している
func NewNobita(doraemon Doraemon) Nobita {
 return Nobita{
  doraemon: doraemon,
 }
}

func (n Nobita) study() string {
 item, err := n.doraemon.GetItem()
 if err != nil {
  return "勉強できないよ〜😭"
 }
 return fmt.Sprintf("%sを使って勉強できた!", item)
}

func main() {
    // ドラ🔵もんはmainで作成できる
 doraemon := NewDoraemon()
 nobita := NewNobita(doraemon)
 result := nobita.study()
 fmt.Println(result)
}

さて、こちらも時間があれば上記の一度テストをしてみてください。DIしていない例と同様、失敗テストを実装するのが困難です。

依存関係の逆転

Go Playground

type FourDimensionalPocket interface {
 GetItem() (string, error)
}

type Doraemon struct {
}

func (d Doraemon) GetItem() (string, error) {
 return "暗記パン🍞", nil
}

func NewDoraemon() *Doraemon {
 return &Doraemon{}
}

// の🟡太は四次⚪️ポケットに依存する
type Nobita struct {
 pocket FourDimensionalPocket
}

func NewNobita(pocket FourDimensionalPocket) Nobita {
 return Nobita{
  pocket: pocket,
 }
}

func (n Nobita) study() string {
 item, err := n.pocket.GetItem()
 if err != nil {
  return "勉強できないよ〜😭"
 }
 return fmt.Sprintf("%sを使って勉強できた!", item)
}

func main() {
 doraemon := NewDoraemon()
 nobita := NewNobita(doraemon)
 result := nobita.study()
 fmt.Println(result)
}

さて、上記のテストは簡単にできます。

テストの例
// ドラ🔵もんをモックする
type mockDoraemon struct {
 want bool
}

func mockNewDoraemon(want bool) mockDoraemon {
 return mockDoraemon{want: want}
}

func mockNewNobita() FourDimensionalPocket {
 return mockDoraemon{}
}

func (d mockDoraemon) GetItem() (string, error) {
 if d.want {
  return "", assert.AnError
 }
 return "暗記パン🍞", nil
}

func TestNobita_study(t *testing.T) {
 tests := []struct {
  name    string
  want    string
  wantErr bool
 }{
  {
   name:    "Success Test",
   want:    "暗記パン🍞を使って勉強できた!",
   wantErr: false,
  },
  {
   name:    "Fail Test",
   want:    "勉強できないよ〜😭",
   wantErr: true,
  },
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   doraemon := mockNewDoraemon(tt.wantErr)
   nobita := NewNobita(doraemon)
   if got := nobita.study(); got != tt.want {
    t.Errorf("Nobita.study() = %v, want %v", got, tt.want)
   }
  })
 }
}

終わりに

DIや依存関係の逆転は間違えて使ってしまうと、かえって悪化させてしまう可能性があるので、適切な使い方で使えるようにしましょう。

ちなみに、今回はGoでコード例実装してみましたが、GoのinterfaceとJavaのinterfaceでは扱いが少し異なります。気になる方は、以下の記事を参考にしてください。

11
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?