前書き
Airframe Meetup #1 - connpassへ参加せてもらってなかなか良い刺激になったので、一念発起してScala用のDIライブラリの比較記事を書くことにしました。
が、ぶっちゃけるとAirframe: DI Framework Comparisonの記事のほうが100倍ぐらいDI真面目に使っている人がこの記事の10倍くらい時間かけて書いているに違いないのでそっち読んだほうがよいです。
この記事は多様かつ様々な視点からの意見があったほうがよいという背景から書いています。
※注意: この記事を書いている人間はGuice, Airframe, MacWireそれぞれの使用時間が3時間未満なのでその程度の知識の人間が書いている前提で読んでください
ドキュメント(仕様)を読んで比較して気づくこと
Airframe: DI Framework Comparisonを参考にしつつ特に気になった点を挙げます。
GuiceはJavaにも対応
必然Scala固有の機能(traitなど)には対応しておらず、Scalaで書くには少々冗長に感じることが多そうです。
MacWireは静的(コンパイル時)注入、GuiceとAirframeは動的(ランタイム時)注入
コンパイル時に注入が完了するということは、コンパイルさえ成功させれば実行中はDIに関するエラーは絶対に出ないことが保証されます。
一方DIが動的である場合、自分が書いたまずいコードが原因で実行中にDI関連でエラーが起こる可能性は排除できません。
またそのエラーがいつ起こるかわからないことはなかなか厄介です(ここは静的型付け言語と動的型付け言語の関係に似ています)。
Generic Type(Array[_]やSeq[_]など)に対応しているのはAirframeのみ1
Guice, MacWireはGeneric Typeのbindingができないのに対してAirframeにはそれができます。
言い換えるとAirframeならSeq[Int]
とSeq[String]
を判別して別々のインスタンスをbindできるということです。
コードを書いて比較して気づくこと
各々それぞれいろんな書き方があるので、一旦コンストラクタインジェクションのスタイルで統一しています。
またコンストラクタインジェクションの中でもドキュメントのサンプルコードで真っ先に出てくるような、典型的かつシンプルな例を書いています。
プロジェクト全体は bigwheel/scala-di-library-comparison です。
コード全体
trait TeacherRepository {
def getRandomly(): String
}
class TeacherRepositoryImpl extends TeacherRepository {
override def getRandomly(): String = "風見先生"
}
object Main {
class TeacherSelectService_pure_scala(teacherRepository: TeacherRepository) {
def printYourTeacher(): Unit = {
println("あなたの先生は " + teacherRepository.getRandomly())
}
}
import com.google.inject.Inject
class TeacherSelectService_google_guice @Inject() (teacherRepository: TeacherRepository) {
def printYourTeacher(): Unit = {
println("あなたの先生は " + teacherRepository.getRandomly())
}
}
class TeacherSelectService_airframe(teacherRepository: TeacherRepository) {
def printYourTeacher(): Unit = {
println("あなたの先生は " + teacherRepository.getRandomly())
}
}
class TeacherSelectService_macwire(teacherRepository: TeacherRepository) {
def printYourTeacher(): Unit = {
println("あなたの先生は " + teacherRepository.getRandomly())
}
}
def main(args: Array[String]): Unit = {
// pure scala
{
new TeacherSelectService_pure_scala(new TeacherRepositoryImpl).printYourTeacher()
}
// google guice
// コードの参考ページ: https://qiita.com/saka1_p/items/45fdf1f736173fcf1c5a
{
import com.google.inject._
class RealModule extends AbstractModule {
override protected def configure(): Unit = {
bind(classOf[TeacherRepository]).to(classOf[TeacherRepositoryImpl])
}
}
val injector = Guice.createInjector(new RealModule)
injector.getInstance(classOf[TeacherSelectService_google_guice]).printYourTeacher()
}
// airframe
// コードの参考ページ: https://wvlet.org/airframe/docs/index.html#constructor-injection
{
import wvlet.airframe._
val design = newDesign
.bind[TeacherRepository].toSingletonOf[TeacherRepositoryImpl]
design.build[TeacherSelectService_airframe] { teacherSelectService =>
teacherSelectService.printYourTeacher()
}
}
// macwire
// コードの参考ページ: https://github.com/adamw/macwire#macwire
{
import com.softwaremill.macwire._
class Module {
lazy val teacherRepository: TeacherRepository = wire[TeacherRepositoryImpl]
lazy val teacherSelectService: TeacherSelectService_macwire = wire[TeacherSelectService_macwire]
}
new Module().teacherSelectService.printYourTeacher()
}
}
}
注入先の比較
コンストラクタインジェクションで比較する限り、注入するクラスの記述はpure Scala, Airframe, MacWireの3つでは完全に一緒です。
Guiceのみ@Inject()
というアノテーション記述が必要です。
binding定義と注入した結果の取り出し部分の比較
bindingと注入した結果を取り出すコードはどれも結構違います。
最も冗長なGuiceをベースにして比較すると、Scala専用のAirrameおよびMacWireではGuiceのInjectorに当たるものが存在せずModuleと役割が統合されています。
また、Moduleの記述もGuiceはconfigureメソッドの内部でbind定義を書かなければいけないなどScalaのコードとしては少しぎこちない点が見られます。
注入した結果の利用方法に着目すると、Airframeは結果の取得がブロックであること、つまり注入されたインスタンスのライフタイムが明確に制限されている点が他と大きく違います。
ちょうど昨日Airframe Meetup #1 - connpassで作成者の話を聞いていたのですが、これは注入物のset up/tear downのタイミングを正確に制御し、かつその順序は考えないようにしたい(初期化・終了処理の順序解決はライブラリに任せたい)という設計意図が背景にあるようです(Airframe Meetup #1 2018-10-23 @ Arm Treasure Data (Tokyo Office)のスライドの19ページ目あたり)。
Moduleの視点ではGuiceとAirframeが注入した結果をModule(Injector)から取り出すというスタイルを取っているのに対してMacWireは注入結果もModuleへ一緒に書かれています。
悪くはないものの、GuiceのようなDIライブラリに慣れていると最初は違和感があるかもしれません。
結局どれを使うべきなのか
- Airframeを使うべきケース
- Guice系統の正統進化がほしい
- DI関連オブジェクトのライフサイクルを管理する必要があり、それをより楽に行いたい
- Generic Typeを注入する必要がある
- MacWireを使うべきケース
- 静的片付けでDIしたい(コンパイル時にDIしたい)
- 注入した結果の取り出しが従来のGuiceと違っても許容できる
- DI関連オブジェクトのライフサイクルを厳密に管理する必要がないまたは薄い
- Guiceを使うべきケース
- Javaで使っていて愛着がある
- やんごとなき事情によりやむなく使う必要がある
-
MacWireもScalaのMacroを利用しているので技術的にはできなくもない気がするんですが、確かにできるという記述は見つかりませんでした。技術的に無理なのか、それとも可能ではあるが機能実装の優先順位が低いだけ? ↩