RustでもScalaとかのようにカッコよくMixinを決めたいよね。
MixinでDIやりたいよね。
やりかた
XxxService
トレイト
まずサービスのインターフェースを定義する。
pub trait HelloService<T> where T: ?Sized {
fn hello(this: &T, to: String) -> ();
}
このジェネリクスが重要になる。
サービスのインターフェースはSelf
を使わず、代わりにジェネリクスで受け取った型を持つthis
を第1引数に取る。this
を受け取らないメソッドは定義しない。
MixinXxxService
トレイト
続いて、インターフェースと対になる、サービスをMixinするトレイトを定義する。
pub trait MixinHelloService {
type Impl: HelloService<Self>;
fn hello(&self, to: String) -> () {
Self::Impl::hello(self, to);
}
}
キーワードの一つ、関連型が現れた。
この関連型Impl
はサービスを実装した型を示している。しかも、先程定義したインターフェースのパラメータにSelf
を渡している。知っての通りSelf
はこのトレイトを実装した具象型だ。
また、このトレイトはサービスのインターフェースで定義したthis
をself
で置き換えたメソッドを定義し、デフォルト実装で関連型Impl
にプロキシしている。
JavaScriptを触ったことのある人なら、this
を退避したself
という名前の変数を作ったことがあるかもしれない。この二つのトレイトは、名前こそ逆だけれど同じことを、型レベルで行っていると考えていい。
なぜSelf
を退避して渡す必要があるか? この二つのトレイトを実装する具象型は異なるからだ。
いよいよサービスを実装しよう。
XxxServiceImpl
型
pub struct HelloServiceImpl;
impl<T> HelloService<T> for HelloServiceImpl
{
fn hello(_this: &T, to: String) -> () {
println!("Hello, {}!", to);
}
}
二つ目のキーワードであるUnit-like構造体が現れた。フィールドを持たない構造体は型レベルで何かするときに便利。Variantの無いenumも使用できるけれど、そこはまあ好みだよね。
この空の型にサービスを実装する。
this
を単に捨てているのは、この実装では不要だからだ。this
を参照する実装も作ることができる(割愛)。
Mixinと利用
ここまででサービスの実装を作り具象型を与えた。
あとはこのサービスをMixinして利用するだけだ。
struct Base;
impl MixinHelloService for Base {
type Impl = HelloServiceImpl;
}
fn main() {
let base = Base {};
do_something(base);
}
fn do_something<S>(base: S) -> ()
where
S: MixinHelloService,
{
MixinHelloService::hello(&base, "World".to_string());
}
見ての通り、何か適当な型にimpl
で関連型として実装を与えるだけのお手軽仕様。実装を差し替えたい場合もここで関連型Impl
に別の型を指定する。Mixinとしては上々のゆるさだ。
利用するときも、Mixinした型のインスタンスを静的ディスパッチによって伝搬し、サービスを使いたい場所でトレイト境界にMixinHelloService
を指定すればいい。実装が何であるかは意識せずサービスを利用できる。
サービスの依存関係
ここまでの説明ではSelf
を伝搬する必要がまったくなかった。
この仕組みはサービスに依存関係が生じたときに威力を発揮する。
まず、先程同様にサービスのインターフェースとMixin用のトレイトを用意する。
pub trait WorldService<T>
where
T: ?Sized,
{
fn hello_world(this: &T) -> ();
}
pub trait MixinWorldService {
type Impl: WorldService<Self>;
fn hello_world(&self) -> () {
Self::Impl::hello_world(self);
}
}
そして実装を作る。
このとき、伝搬されたSelf
に対して追加の要求を宣言できる。
pub struct WorldServiceImpl;
impl<T> WorldService<T> for WorldServiceImpl
where
T: MixinHelloService,
{
fn hello_world(this: &T) -> () {
MixinHelloService::hello(this, "World".to_string());
}
}
ここではMixinHelloService
が実装されていることを要求し、実装の中でthis
を使って呼び出している。
Self
が伝搬することによって、Mixinする対象に制約を掛けることができたわけだ。
WorldService
自体はHelloService
に依存していないことにも着目してほしい。実装次第で依存する対象は変わりうる。HelloService
に依存しない実装も可能だ。
このサービスをMixinしたら、利用するときは単にMixinWorldService
を要求すればいい。
fn do_something<S>(base: S) -> ()
where
S: MixinWorldService,
{
MixinWorldService::hello_world(&base);
}
そう、サービスの実装の依存関係が外に漏れ出さないのだ。
関連型は暗黙的に利用できるため、余計な型引数もなく非常にすっきりした形でwhere
節を書ける。
もちろんこれは静的ディスパッチで型情報を全て伝搬できる時の話であって、動的ディスパッチが絡むと壊れる。そういうとこやぞRocket。