Airframe Meetup #1: Scala開発に役立つ5つのデザインパターンを紹介
先日、初のAirframe Meetup #1を開催し、盛況のうちに終了しました。日本への帰国のタイミングに合わせて数週間前に企画したにも関わらず、コアなScalaエンジニアが多く集まる会になり、楽しい時間を過ごすことができました。企画、会場準備を快く手伝ってくれた@takezoenさんに感謝。
概要 Dependency Injection(DI)をコアとしたScalaライブラリ「Airframe」のMeetupです。 # アジェンダ 時刻 内容 19:00〜 開場 19:30〜…airframe.connpass.com
Airframeは10月17日のTD Tech Talkでも紹介しましたが、Scalaのための軽量ライブラリ集で、ここではScalaエンジニア以外の方にも楽しんでいただけるよう、Airframeの各種モジュールやコンセプトの紹介を中心に行いました。
TD Tech Talk (October 17, 2018)で、Scala開発のための軽量ライブラリコレクションであるAirframe (https://wvlet.org/airframe) について紹介しました。medium.com
今回のAirframe Meetupでは、Scalaエンジニアにフォーカスすることで、コード例を多く交えながらAirframeのDependency Injection (DI)について、よりディープに解説していくことにしました。DIとは何かを理解するには上記のブログを参考にしてください。
Airframeでは「プログラマが考えるべきことを効率的に減らす」ためにライブラリをデザインしています。今回ははAirframeのデザインパターンとでも言うべき5つのパターン(+ Pattern 0 (基礎))について紹介しました。
Pattern 0: Auto Wiring
AirframeのDIでは、constructor injectionか、in-trait injectionを使うことになります。コンストラクタの引数に必要モジュールを列挙するか、trait内にbind[X]構文を用いてモジュールを注入するかどうかの違いです。
どちらの場合も、オブジェクトの設計(design)とオブジェクトの構築方法(build)のやり方は同じです。
ここで、以下のようなA, Bというサービスを作ることを考えます。
実際のコーディングの際には、direct dependency (直接の依存関係)にあるオブジェクト(ここではDBClient, FluentdLogger)だけを知っていればよいので、Airframeのin-trait injectionを使うと欲しい機能だけに注目してコーディングすることができます。
しかし、実際には以下にあるように、DBClient, FluentdLoggerの裏側には以下に示すように詳細な実装が隠れています。
AやBをテストしようとする際に、これらのモジュールの詳細を考えてオブジェクトを構築するのは非常に大変です。たとえば、手でオブジェクトを組み合わせて構築するコードを書くと以下のようになります:
AirframeによるDIを使うと、オブジェクトの構築方法はフレームワーク側に任せることができるので、以下のような記述だけで済みます:
プロダクションのコードでは数十〜数百のモジュールを組み合わせてコーディングすることが普通なので、auto-wiringによるオブジェクト構築の支援はコードを簡潔にするために役立ちます。
Pattern 1: Configuring Modules
モジュールを作る過程で設定パラメータ(コンフィグ)を追加したくなるケースは多いでしょう。Airframeを使うと、モジュールに一番近い場所でコンフィグオブジェクトを定義することができ、モジュールの実装を最小化することができます。Airframeではデフォルトパラメータに基づいてオブジェクトを構築することもできるので、上の例ではConnectionPoolConfig(size=32)というデフォルトオブジェクトが自動的に生成されますし、後からdesignで上書きすることもできます。
モジュールを最小化するという当たり前のようなことが、実際のプログラミングでは非常に難しいものです。特にHand-wiringをしているとコンフィグオブジェクトを、クラスの深いネストをまたがって受け渡す必要があり、その手間を省くために、globalなコンフィグオブジェクトを作ってしまう例をよく見かけます。例えば、Akkaにはglobalなコンフィグから、モジュール毎のコンフィグを読むコードがありますが、このアプローチはAkkaくらい大規模なプロジェクトだから割りに合う実装の仕方ででしょう。
Pattern 2: Switching Bindings for Tests
Airframeでは、テスト用に一部の実装を切り替えるのも容易です。デザインの一部だけを上書きするだけで、テスト用のサービスを構築することができます。
Pattern 3: Managing Lifecycle in FILO Order
データベースのコネクション、HTTPクライアントなど、起動、終了処理は通常FILO (First-In Last-Out)の順番で行うことになります。Airframeでは以下のようなライフサイクルフックを記述することでこの管理実現できます:
オブジェクトの構築をAirframeのようなフレームワークに任せることの利点として、このようなFILOオーダーによるリソース管理もフレームワーク側で自動的に制御できるようになることが挙げられます。
Pattern 4: Bundling Service Traits (Flower-bundle pattern)
ScalaのTraitのmix-inは非常に強力で、trait = serviceと考え、DBClient、FluentdClientなどのサービスを定義していくと、traitのmix-inにより、好きなサービスを必要なだけ組み合わせて、より複雑なサービスを構築していくことができます。再利用可能なサービス(module)を構築していくことで、典型的なアプリケーションの開発が効率化されていくのが魅力です。
実装のパターンとしては、traitの中にサービスのAPIとなる変数1つだけを含む実装(flower)を各種用意し、それを束ねていく(bundle)様子に例えて、Flower-Bundle Patternと呼んでいます。
基本的にコンストラクタでも同様の組み合わせは実現できますが、クラスが階層に挟まってしまいフラットにバンドルできないため、traitの方が無駄な階層ができずに綺麗に実装できます。
Pattern 5: Binding Factory
最近Airframeに追加したパターンですが、一部の設定だけを上書きし、その他のモジュールはdesignに指定されたものを使いたい場合に、bindFactoryが使えます。複数のDBへの接続を使い分けたいが、ConnectionPoolなどは再利用したいときなどに便利です。
その他の話題
さすが普段からScalaを書いている人たちの集まりで、さらにマニアックな話で盛り上がりました。
- Higher-kinded typeでもbindできるのはAirframeのみ。これはかとじゅんさんの要望で実現。
DDDリポジトリを楽に実装するライブラリ](https://qiita.com/j5ik2o/items/5f29e2ec22a5eac2a186)で紹介した[scala-ddd-base](https://github.com/j...qiita.com
- AirframeはJDK11まで対応している。JDK8からJDK9への移行が一番大変で、そこを乗り越えるとあとは楽。Scalaは2.12からJDK9以降に対応している。
- Scala 2.13.0-M5と JDK11で、String.linesメソッド名の衝突のバグがあった。Scala 2.13.0-RC1では修正される。
- Airframeは他のライブラリへの依存関係を極力減らしている。基本的には、Scalaのreflectライブラリのみに依存。
- 依存関係を減らすためだけに、JSONパーサー airframe-jsonや、msgpackパーサー airframe-msgpackまで自作
- Jawn, circe JSON parserはなぜ速いか。table switch命令(コンパイル時にチェックされる)を使っているから。https://www.scala-lang.org/api/2.12.1/scala/annotation/switch.html また、alphabet, numericのみの文字列用のfast pathがある。
- したがって、日本語を多々含む文字列がある場合、json4s-jacksonの方が速いことがある。(巷のベンチマークを信用しすぎないのが吉)。速いと言われているuJsonがなぜかものすごく遅い。
- 無知による再発明(reinvention)は悪だが、十分承知した上で再実装するのは、知識の再現(reproduction)であって、学習のためには非常に有効。どんどん実践するべきだし、再発明より高速にできる。airframe-jsonも2日しか実装に使っていない。
- AirframeはなぜMessagePackを使っているか。object serializerの実装を簡単にできる。MessagePack + SQLエンジンの実装もこっそり進めている。
- 基本的にREADME.mdを書いていないモジュールはAPIが安定していないので「まだ使わないで」という意図。
- 基本的に、bug fixなどを含めてrelease頻度が多いので(10分でScala 2.11, 2.12, 2.13, Scala.js x 18 modulesがリリースできるようになったため)、Airframeは常に最新版を使うのが吉。今年だけで30以上リリースしている。
- SQLエンジンもScalaなら500行で書けるという論文の紹介。SQL -> Scalaでパース -> データベースクエリを実行するCやScalaコードを生成
- Treasure DataのScalaエンジニアは最初は1人だったが、徐々に増えてきた。
開催後
Airframe Meetupをきっかけに以下のようなDIライブラリの比較記事も書いていただきました。Airframeが「Guiceの正統的進化」というのは、開発者にとしてもそこを目指した経緯があり、しっくりくる表現です。
前書き [Airframe Meetup \#1 \- connpass](https://airframe.connpass.com/event/104722/)へ参加せてもらってなかなか良い刺激になったので、一念発起してSc...qiita.com
当日の様子は#airframeタグのツイートからも垣間見ることができます。
Airframeの今後
DIの機能としては、
- implicitパラメータを含むコンストラクタ class A(p1, p2, …)(implicit x1, x2, …) への対応 (#281)
- HTTP request handling時など、child sessionが使えると都合がよい (#70)
の2点が直近の課題。その他のモジュールもまだ増えていく予定です。
また日本に帰国する機会にAirframe Meetup #2が実現できれば良いかと思います。例えば、airframe-codec, msgpack周りや、Airframe DI + Finagleによるweb frameworkなど紹介できる事例がいくつかあります。
airframe-http is the latest addition to Airframe, lightweight building blocks for Scala. airframe-http is a library for…medium.com
Airframe Meetup #1に参加していただいた皆さま、どうもありがとうございました。会場の規模の都合で参加できなかった方にも(すぐに満席になってしまったため、告知もあまり積極的にできませんでした)この記事が参考になれば幸いです。