Golangにおける依存性注入(DI)の探求:ゼロからヒーローへ
Emily Parker
Product Engineer · Leapcell

Golangにおける依存性注入(DI)の探求
概要
この記事では、Golangにおける依存性注入(DI)に関連する内容に焦点を当てています。最初に、DIの概念を典型的なオブジェクト指向言語であるJavaを例に紹介し、初心者にも理解しやすいアプローチを目指します。この記事で扱う知識ポイントは比較的広範囲にわたり、オブジェクト指向プログラミングのSOLID原則や、さまざまな言語で一般的なDIフレームワークなどを網羅しています。
I. はじめに
プログラミングの分野において、依存性注入は重要なデザインパターンです。Golangでのその応用を理解することは、コードの品質、テスト容易性、および保守性を向上させる上で非常に重要です。GolangにおけるDIをより良く説明するために、まず一般的なオブジェクト指向言語であるJavaから始め、DIの概念を紹介します。
II. DIの概念の分析
(I) DIの全体的な意味
依存性とは、何かを頼ってサポートを得ることを意味します。たとえば、人々は携帯電話に非常に依存しています。プログラミングの文脈では、クラスAがクラスBの特定の機能を使用する場合、クラスAはクラスBに依存関係があることを意味します。Javaでは、別のクラスのメソッドを使用する前に、通常、そのクラスのオブジェクトを作成する必要があります(つまり、クラスAはクラスBのインスタンスを作成する必要があります)。そして、オブジェクトの作成タスクを他のクラスに渡し、依存関係を直接使用するプロセスが「依存性注入」です。
(II) 依存性注入の定義
依存性注入(DI)は、デザインパターンであり、Springフレームワークの中核概念の1つです。その主な機能は、Javaクラス間の依存関係を排除し、疎結合を実現し、開発とテストを容易にすることです。DIを深く理解するには、それが解決しようとしている問題を最初に理解する必要があります。
III. Javaコード例を用いた一般的な問題とDIプロセスの説明
(I) 密結合の問題
Javaでは、クラスを使用する場合、従来のアプローチはそのクラスのインスタンスを作成することです。以下のコードに示すように。
class Player{ Weapon weapon; Player(){ // Swordクラスと密結合 this.weapon = new Sword(); } public void attack() { weapon.attack(); } }
この方法には、結合が強すぎるという問題があります。たとえば、プレイヤーの武器が剣(Sword)として固定されており、それを銃(Gun)に交換するのが困難です。SwordをGunに変更したい場合は、関連するすべてのコードを変更する必要があります。コードの規模が小さい場合は、これは大きな問題ではないかもしれませんが、コードの規模が大きい場合は、多くの時間と労力を消費します。
(II) 依存性注入(DI)プロセス
依存性注入は、クラス間の依存関係を排除するデザインパターンです。たとえば、クラスAがクラスBに依存する場合、クラスAはクラスBを直接作成しなくなります。代わりに、この依存関係は外部のxmlファイル(またはjava構成ファイル)で構成され、Springコンテナは構成情報に従ってbeanクラスを作成および管理します。
class Player{ Weapon weapon; // weaponが注入される Player(Weapon weapon){ this.weapon = weapon; } public void attack() { weapon.attack(); } public void setWeapon(Weapon weapon){ this.weapon = weapon; } }
上記のコードでは、Weaponクラスのインスタンスはコード内で作成されず、コンストラクタを介して外部から渡されます。渡される型は親クラスWeaponであるため、渡されるオブジェクトの型はWeaponの任意のサブクラスにすることができます。渡される特定のサブクラスは、外部のxmlファイル(またはjava構成ファイル)で構成できます。Springコンテナは、構成情報に従って必要なサブクラスのインスタンスを作成し、Playerクラスに注入します。例は次のとおりです。
<bean id="player" class="com.qikegu.demo.Player"> <construct-arg ref="weapon"/> </bean> <bean id="weapon" class="com.qikegu.demo.Gun"> </bean>
上記のコードでは、<construct-arg ref="weapon"/>
のrefはid="weapon"
を持つbeanを指しており、渡されるweaponの型はGunです。Swordに変更したい場合は、次の変更を加えることができます。
<bean id="weapon" class="com.qikegu.demo.Sword"> </bean>
疎結合は、結合を完全に排除することを意味するものではないことに注意してください。クラスAはクラスBに依存しており、それらの間には密結合があります。依存関係が、クラスBの親クラスB0に依存するように変更された場合、クラスAとクラスB0の間の依存関係の下で、クラスAはクラスB0の任意のサブクラスを使用できます。このとき、クラスAとクラスB0のサブクラス間の依存関係は疎結合です。依存性注入の技術的基盤は、ポリモーフィズムメカニズムとリフレクションメカニズムであることがわかります。
(III) 依存性注入のタイプ
- コンストラクタインジェクション: 依存関係は、クラスのコンストラクタを介して提供されます。
- セッターインジェクション: インジェクタは、クライアントのセッターメソッドを使用して依存関係を注入します。
- インターフェースインジェクション: 依存関係は、それらを渡す任意のクライアントに依存関係を注入するための注入メソッドを提供します。クライアントはインターフェースを実装する必要があり、このインターフェースのセッターメソッドは依存関係を受け取るために使用されます。
(IV) 依存性注入の機能
- オブジェクトを作成します。
- どのクラスがどのオブジェクトを必要とするかを明確にします。
- これらすべてのオブジェクトを提供します。オブジェクトに何らかの変更が発生した場合、依存性注入は調査し、これらのオブジェクトを使用するクラスに影響を与えるべきではありません。つまり、オブジェクトが将来変更された場合、依存性注入はクラスに正しいオブジェクトを提供する責任があります。
(V) 制御の反転 - 依存性注入の背後にある概念
制御の反転とは、クラスはその依存関係を静的に構成するのではなく、他のクラスによって外部的に構成されるべきであることを意味します。これは、S.O.L.I.Dの5番目の原則に従います-クラスは具体的なものではなく抽象化に依存するべきです(ハードコーディングを避けてください)。これらの原則によれば、クラスは責任を果たすために必要なオブジェクトを作成するのではなく、自分の責任を果たすことに焦点を当てるべきです。これは、依存性注入が登場するところであり、クラスに必要なオブジェクトを提供します。
(VI) 依存性注入を使用する利点
- ユニットテストを容易にします。
- 依存関係の初期化はインジェクターコンポーネントによって完了するため、ボイラープレートコードを減らします。
- アプリケーションを簡単に拡張できるようにします。
- 疎結合の実現を支援します。これは、アプリケーションプログラミングにおいて非常に重要です。
(VII) 依存性注入を使用する欠点
- 学習プロセスは少し複雑であり、過度の使用は管理などの問題につながる可能性があります。
- 多くのコンパイルエラーは、ランタイムまで遅延されます。
- 依存性注入フレームワークは、通常、リフレクションまたは動的プログラミングを通じて実装されるため、「参照の検索」、「呼び出し階層の表示」、安全なリファクタリングなど、IDE自動化機能の使用を妨げる可能性があります。
依存性注入は、自分自身で実装することも、サードパーティのライブラリまたはフレームワークを使用して実現することもできます。
(VIII) 依存性注入を実装するためのライブラリとフレームワーク
- Spring (Java)
- Google Guice (Java)
- Dagger (Java and Android)
- Castle Windsor (.NET)
- Unity (.NET)
- Wire(Golang)
IV. Golang TDDにおけるDIの理解
Golangの使用中、多くの人が依存性注入について多くの誤解を抱いています。実際、依存性注入には多くの利点があります。
- フレームワークは必ずしも必要ではありません。
- 設計を過度に複雑にすることはありません。
- テストが簡単です。
- 優れた一般的な関数を記述できます。
誰かに挨拶する関数を例に取ります。実際に出力をテストすることを期待しています。初期の関数は次のとおりです。
func Greet(name string) { fmt.Printf("Hello, %s", name) }
ただし、fmt.Printf
を呼び出すと、コンテンツが標準出力に出力され、テストフレームワークを使用してキャプチャするのが困難です。このとき、出力の依存関係を注入する(つまり、「渡す」)必要があります。この関数は、どこにどのように出力するかを気にする必要がないため、特定の型ではなくインターフェースを受け取る必要があります。このようにして、インターフェースの実装を変更することで、出力されるコンテンツを制御し、テストを実現できます。
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...) }
Printf
内では、os.Stdout
を渡してFprintf
を呼び出すだけです。さらにFprintf
の定義を見ると、次のようになります。
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) }
io.Writer
は、「データをどこかに配置する」ためによく使用されるインターフェースです。これに基づいて、この抽象化を使用して、コードをテスト可能にし、再利用性を高めます。
(I) テストの記述
func TestGreet(t *testing.T) { buffer := bytes.Buffer{} Greet(&buffer,"Leapcell") got := buffer.String() want := "Hello, Leapcell" if got != want { t.Errorf("got '%s' want '%s'", got, want) } }
bytes
パッケージのbuffer
型は、Writer
インターフェースを実装しています。テストでは、それをWriter
として使用します。Greet
を呼び出した後、それを通じて書き込まれたコンテンツを確認できます。
(II) テストの実行の試み
テストの実行時にエラーが発生します。
./di_test.go:10:7: too many arguments in call to Greet
have (*bytes.Buffer, string)
want (string)
(III) テストが実行され、失敗したテスト出力を確認するための最小化されたコードの記述
コンパイラのプロンプトに従って、問題を修正します。変更された関数は次のとおりです。
func Greet(writer *bytes.Buffer, name string) { fmt.Printf("Hello, %s", name) }
この時点で、テスト結果は次のようになります。
Hello, Leapcell di_test.go:16: got '' want 'Hello, Leapcell'
テストが失敗します。name
は出力できますが、出力は標準出力に出力されることに注意してください。
(IV) 合格させるのに十分なコードの記述
writer
を使用して、テストのバッファに挨拶を送信します。fmt.Fprintf
はfmt.Printf
に似ています。違いは、fmt.Fprintf
は文字列を渡すためのWriter
パラメータを受け取るのに対し、fmt.Printf
はデフォルトで標準出力に出力することです。変更された関数は次のとおりです。
func Greet(writer *bytes.Buffer, name string) { fmt.Fprintf(writer, "Hello, %s", name) }
この時点で、テストに合格します。
(V) リファクタリング
最初に、コンパイラはbytes.Buffer
へのポインタを渡す必要があることを促しました。技術的には、これは正しいのですが、十分に一般的ではありません。これを説明するために、Greet
関数をGoアプリケーションに接続して、コンテンツを標準出力に出力します。コードは次のとおりです。
func main() { Greet(os.Stdout, "Leapcell") }
実行時にエラーが発生します。
./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet
前に述べたように、fmt.Fprintf
を使用すると、io.Writer
インターフェースを渡すことができ、os.Stdout
とbytes.Buffer
の両方がこのインターフェースを実装しています。したがって、より一般的なインターフェースを使用するようにコードを変更します。変更されたコードは次のとおりです。
package main import ( "fmt" "os" "io" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func main() { Greet(os.Stdout, "Leapcell") }
(VI) io.Writerの詳細
io.Writer
を使用することで、コードの一般性が向上しました。たとえば、データをインターネットに書き込むことができます。次のコードを実行します。
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
にアクセスすると、Greet
関数が呼び出されていることがわかります。HTTPハンドラーを記述するときは、http.ResponseWriter
とhttp.Request
を提供する必要があります。http.ResponseWriter
もio.Writer
インターフェースを実装しているため、Greet
関数はハンドラーで再利用できます。
V. 結論
最初のバージョンのコードは、制御できない場所にデータを書き込むため、テストが容易ではありません。テストに導かれて、コードをリファクタリングします。依存関係を注入することで、データの書き込み方向を制御でき、多くのメリットが得られます。
- コードのテスト: 関数がテストしにくい場合は、通常、関数の依存関係またはグローバル状態のハードリンクがあるためです。たとえば、サービス層がグローバルデータベース接続プールを使用している場合、テストが難しいだけでなく、実行も遅くなります。DIは、インターフェースを介してデータベースの依存関係を注入し、テストでモックデータを制御できるようにすることを提唱しています。
- 関心の分離: データの生成場所とデータの生成方法を分離します。特定のメソッド/関数が多すぎる機能(データを生成して同時にデータベースに書き込む、またはHTTPリクエストとビジネスロジックを同時に処理するなど)を実行していると感じる場合は、DIのツールを使用する必要があるかもしれません。
- 異なる環境でのコードの再利用: コードは、最初に内部テスト環境で適用されます。後で、他の人がこのコードを使用して新しい機能を試したい場合は、自分の依存関係を注入するだけで済みます。
Leapcell:Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォーム
最後に、Golangのデプロイに最適なプラットフォームをお勧めします:Leapcell
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発します。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストも料金もありません。
3. 比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万リクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOpsの統合。
- 実用的な洞察のためのリアルタイムメトリックとロギング。
5. 簡単なスケーラビリティと高性能
- 高い同時実行性を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドゼロ - 構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ