Site cover image

fhhm’s blog

Guiceの素振り

DIについて概念はなんとなく知っていたけれど、具体的にどうやって実装(ライブラリを使ってどうやって実現するのか)はやったことがなかったのでGuiceを使って試してみた。

試してみたコードはこちら。

DIおさらい

おさらいとして、DIについてNotionAIにまとめてもらった。

Dependency Injection(DI)は、コンポーネント間の依存関係を外部から注入する設計パターンです。DIを使用することで、以下のような利点が得られます:

結合度の低減:コンポーネント間の依存関係を疎結合にすることができ、コードの保守性が向上します
テストの容易性:依存するコンポーネントをモックやスタブに置き換えることが容易になり、単体テストが書きやすくなります
再利用性の向上:コンポーネントが具体的な実装に依存せず、インターフェースに依存することで、コードの再利用性が高まります
設定の集中管理:依存関係の設定を一箇所にまとめることができ、アプリケーションの構成管理が容易になります

例えば、下記のコードではUserServiceUserRepositoryというインターフェースに依存しており、具体的な実装(InMemoryUserRepository)はDIコンテナによって注入されます。これにより、将来的に実装を変更する際も、UserServiceのコードを変更する必要がありません。

実装

⚠️
ブログ上ではimport等省いている箇所があります

前提

  • UserServiceUserRepositoryを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してくれる。