依存性注入

Dependency Injection

この章のすべてのコードはここにあります

これにはインターフェースの理解が必要になるため、構造体のセクションをすでに読んでいることが前提です。

プログラミングコミュニティには、依存性注入に関する誤解がたくさんあります。

このガイドでは、

  • フレームワークは必要ありません

  • デザインが複雑になりすぎない

  • テストを容易にします

  • 優れた汎用関数を作成できます

hello-worldの章で行ったように、誰かに挨拶する関数を書きたいのですが、今回は actual Printing をテストします。

要約すると、その関数は次のようになります。

func Greet(name string) {
    fmt.Printf("Hello, %s", name)
}

しかし、これをどのようにテストできますか? fmt.Printfを呼び出すと stdout に出力されますが、テストフレームワークを使用してキャプチャするのはかなり困難です。

私たちがする必要があるのは、表示の依存関係を注入 (過ぎたるは及ばざるが如し)をできるようにすることです。 関数は気にする必要はありません _ 場所 _ または _ 方法 _ 表示が行われるため、 _ インターフェースを受け入れる必要があります_ 具体的なタイプではありません。

その場合は、実装を変更して表示するように制御し、テストできるようにします。 実際では、stdoutに書き込むものを注入します。

fmt.Printfのソースコードを見ると、フックする方法がわかります。

// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
    return Fprintf(os.Stdout, format, a...)
}

面白いですね! 内部では、 Printfos.Stdoutを渡して Fprintfを呼び出しているだけです。

os.Stdoutとは正確に何ですか? Fprintfは第1引数として何が渡されることを期待していますか?

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintf(format, a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

io.Writer

type Writer interface {
    Write(p []byte) (n int, err error)
}

さらに多くのGoコードを書くと、このインターフェイスが「このデータをどこかに置く」ための優れた汎用インターフェイスであるため、多くのポップアップが表示されます。

つまり、私たちは最終的に Writerを使用して挨拶をどこかに送信していることを知っています。この既存の抽象化を使用して、コードをテスト可能にし、再利用可能にします。

最初にテストを書く

func TestGreet(t *testing.T) {
    buffer := bytes.Buffer{}
    Greet(&buffer, "Chris")

    got := buffer.String()
    want := "Hello, Chris"

    if got != want {
        t.Errorf("got %q want %q", got, want)
    }
}

bytesパッケージのbufferタイプは Writerインターフェースを実装しています。

テストでこれを使用してWriterとして送信し、Greetを呼び出した後に何が書き込まれたかを確認できます。

テストを試して実行する

テストはコンパイルされません

./di_test.go:10:7: too many arguments in call to Greet
    have (*bytes.Buffer, string)
    want (string)

テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します

コンパイラを読んで、問題を修正してください。

func Greet(writer *bytes.Buffer, name string) {
    fmt.Printf("Hello, %s", name)
}

Hello, Chris di_test.go:16: got '' want 'Hello, Chris'

テストは失敗します。名前は出力されますが、標準出力になることに注意してください。

成功させるのに十分なコードを書く

テストでは、ライターを使用して挨拶をバッファに送信します。fmt.Fprintffmt.Printfに似ていますが、代わりに Writerを使用して文字列を送信しますが、fmt.Printfのデフォルトはstdoutです。

func Greet(writer *bytes.Buffer, name string) {
    fmt.Fprintf(writer, "Hello, %s", name)
}

テストに合格しました。

リファクタリング♪

以前のコンパイラーは、bytes.Bufferへのポインターを渡すように指示しました。これは技術的には正しいですが、あまり役に立ちません。

これを実証するために、Greet関数を標準出力に出力するGoアプリケーションに接続してみてください。

func main() {
    Greet(os.Stdout, "Elodie")
}

./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet

前に説明したように、fmt.Fprintfを使用すると、os.Stdoutbytes.Bufferの両方の実装がわかっているio.Writerを渡すことができます。

より汎用的なインターフェースを使用するようにコードを変更すると、テストとアプリケーションの両方で使用できるようになります。

package main

import (
    "fmt"
    "os"
    "io"
)

func Greet(writer io.Writer, name string) {
    fmt.Fprintf(writer, "Hello, %s", name)
}

func main() {
    Greet(os.Stdout, "Elodie")
}

io.Writerの詳細

io.Writerを使用してデータを書き込むことができる他の場所は何ですか? Greet関数はどれほど一般的な目的ですか?

インターネット

以下を実行します

package main

import (
    "fmt"
    "io"
    "net/http"
)

func Greet(writer io.Writer, name string) {
    fmt.Fprintf(writer, "Hello, %s", name)
}

func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
    Greet(w, "world")
}

func main() {
    http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
}

プログラムを実行し、http://localhost:5000に移動します。グリーティング機能が使用されているのがわかります。

HTTPサーバーについては後の章で説明しますので、詳細についてはあまり気にしないでください。

HTTPハンドラーを作成すると、 http.ResponseWriterと、リクエストの作成に使用された http.Requestが与えられます。サーバーを実装するときは、ライターを使用して応答を write します。

http.ResponseWriterio.Writerを実装していると思われるので、ハンドラー内で Greet関数を再利用できます。

まとめ

最初のコードは、制御できない場所にデータを書き込んだため、簡単にテストできませんでした。

テストによって動機付けされたコードをリファクタリングして、制御できるようにしました。 データを、依存関係を注入することによって書き込まれ、次のことが可能になりました:

Motivated by our tests we refactored the code so we could control where the data was written by injecting a dependency which allowed us to:

  • コードをテストする関数を簡単にテストできない場合は、通常、依存関係が関数またはグローバルな状態に組み込まれているためです。たとえば、ある種のサービス層で使用されているグローバルデータベース接続プールがある場合、テストが困難になる可能性が高く、実行が遅くなります。DIは、(インターフェイスを介して)データベースの依存関係を挿入するように動機付けし、テストで制御できるものでモックアウトできます。

  • 懸念事項を分離して、「データの移動先」と「生成方法」を分離します。メソッド/関数の責任が多すぎると感じた場合は、(データの生成、およびデータベースへの書き込み、HTTPリクエストの処理、およびドメインレベルのロジックの実行)おそらくDIが必要なツールになるでしょう。

  • コードをさまざまなコンテキストで再利用できるようにするコードを使用できる最初の「新しい」コンテキストは、テスト内です。しかし、さらに誰かがあなたの関数で何か新しいことを試したい場合、彼らは彼ら自身の依存関係を注入することができます。

モックにするのはどうなの? DIにも必要だそうですが、それも悪だそうです。

モックについては後で詳しく説明します(そしてそれは悪ではありません)。 モックを使用して、実際に注入するものを、テストで制御および検査できる偽バージョンに置き換えます。 私たちの場合でも、標準ライブラリには、使用する準備ができています。

Go標準ライブラリは本当に良いです。時間をかけて勉強してください。

このようにio.Writerインターフェースにある程度慣れていることで、テストでbytes.BufferWriterとして使うことができ、標準ライブラリの他のWriterを使ってコマンドラインアプリやウェブサーバで関数を使うことができます。

標準ライブラリに慣れるほど、これらの汎用インターフェイスが表示され、独自のコードで再利用して、ソフトウェアをさまざまなコンテキストで再利用可能にすることができます。

この例は、プログラミング言語Go, の章に大きく影響されているため、これを楽しんだ場合、是非買ってみてください!

最終更新