結論
トレイトオブジェクトで動的ディスパッチ
背景
RustのWebアプリケーションフレームワークRocketを勉強中。公式サイトのガイドでは、データベースのコネクションプールをRocketで管理する例が扱われている。この例にそのまま倣うと依存性がべったりで辛いので、DIパターン(依存性の注入)に書き換えた。
ガイドを見てみる
ガイドの当該部分からコードを切り貼りしつつ、元の実装を見ていこう。
Rocketに管理させるPool型はこう書かれている。
// An alias to the type for a pool of Diesel SQLite connections.
type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
要するにただのエイリアスなんだけど、型パラメータにSqliteなる文字が。Rustは「型はドキュメント」を地で行っていて、データの特性、ひいては依存性が型で表現されている。つまるところ、このPool型はSQLiteのConnectionをManageしたPoolだ。(?)
Poolはこんな感じで(ただの例なので、危なっかしく)初期化され、
/// Initializes a database pool.
fn init_pool() -> Pool {
let manager = ConnectionManager::<SqliteConnection>::new(DATABASE_URL);
r2d2::Pool::new(manager).expect("db pool")
}
Rocketのmanageメソッドに引き渡されて管理される。
fn main() {
rocket::ignite()
.manage(init_pool())
.launch();
}
Rocketは型ごとに高々一つグローバルな状態を管理する。そのため、Pool型とinit_pool()で作成した値は1:1で対応するし、Pool型を指定すればその値が取れる。
で、ハンドラが引数に受け取るDbConn型はこう。
// Connection request guard type: a wrapper around an r2d2 pooled connection.
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<SqliteConnection>>);
/// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status.
impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<DbConn, ()> {
let pool = request.guard::<State<Pool>>()?;
match pool.get() {
Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
}
}
}
// For the convenience of using an &DbConn as an &SqliteConnection.
impl Deref for DbConn {
type Target = SqliteConnection;
fn deref(&self) -> &Self::Target {
&self.0
}
}
なんという分かりやすさ。FromRequestトレイトを実装した型はハンドラの引数に含むことができ、ハンドラが呼び出されるときにfrom_requestメソッドで値を生成して引き渡される。この場合はRocketがPool型の値を管理していないか問い合わせ、存在すればコネクションをくるんで返している。また、Derefトレイトを実装しているからSQLiteのコネクションに直接変身できる。
ハンドラでは以下のようにこのDbConn型が扱える。
#[get("/tasks")]
fn get_tasks(conn: DbConn) -> QueryResult<Json<Vec<Task>>> {
all_tasks.order(tasks::id.desc())
.load::<Task>(&*conn)
.map(|tasks| Json(tasks))
}
問題点
ここまで見てくればこの例がそのまま使えない理由も分かると思う。テスト時なんかに、SQLiteではなく適当なモックに差し替えたいときにはどうすればいいだろう?
こういう依存性の問題を解決するのに適しているものといえばあれだよ、DIだよ。
DIしよう
有名なデザインパターンなので説明は割愛。要するに多相。
先述の通りRustでは依存関係のような属性は型情報に含まれる。そしてそのような属性を型パラメータとして与えるジェネリクスと、ばっさり切り落として隠蔽するトレイトオブジェクトの2種類の方法が用意されている。今回は型パラメータで静的に解決することができないため、後者のトレイトオブジェクトを使う。
トレイトオブジェクトは&トレイトかBox<*トレイト*>のような形で作られる。まずはトレイトを作ろう。元々の役者はコネクションプールと個々のコネクションだったから、それぞれに対応するトレイトを作成する。
pub trait Database: Send + Sync {
fn get(&self) -> Result<Box<DatabaseAccess>, ()>;
}
pub trait DatabaseAccess {
…
}
Box<Database>がPoolに、Box<DatabaseAccess>がDbConnに相当する型だ。肝は具象型がなんだろうとBox<Database>として管理されるところ。
Rocketが管理する状態の型はSendでSyncでないといけないため、DatabaseトレイトはSendとSyncを継承する。また、getメソッドを呼んでBox<DatabaseAccess>を生成できるようにする。
DbConnと同様に、Box<DatabaseAccess>にはFromRequestトレイトを実装する。具象型が何かを気にせずBox<Database>型を探しにいけばいい。
impl<'a, 'r> FromRequest<'a, 'r> for Box<DatabaseAccess>{
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Box<DatabaseAccess>, ()> {
let database = request.guard::<State<Box<Database>>>()?;
match database.get() {
Ok(conn) => Outcome::Success(conn),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
}
}
}
ついでにモックを作って
struct MockDB();
impl Database for MockDB {
fn get(&self) -> Result<Box<DatabaseAccess>, ()> {
Ok(Box::new(MockDBAccess()))
}
}
struct MockDBAccess();
impl DatabaseAccess for MockDBAccess {
…
}
注入してみる。
pub fn init_mock() -> Box<Database> {
Box::new(MockDB())
}
rocket::ignite().manage(init_mock())
見ての通りBox<DatabaseAccess>はDatabaseトレイトを実装した具象型についての知識がない。ハンドラもDatabaseAccessトレイトが提供するインターフェイス越しにデータを触ることになる。依存性は切り離された。わあい。