AbemaTVの広告配信サーバーで使われている、Scalaっぽく使えるDI
経緯
Scalaで、これだ!というDIフレームワークってあんまりないですよね。
メジャーどころはだいたいJava製とかで、Scalaだからできる書き方とかもたくさんある中で、それを活かせないライブラリが多いなぁという印象です。
(immutableな設計してもライブラリの兼ね合いで結局mutableになっちゃう...とかね
いい比較事例なのでお名前をお借りしたいと思いますが、業界だと、ドワンゴさんのDIの手法が有名ですよね。
あれはもう教科書通りのお手本にすべき設計だと思います。
ライブラリ無しで実現するんだったら他の選択肢はないんじゃないかと思うくらい。
でも一つ、個人的な感想を言わせてもらうと、定義するモノが多く、 extends ... with ... with ...
ってなってしまうのがちょっとつらい。パッと何を注入してるのかわからなかったりします。
当時のドワンゴさんのScalaコードが22万行、それを管理するDI手法・・・異次元だ・・・と思いきや、気づけばこのDIを使ったAbemaTVの広告配信システムもScalaコードが21万行でした。
案外公開しても恥ずかしい物でもなくなってきたかな、ということで、
Scalaの魅力の一つである、 短く書ける ってところを推し進める形で、今回は一つ紹介させてください。
ちなみにScaladiaはランタイムで依存性を解決するので、コンパイル時の検知はできません。
なぜ使わなかったかというと、Scalaバージョンが上がった時にmacroが破壊的変更を受けるかもしれないとのことで、メンテできる自信がありませんでした・・・
Scaladia
基本的に多くのDIライブラリでは、依存性の管理は毎回厳密に自分で管理しなければならないことが多いと思います。
Scaladiaでは、TypeTagとObjectのlazy initを使って、基本的にはこいつを使え!という設定ができるので、
各インターフェースごとにbindの設定なんぞ(基本的には)書く必要がありません。
基本
/**
* 基底インターフェース
**/
trait X {
def test: String
}
/**
* 実装クラス
**/
object A exntends AutoInject[X] with X {
def test: String = "TEST"
}
object Main extends Injector {
def main(args: Array[String]): Unit = println(inject[X].test)
}
これだけでいつどこからinject[X]しても A が注入されます。
RepositoryのREADMEにも記載されていますが、
AutoInject[T] を継承したクラスは、inject[T]された時、自動でロードされます。
DIされるオブジェクトからのDI (循環参照はエラーになります。)
もちろん再帰的な依存性の定義がある場合も正常に動作します。
trait A {
val callA: String
}
object A1 extends AutoInject[A] with A {
val callA: String = "HOGE"
}
trait B {
def callB: String
}
object B1 exntends AutoInject[B] with B {
def callB: String = inject[A].callA
}
object Main extends Injector {
def main(args: Array[String]): Unit = println inject[B].callB // "HOGE"
}
この時 A1
の中で B
をDIしようとすれば当然循環参照のエラーが発生します。
依存性の上書き
特定のクラス・オブジェクトからは異なる依存性を注入する場合
trait A {
def test: String
}
object A1 exntends AutoInject[A] with A {
def test: String = "TEST"
}
object A2 exntends A {
def test: String = "OTHER_TEST"
}
object Main extends Injector {
def main(args: Array[String]): Unit = println inject[A].test // "TEST"
}
trait ForType2 extends Injector {
narrow[A](A2).accept(this).indexing()
}
object OtherMain extends ForType2 {
def main(args: Array[String]): Unit = println inject[A].test // "OTHER_TEST"
}
このように、基本的にはAutoInjectがMixInされた実態が注入されるわけですが、
特定の呼び出しの時にのみ、異なる実態に差し替えることができます。
DI可能なクラスを制限する
特定のクラス・オブジェクト以外からの注入を許可したくないことがあります。
trait A {
def test: String
}
object A1 exntends A with Injector {
def test: String = "TEST"
}
trait BaseRunner extends Injector {
// OtherMainからのアクセスのみ許可します。
narrow[A](this).accept(OtherMain).indexing()
}
object Main extends BaseRunner {
def main(args: Array[String]): Unit = println inject[A].test // InjectableDefinitionException
}
object OtherMain extends BaseRunner {
def main(args: Array[String]): Unit = println inject[A].test // "TEST"
}
acceptは、その実態が注入できるクラス・オブジェクトの境界定義をする関数です。
narrow[A](A').accept(Imple).indexing()
とすれば、A'
は Imple
オブジェクトでしかInjectできなくなり、
他のクラス・オブジェクトからinjectしようとした場合、
AutoInject[A] を継承した実態か、他のクラスで明示的に登録された実態のみがDIされます。
また、
narrow[A](A').accept[X].accept[Y].indexing()
とすれば、class X
or class Y
を継承したクラスからのみ注入されます。
なお、オブジェクトスコープ vs クラススコープの場合、オブジェクトスコープが優先して注入されます。
いかがでしょうか、同じもやもやを感じた際は、ぜひ使ってみてください。
今後もIOモナドHTTPClientなど、色々と作って行きたいと思います。