yuki さんの下記の記事を読みました。
DI の実現方法について、たくさんの用法を示していただき、大変勉強になりました。
(モナドが出てくるとは...)
さて、私自身も最初のころ静的ディスパッチによる実装を導入していましたが、今はもっと単純な実装をするようになったので、紹介しておきます。
条件付きコンパイルによる実装
簡単に言うと、以下のようなアプローチです。
struct UserRepositoryImpl{
db: Database
};
impl UserRepositoryImpl {
pub fn find_user(&self, id: String) -> Result<Option<User>>
{
// 本番用の実装
}
}
struct UserRepositoryMock {
data: Vec<Result<Option<User>>>
}
impl UserRepositoryMock {
pub fn find_user(&self, id: String) -> Result<Option<User>>
{
// テストの実装
}
pub fn insert_user(&mut self, Result<Option<User>>) {
// テスト用のヘルパーメソッド。
}
}
// テストと本番で切り替え
#[cfg(not(test))]
type UserRepository = UserRepositoryImpl;
#[cfg(test)]
type UserRepository = UserRepositoryMock;
// 利用は UserRepository を使う。
struct AppState {
user_repository: UserRepository
}
trait によるインターフェースの定義もせず、Rust のコンパイラの力に頼り切った(cargo check/test がそれぞれ通れば良い)実装ですが、とてもシンプルな実装方法です。
前提
念のために言いますが、元の記事で DI を挙げているのは、テストの目的以外も含めているのかもしれず、上のアイデアは記事のもともとの主題から少しずれています。
(DI にはテスト以外の用途にも対応できる柔軟性がありますが、多くのプロジェクトはテストのためだけに DI を利用している印象があります。)
本番/テストの2種類をただ切り分けるという目的だけなら、上のような単純な実装で割り切っても良いのかなと思いました。
最後に
複雑なプロジェクトでは本番とテストの2種類の切り替えだけでは不足するかもしれませんが、私のいくつかのプロジェクトではこれで十分でした。
一口に DI といってもさまざまな実装方法があることがわかるので、ぜひ yuki さんの記事 をご覧になってください。