Leapcell: ウェブホスティング、非同期タスク、およびRedisの次世代サーバレスプラットフォーム
Golangにおける依存性注入(DI)の探究
概要
本文は、Golangにおける依存性注入(DI)に関する内容を焦点に扱っています。最初に、典型的なオブジェクト指向言語であるJavaを援用してDIの概念を紹介し、初心者にとっての理解の手がかりを提供することを目的としています。本文の知識ポイントは比較的散在しており、オブジェクト指向プログラミングのSOLID原則や、様々な言語における典型的なDIフレームワークなどを網羅しています。
I. 序論
プログラミングの分野において、依存性注入は重要なデザインパターンの一つです。Golangにおけるその応用を理解することは、コードの品質、テスト可能性、保守性を向上させる上で極めて重要な意味を持っています。GolangにおけるDIをより良く説明するために、まず一般的なオブジェクト指向言語であるJavaからDIの概念を紹介します。
II. DI概念の分析
(I) DIの全体的な意味
依存性とは、何かに頼って支援を得ることを意味します。例えば、人々は携帯電話に大きく依存しています。プログラミングの文脈において、クラスAがクラスBの特定の機能を使用する場合、それはクラスAがクラスBに依存していることを意味します。Javaでは、別のクラスのメソッドを使用する前に、通常はそのクラスのオブジェクトを作成する必要があります(すなわち、クラスAはクラスBのインスタンスを作成する必要があります)。そして、オブジェクトを作成するタスクを他のクラスに任せ、直接依存関係を利用するプロセスが「依存性注入」です。
(II) 依存性注入の定義
依存性注入(DI)はデザインパターンの一つであり、Springフレームワークの核心概念の一つです。その主な機能はJavaのクラス間の依存関係を排除し、緩和結合を実現し、開発とテストを容易にすることです。DIを深く理解するために、まずそれが解決しようとする問題を理解する必要があります。
III. Javaコードの例を用いた一般的な問題とDIプロセスの説明
(I) 強結合の問題
Javaであるクラスを使用する場合、通常のアプローチはそのクラスのインスタンスを作成することです。以下のコードを参照してください:
class Player{
Weapon weapon;
Player(){
// Swordクラスと強く結合している
this.weapon = new Sword();
}
public void attack() {
weapon.attack();
}
}
このメソッドには結合が強すぎるという問題があります。例えば、プレイヤーの武器が固定的にソード(Sword)となっており、銃(Gun)に置き換えるのが難しいです。もしソードを銃に変更したい場合、関連するすべてのコードを修正する必要があります。コード規模が小さい場合は、これは大きな問題ではないかもしれませんが、コード規模が大きくなると、多くの時間とエネルギーを消費することになります。
(II) 依存性注入(DI)プロセス
依存性注入は、クラス間の依存関係を排除するデザインパターンです。例えば、クラスAがクラスBに依存する場合、クラスAはもはや直接クラスBを作成しません。代わりに、この依存関係は外部のxmlファイル(またはjava configファイル)に設定され、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 configファイル)に設定することができます。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に依存し、それらの間には強い結合があります。もし依存関係がクラスAがクラスBの親クラスB0に依存するように変更された場合、クラスAとクラスB0の依存関係の下で、クラスAはクラスB0の任意のサブクラスを使用することができます。このとき、クラスAとクラスB0のサブクラスの間の依存関係は緩和結合です。このように、依存性注入の技術的な基礎は多態性メカニズムとリフレクションメカニズムです。
(III) 依存性注入の種類
- コンストラクタ注入:依存関係はクラスのコンストラクタを通じて提供されます。
- セッター注入:インジェクタはクライアントのセッターメソッドを使用して依存関係を注入します。
- インターフェイス注入:依存関係は注入メソッドを提供し、それに渡された任意のクライアントに依存関係を注入します。クライアントはインターフェイスを実装する必要があり、このインターフェイスのセッターメソッドは依存関係を受け取るために使用されます。
(IV) 依存性注入の機能
- オブジェクトを作成する。
- どのクラスがどのオブジェクトを必要とするかを明確にする。
- これらすべてのオブジェクトを提供する。もしオブジェクトに何らかの変更が生じた場合、依存性注入は調査し、それがこれらのオブジェクトを使用するクラスに影響を与えないようにする。すなわち、将来オブジェクトが変更された場合、依存性注入はクラスに正しいオブジェクトを提供する責任を負う。
(V) 制御の逆転 - 依存性注入の背後にある概念
制御の逆転とは、あるクラスがその依存関係を静的に設定すべきではなく、他のクラスによって外部から設定されるべきことを意味します。これはS.O.L.I.Dの第五の原則に則っています - クラスは抽象化に依存すべきであり、特定のものに依存してはならない(ハードコーディングを避ける)。これらの原則に従って、あるクラスはそれ自身の責務を果たすことに集中すべきであり、その責務を果たすために必要なオブジェクトを作成することではなく、依存性注入が必要なオブジェクトを提供することに役割を果たします。
(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
のソースコードを見ると、以下のようになっています:
// 書き込まれたバイト数と、発生した書き込みエラーを返します。
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: ウェブホスティング、非同期タスク、およびRedisの次世代サーバレスプラットフォーム
最後に、Golangをデプロイするのに最適なプラットフォームをお勧めします:Leapcell
1. 多言語対応
- JavaScript、Python、Go、またはRustで開発できます。
2. 無料で無制限のプロジェクトをデプロイ
- 使用量に応じてのみ課金 — リクエストがなければ、課金はありません。
3. 抜群のコスト効率
- 実行量に応じた課金で、アイドル時の課金はありません。
- 例:平均応答時間60msで、$25で694万件のリクエストをサポートできます。
4. 合理化された開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- アクション可能なインサイトを得るためのリアルタイムメトリクスとロギング。
5. 簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を簡単に処理できる自動スケーリング。
- 運用オーバーヘッドゼロ — 開発に集中できます。
Leapcell Twitter: https://x.com/LeapcellHQ