はじめに
普段はLaravelを使っているのですが、業務外でGo言語の学習を始め、現在アウトプットとしてオニオンアーキテクチャーでREST APIを実装しています。ただ、Go言語でDIを実現する方法が分からなかったので、今回調べてみた結果を記事にします。
この機会にそもそもDIとは?という部分も合わせて解説しているのでご覧いただけると嬉しいです。
対象読者
- DIって言葉はよく聞けけどあまりよく分かっていない方
- DIとDIコンテナの違いが分からない方
- Go言語でDIを実現する方法が知りたい方
- PHPからGo言語に乗り換え、オニオンアーキテクチャで設計しようとしている方
そもそもDIとは
DIの定義
まずはDIとは何か?ということについて触れていきます。Wikipediaによると、
依存性の注入(いぞんせいのちゅうにゅう、英: Dependency injection)とは、あるオブジェクトや関数が、依存する他のオブジェクトや関数を受け取るデザインパターンである。英語の頭文字からDIと略される。DIは制御の反転の一種で、オブジェクトの作成と利用について関心の分離を行い、疎結合なプログラムを実現することを目的としている。
dependencyを「依存性」と訳すのは本来の意味[1] から外れているため「依存オブジェクト注入」の用語を採用する文献も複数存在する。
といった風に記載されています。
つまりDIとは、保守性(変更容易性)の高い疎結合なプログラムを作ることを目的にしており、その目的を達成するために、依存しているオブジェクトを外部から注入します。これにより依存方向を逆転させ、オブジェクトの作成と利用について関心の分離を実現するデザインパターンであると言えます。
例えば
オニオンアーキテクチャのインフラストラクチャーとユースケースの部分を切り出して考えてみます。
これはユースケース層がインフラストラクチャー層に依存してしまっている状態であり、DBなどの下位モジュールの変更に上位モジュールであるユースケースが影響されてしまっています。
そこで、DIを使います。
このように依存方向を逆転させることで、ユースケース層はインフラストラクチャー層に依存することがなくなり、疎結合な作りになります。これにより、モックのリポジトリを使えたり、テストがしやすくなったりとさまざまな恩恵を受けられます。
ちなみに、ソフトウェア開発の原則であるSOLIDに依存性逆転の原則というものがありますが、依存性逆転の原則を実現するための手段として、DIがある、という認識です。
また、DIコンテナという言葉もよく混同されがちですが、DIはパターンの1つであり、DIコンテナはそのパターンを実現するためのフレームワークです。
DIを用いることのメリット4選
DIには下記4つのメリットがあります。
- ソフトウエアの階層をきれいに分離した設計が容易になる
- コードが簡素になり,開発期間が短くなる
- テストが容易になり,「テスト・ファースト」による開発スタイルを取りやすくなる
- 特定のフレームワークへの依存性が極小になるため,変化に強いソフトウエアを作りやすくなる(=フレームワークの進化や,他のフレームワークへの移行に対応しやすくなる)
※ こちらの記事を参照
DIがない時と、DIがある時のコードの違い
ここからはGo言語を用いてDIのコードを実装していきます。先ほど用いた
- リポジトリ(DBとのやりとりをする層)
- ユースケース(リポジトリを呼び出す層)
を使用していきます。
DIがない時
まずはDIを使用した時との違いを明確にするために、DIを用いずにユースケースがリポジトリに依存してしまっている例から実装していきます。
ユースケース
まずはユースケース層です。下記実装から見ても分かるようにユースケースはリポジトリに依存しています。上位モジュールは下位モジュールに依存するべきではないので、この実装はもちろんNGパターンです。
package UseCases
type GetUserUseCase struct {
repository Repositories.UserRepository
}
func NewGetUserUseCase() *GetUserUseCase {
// リポジトリを生成(依存の原因はここ)
repository := Repositories.NewUserRepository()
return &GetUserUseCase{repository}
}
func (useCase *GetUserUseCase) GetUser(userId Domains.userId) UserDto {
user := useCase.repository.FindById(userId)
// DTOの生成と返却処理
}
リポジトリ
続いてリポジトリです。
package Repositories
type UserRepository struct {}
func NewUserRepository() *UserRepository {
return &UserRepository{}
}
func (repository *UserRepository) FindById(userId UserId) Domains.User {
// 取得処理
}
DIがある時
ここからは先ほどの「ユースケースがリポジトリに依存している問題」をDIにより解消していきます。
DIには注入の方法にいくつか種類がありますが、今回は一番頻繁に使用されているコンストラクタインジェクションに焦点を当てて先ほどのコードを修正してきます。
リポジトリ
リポジトリではインターフェースを定義します。そしてコンストラクタで返却するオブジェクトの型もインターフェースに変更します。
package Repositories
type UserRepositoryInterface interface {
FindById(userId Domains.UserId) Domains.User
}
type userRepository struct {}
func NewUserRepository() UserRepositoryInterface {
return &UserRepository{}
}
func (repository *UserRepository) FindById(userId UserId) Domains.User {
// 取得処理
}
ユースケース
package UseCases
type GetUserUseCase struct {
repository Repositories.UserRepositoryInterface
}
// 依存していたオブジェクト(リポジトリ)を外部から注入するようにしている
func NewGetUserUseCase(repository Repositories.UserRepositoryInterface) *GetUserUseCase {
return &getUserUseCase{repository}
}
func (useCase *GetUserUseCase) GetUser(userId Domains.userId) UserDto {
user := useCase.repository.FindById(userId)
// DTOの生成と返却処理
}
コンストラクタで先ほど依存していたリポジトリを外部から注入するようにしています。また、リポジトリのインターフェースを
このようにDIを活用することで、ユースケースがリポジトリに依存しないようになります。
ちなみにコントローラーはユースケースに依存しても問題ないので、ユースケースのインターフェースは使用していません。
最後に念の為、これらを呼び出す側の実装もしていきます。今回はユーザーコントローラーがユースケースを呼び出すこととします。
package Controllers
type UserController struct {}
func (controller *UserController) GetUser(context *gin.Context) {
userIdInt, _ := strconv.Atoi(context.Param("userId"))
userId := Domains.UserId{Value: userIdInt}
// リポジトリの実体を生成
repository := Repositories.NewUserRepository()
// リポジトリの実体をユースケースのコンストラクタに注入
useCase := User.NewGetUserUseCase(repository)
userDto := useCase.GetUser(userId)
context.JSON(http.StatusOK, gin.H{
"user": map[string]any{
"userId": userDto.Id,
"firstName": userDto.FirstName,
"lastName": userDto.LastName,
},
})
}
終わりに
以上でGo言語でDIを実現する方法は終わりです。
今後はGo言語のDIライブラリであるgoogleのwireなどを導入して、実装してみたいと思います。
何かご指摘等あればお気軽にコメントください!!