この記事は、Nim Advent Calendar 2020 その2 の2日目です(埋めていくことにしました)。
NimでインターフェースとDIコンテナを使う方法を完全に発見したので共有します。
インターフェースって何?
めちゃくちゃ端折って言うと、アプリケーションの外部とデータをやり取りする時に、「どことやり取りするか」をアプリケーション内部が知らなくてもいいようにするものです。
外部とデータをやり取りするとは、具体的にはRDBやNoSQLや外部APIやJSONファイルってこと。
例えばユーザーデータを保存するプログラムがあるとします。
type User* = ref objct
id*: int
name*: string
email*: string
isAdmin*: bool
RDBに保存すると
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ファイルに保存すると
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()
呼び出し元では
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ファイルかどこなのかは関心事ではありません。「どっか知らんけど保存する」にしたいわけです
↓こんな風にしたい
proc save*(self:UserUsecase) =
let user = User(id:1, name:"user1", email:"user1@nim.com", isAdmin:false)
# どっか知らんけど保存する
newUserRepository().save(user)
この時、UserUsecase
をUserRepository
というインターフェース(=抽象)に依存させるようにします。
インターフェースを作る
インターフェースはこんな感じにします。tuple
を使います。
tuple公式ドキュメント
Nimのtuple解説記事
import user_entity
type IUserRepository* = tuple
save: proc(user:User)
そしてインターフェースの実装(=具象)はこうします。
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)
)
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にしておきます。メソッド定義にアスタリスク(*)を付けないようにしましょう。これでインターフェースを経由しない呼び出しを防ぐことが出来ます
使う時にはより上位からユースケースへインターフェースの実装を渡し、ユースケースはインターフェースへ依存するようにします。
import user_repository_interface
import user_usecase
let userRepository = newUserRdbRepository().toInterface()
let usecase = newUserUsecase(userRepository) # 依存性の注入
usecase.save()
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コンテナです。
# 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() # シングルトンになる
import di_container
let userRepository = di.userRepository
let usecase = newUserUsecase(userRepository) # 依存性の注入
usecase.save()