LoginSignup
18
11

More than 3 years have passed since last update.

NimでインターフェースとDIコンテナを使う

Last updated at Posted at 2021-01-26

この記事は、Nim Advent Calendar 2020 その2 の2日目です(埋めていくことにしました)。

NimでインターフェースとDIコンテナを使う方法を完全に発見したので共有します。

インターフェースって何?

めちゃくちゃ端折って言うと、アプリケーションの外部とデータをやり取りする時に、「どことやり取りするか」をアプリケーション内部が知らなくてもいいようにするものです。
外部とデータをやり取りするとは、具体的にはRDBやNoSQLや外部APIやJSONファイルってこと。

例えばユーザーデータを保存するプログラムがあるとします。

user_entity.nim
type User* = ref objct
  id*: int
  name*: string
  email*: string
  isAdmin*: bool

RDBに保存すると

repositories/user_rdb_repository.nim
import ../user_entity

type UserRdbRepository* = ref object

proc newUserRdbRepository*():UserRdbRepository =
  return UserRdbRepository()

proc save*(self:UserRdbRepository, user:User) =
  rdb().table("users").insert(%*{
    "id": user.id,
    "name": user.name,
    "email": user.email,
    "is_admin": user.isAdmin
  })

Jsonファイルに保存すると

repositories/user_json_repository.nim
import ../user_entity

type UserJsonRepository* = ref object

proc newUserJsonRepository*():UserJsonRepository =
  return UserJsonRepository()

proc save*(self:UserJsonRepository, user:User) =
  let userJson = %*{
    "id": user.id,
    "name": user.name,
    "email": user.email,
    "is_admin": user.isAdmin
  }
  let f = open("sample.json", FileMode.fmWrite)
  f.write(userJson.pretty())
  f.close()

呼び出し元では

user_usecase.nim
import user_entity
import repositories/user_rdb_repository
import repositories/user_json_repository

type UserUsecase* = ref object

proc save*(self:UserUsecase) =
  let user = User(id:1, name:"user1", email:"user1@nim.com", isAdmin:false)
  # RDBに保存する時
  newUserRdbRepository().save(user)
  # JSONファイルに保存する時
  newUserJsonRepository().save(user)

になります。
しかし、呼び出し元では保存先がRDBかJSONファイルかどこなのかは関心事ではありません。「どっか知らんけど保存する」にしたいわけです

↓こんな風にしたい

user_usecase.nim
proc save*(self:UserUsecase) =
  let user = User(id:1, name:"user1", email:"user1@nim.com", isAdmin:false)
  # どっか知らんけど保存する
  newUserRepository().save(user)

この時、UserUsecaseUserRepositoryというインターフェース(=抽象)に依存させるようにします。

インターフェースを作る

インターフェースはこんな感じにします。tupleを使います。
tuple公式ドキュメント
Nimのtuple解説記事

user_repository_interface.nim
import user_entity

type IUserRepository* = tuple
  save: proc(user:User)

そしてインターフェースの実装(=具象)はこうします。

repositories/user_rdb_repository.nim
import ../user_entity
import ../user_repository_interface

type UserRdbRepository* = ref object

proc newUserRdbRepository*():UserRdbRepository =
  return UserRdbRepository()

proc save(self:UserRdbRepository, user:User) =
  rdb().table("users").insert(%*{
    "id": user.id,
    "name": user.name,
    "email": user.email,
    "is_admin": user.isAdmin
  })

proc toInterface*(self:UserRdbRepository):IUserRepository =
  return (
    save: proc(user:User) = self.save(user)
  )
repositories/user_json_repository.nim
import ../user_entity
import ../user_repository_interface

type UserJsonRepository* = ref object

proc newUserJsonRepository*():UserJsonRepository =
  return UserJsonRepository()

proc save(self:UserJsonRepository, user:User) =
  let userJson = %*{
    "id": user.id,
    "name": user.name,
    "email": user.email,
    "is_admin": user.isAdmin
  }
  let f = open("sample.json", FileMode.fmWrite)
  f.write(userJson.pretty())
  f.close()

proc toInterface*(self:UserJsonRepository):IUserRepository =
  return (
    save: proc(user:User) = self.save(user)
  )

toInterfaceというメソッドが追加されました。
これは実装のメソッドを、インターフェースの要素であるメソッドに組み替える働きをします。
また実装の各メソッドはインターフェースを経由してアクセスされるため、privateにしておきます。メソッド定義にアスタリスク(*)を付けないようにしましょう。これでインターフェースを経由しない呼び出しを防ぐことが出来ます

使う時にはより上位からユースケースへインターフェースの実装を渡し、ユースケースはインターフェースへ依存するようにします。

controller.nim
import user_repository_interface
import user_usecase

let userRepository = newUserRdbRepository().toInterface()
let usecase = newUserUsecase(userRepository) # 依存性の注入
usecase.save()
user_usecase.nim
import user_entity
# ユースケースはインターフェースという抽象へ依存しているが実装へは依存していない!
import user_repository_interfcae

type UserUsecase* = ref object
  repository:IUserRepository

# コンストラクタ
proc newUserUsecase*(repository:IUserRepository):UserUsecase =
  return UserUsecase(repository:repository)

proc save*(self:UserUsecase) =
  let user = User(id:1, name:"user1", email:"user1@nim.com", isAdmin:false)
  # どっか知らんけど受け渡されたインターフェースの実装に保存
  self.repository.save(user)

DIコンテナ

しかしこれではまだ不十分です。例えばテスト実行時にはデータの保存先はJSONファイルにして、アプリケーション実行時にRDBにしたい時には、コントローラーで呼び出すインターフェースの実装をいちいち書き換えるのでしょうか?コントローラーの実装が多くなると、書き換えるだけでもすごく面倒だし時間もかかるしミスも起きるようになります。
そこで、どのインターフェースの実装を呼び出すかを一箇所で集中管理できるようにします。これがDIコンテナです。

di_container.nim
# user
import user_repository_interface
import repositories/user_rdb_repository
import repositories/user_json_repository
# post
import post_repository_interface
import repositories/post_rdb_repository
import repositories/post_mock_repository

type DiContainer* = tuple
  userRepository: IUserRepository
  postRepository: IPostRepository

# どのインターフェースの実装を呼び出すかを一元管理できる
proc newDiContainer():DiContainer =
  if getEnv("APP_ENV") == "test": # 環境変数で呼び出す実装を差し替えたり
    return (
      userRepository: newUserJsonRepository().toInterface(),
      postRepository: newPostMockRepository().toInterface()
    )
  else: # アプリケーション実行時にはRDBにアクセスする
    return (
      userRepository: newUserRdbRepository().toInterface(),
      postRepository: newPostRdbRepository().toInterface()
    )

let di* = newDiContainer() # シングルトンになる
controller.nim
import di_container

let userRepository = di.userRepository
let usecase = newUserUsecase(userRepository) # 依存性の注入
usecase.save()

参考文献

Interfaces In Nim
Nimのオブジェクト指向の整理その2

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11