DIについて概念はなんとなく知っていたけれど、具体的にどうやって実装(ライブラリを使ってどうやって実現するのか)はやったことがなかったのでGuiceを使って試してみた。
試してみたコードはこちら。
DIおさらい
おさらいとして、DIについてNotionAIにまとめてもらった。
Dependency Injection(DI)は、コンポーネント間の依存関係を外部から注入する設計パターンです。DIを使用することで、以下のような利点が得られます:
結合度の低減:コンポーネント間の依存関係を疎結合にすることができ、コードの保守性が向上します
テストの容易性:依存するコンポーネントをモックやスタブに置き換えることが容易になり、単体テストが書きやすくなります
再利用性の向上:コンポーネントが具体的な実装に依存せず、インターフェースに依存することで、コードの再利用性が高まります
設定の集中管理:依存関係の設定を一箇所にまとめることができ、アプリケーションの構成管理が容易になります
例えば、下記のコードではUserService
はUserRepository
というインターフェースに依存しており、具体的な実装(InMemoryUserRepository
)はDIコンテナによって注入されます。これにより、将来的に実装を変更する際も、UserService
のコードを変更する必要がありません。
実装
⚠️
ブログ上では
import
等省いている箇所があります前提
UserService
にUserRepository
をDIしたい
class UserService @Inject() (userRepository: UserRepository) {
def getUserBy(id: Long): Option[User] = userRepository.findBy(id)
}
UserRepository
とその実装はこんな感じ
trait UserRepository {
def findBy(id: Long): Option[User]
}
class InMemoryUserRepository @Inject() () extends UserRepository {
private val users = Map[Long, User](
1L -> User(1L, "Alice", 25),
2L -> User(2L, "Bob", 30),
3L -> User(3L, "Charlie", 35)
)
override def findBy(id: Long): Option[User] = users.get(id)
}
実装
bind
を使うパターン
まずは基本的なパターン
Module
(DIコンテナ)にインターフェースと実装の対応を定義する
class AppConfigModule extends AbstractModule {
override def configure(): Unit = bind(classOf[UserRepository]).to(classOf[InMemoryUserRepository])
}
UserRepository
というインターフェースにInMemoryUserRepository
という実装をバインドしている。
- 呼び出し
object Main extends App {
val injector = Guice.createInjector(new AppConfigModule)
val userService = injector.getInstance(classOf[UserService])
println("=== User Information ===")
println(s"User 1: ${userService.getUserBy(1L)}")
println(s"User 2: ${userService.getUserBy(2L)}")
println(s"User 3: ${userService.getUserBy(3L)}")
println(s"Unknown User: ${userService.getUserBy(999L)}")
}
/*
=== User Information ===
User 1: Some(User(1,Alice,25))
User 2: Some(User(2,Bob,30))
User 3: Some(User(3,Charlie,35))
Unknown User: None
*/
@Provides
を使うパターン
Module
(DIコンテナ)にインターフェースと実装の対応を定義する
class AppConfigModule extends AbstractModule {
// Using bind pattern
override def configure(): Unit = bind(classOf[UserRepository]).to(classOf[InMemoryUserRepository])
// Using @Provides pattern
@Provides
@Singleton
def provideAccountRepository(impl: InMemoryAccountRepository): AccountRepository = impl
}
@Provides
を使うことで、実装のプロバイダーを定義できる。↑の例では、provideAccountRepository
が、その実装としてInMemoryAccountRepository
を提供するようになる。
- 呼び出し
object Main extends App {
val injector = Guice.createInjector(new AppConfigModule)
val userService = injector.getInstance(classOf[UserService])
val accountService = injector.getInstance(classOf[AccountService])
println("=== User Information ===")
println(s"User 1: ${userService.getUserBy(1L)}, Account: ${accountService.getAccountBy(1L)}")
println(s"User 2: ${userService.getUserBy(2L)}, Account: ${accountService.getAccountBy(2L)}")
println(s"User 3: ${userService.getUserBy(3L)}, Account: ${accountService.getAccountBy(3L)}")
println(
s"Unknown User: ${userService.getUserBy(999L)}, Account: ${accountService.getAccountBy(999L)}"
)
}
/*
=== User Information ===
User 1: Some(User(1,Alice,25)), Account: Some(Account(1,1,1000))
User 2: Some(User(2,Bob,30)), Account: Some(Account(2,2,2000))
User 3: Some(User(3,Charlie,35)), Account: Some(Account(3,3,3000))
Unknown User: None, Account: None
*/
i.e. AccountRepository
をInjectしようとすると、GuiceがAccountRepository
のプロバイダーを探し、バインドされているInMemoryAccountRepository
をInjectしてくれる。