はじめに
この記事は4月に執筆したものの Scala 祭りだったり執筆中に疑問に思った事を改めて整理しているうちにお蔵入りしてしまったものの棚卸です。特に加筆修正などしていませんので、ポエム記事としてタグ付けしてしています。今は Scala ネイティブの機能で DI を検討する場合には、 DI 以前にまずコンパニオンオブジェクトなどのファクトリで処理できないかを考え、必要に応じて暗黙パラメータで処理するようにしています。アーキテクチャ上明らかにその方が分かりやすい場合には MinimalCake パターンも利用する事があります。
前置き
所信表明。2時間で記事としてのアウトプットを一個作る。これを一先ず習慣になるようやっていきます。
年始に一個記事を投稿させて頂き、タイトルをそこそこ中身がありそうに見せれた事でそれなりの view とご祝儀的ないいねを頂きました。
ですが、実はあれでも記述に6時間ほど費やしてしまっていて、かけた時間の割には内容が薄く参照や具体例などが乏しいというのが反省点となりました。
アウトプットを習慣化していくにあたって反省を踏まえ、まずは日頃から情報を取り入れる事を意識して、さぁ書くぞ、と決めたら大体2時間程度で一本形にする、というのを一つの目安にしていくことにしました。
一定の品質は求めつつも、主題を2時間くらいでアウトプットを形にできるスコープの粒度や、日頃必要な情報収集のための思考方法をストックできれば目標達成とします。
目的
Scala ネイティブの記述力を利用した DI 手法について自分なりに考えた事を書きます。
ここ半年程、自社プロダクトにおける Scala 製のバッチ処理を任され保守開発を継続してきました。
サービスの刷新に合わせて開発されたものを引き継いだ訳ですが、よくよく仕様を確認していると運用上の課題が未解決な部分がそれなりにあり、対応するため設計の見直しとリファクタリングを積み重ねた半年でした。
仕様変更が発生した事で複雑化しつつあったロジックを切り分け抽象化するために利用したテクニックの一つが俗に言う依存性の注入 (DI, Dependency Injection) です。
既にその必要性・功罪が十分に議論され様々なフレームワークでサポートされているソフトウェア開発においてはもはや当たり前のように利用されている要素技術な訳ですが、 Scala では特別なフレームワークを利用せずとも本質的に DI が実現したいデザインを表現できる事が知られています。
この記事では Scala における DI 手法をサクッと振り返り、そのメリットデメリットについて考察します。
Scala での DI について考える
Dependency Injection is 何?
ある オブジェクトA
の振る舞いが別の オブジェクトB
の詳細によって決定される場合、「オブジェクトA
は オブジェクトB
に依存している」と言えます。
コードでざっと書いてみるとこんな感じですかね。Bar#bar はメソッド内部で Foo#foo を利用しているため Foo に依存しています。実際に Bar を利用するためには Foo のインスタンスを与えてやらないといけません。
abstract class Foo(x: Int) {
val foo: Int => Foo
}
abstract class Bar() {
val f: Foo
def bar(i: Int) = {
f.foo(i)
}
}
じゃぁどうやってその依存しているオブジェクトを与えてやるか、というところで Bar の中に Fooの実装クラスである FooImpl を new して与えてやると、 Foo という抽象だけでなく、 FooImpl そのものまで Bar の中に現れて参照ができてしまいます。
「Bar の外のどこかで作った Foo の実装を入れてあげる仕組み」が DI is 何 の正体で、Bar 以外のどこかで実際に依存する Foo の実体を決められる (= 参照が消える) のがその効果です。
Bar の外でその依存先を決定できるメリットは分かりやすくはテストコードを記述する際に現れます。Bar をテストするための Foo の Mockオブジェクトはどうすれば Bar に与えられるでしょうか。そう、Mockオブジェクトを依存先として、Foo に注入してやればいいんです。
DI が認知され始めたばかりの頃はテストくらいでしか意味を持たず、オブジェクトの振る舞いが実装上不透明になってしまうためむしろ利用を避けて依存先を明記すべきだという意見もあったくらいですが、現在はクリーンアーキテクチャのような外側から内側への依存関係のみ許可するようなアーキテクチャで依存関係逆転の原則 (DIP, Dependency Inversion Principle) を実現するための重要な役割を担っていますね。
Qiita で検索すると DI については既にして多く記事があるので、より詳しくはそちらを読んで頂けると。
参考:
How to DI?
DI の種類については世界で一番有名な辞典を引くと以下の三種類が列挙されています。
- コンストラクタインジェクション
- フィールドインジェクション
- セッターインジェクション
分類はオブジェクトのどの部分でDIを実行するかで大別されます。読んで字の如くではあるのですが、具体的な際はいづれのパターンもサポートしている Spring を対象にして記述した以下の記事が非常に分かりやすいと思います。
SpringでField InjectionよりConstructor Injectionが推奨される理由
Spring は前項の参考記事でも言及されているDIコンテナからのインジェクションをサポートしたJavaアプリケーションのためのフレームワークです。オブジェクトをインジェクションするためにはどこかでインスタンスの作成方法を保持しておく必要がある、というのは自明に思えますが、その方法 (もしくはオブジェクトそのもの) を登録しておくとライフサイクルを管理してくれるモノがDIコンテナです。各種アノテーションをコンストラクタ、フィールド、セッターに添える事で、DIコンテナから型に応じてインジェクションすることが可能になります。
DIコンテナを用いたDIのパターンとしてはフィールドインジェクションよりもコンストラクタインジェクションが推奨されているようです。私が初めて本格的にDIを利用した開発に携わったのはキャリアのスタートである Angular1.4 / TypeScript 環境でのフロントエンド開発でしたが、 Angular1.4 では controller
、 service
といったメソッドでDIコンテナに役割に応じたコンポーネントを登録する事が出来ました。このメソッド中では同時にコンストラクタインジェクションを行う別コンポーネントを宣言する事が可能で、アノテーションとメソッドでの宣言という差異はあれど、Spring におけるコンストラクタインジェクションと概ね同様の機能が扱えていたように思います。
DI in Scala?
ようやく本題です。
Scala では DI をどのように実現できるのか。名前として最も有名なパターンは CakePattern でしょうか?
モナドを扱った ReaderT
Scalaにおける最適なDependency Injectionの方法を考察する 〜なぜドワンゴアカウントシステムの生産性は高いのか〜
タイトルの通り、リンク先は日本で現在最も Scala を使い倒している企業の一角であろう、ドワンゴさんのアドベントカレンダーです。公開されている Scala の研修用テキストも含め、振り返りに勉強にと日々学ばせて頂いています。
DIコンテナを利用したパターンばかり扱っていてDIってこういうものと思い込んでいた頃の初見ではこのパターンは目から鱗でした。
CakePattern では Java で Spring を利用した時のように、 TypeScript で Angular を利用した時のように、DIを利用するためのフレームワークが提供する要素を一切含まず、 Scala の言語仕様そのままの記述で DI としての要件を満たしています。
CakePattern による DI が先述の三種類のパターンのいづれに属するかと考えると、フィールドインジェクションに当たりそうです。
( trait の可能性を考えれば、セッターインジェクションもできなくはない気がしますが意味があるかというと?)
ドワンゴさんの記事で紹介されている MinimalCakePattern はちょうどリファクタリングを実施したい箇所があったため適用してリポジトリ化したところ、「Repository を扱う能力」を trait に切り出す事ができるという点で個人的には感触が良いものでした。
MinimalCakePattern を Repository の実装に適用する場合における登場人物はざっくり列挙すると以下の通りです。
- XxxService: Repository を扱う Service (class)
- UserXxxRepository: Repository の実装を注入するインジェクタ (trait)
- XxxRepository: Repository の抽象 (trait)
- XxxRepositoryImpl: Repository の実装 (class)
抽象と実装を切り分けるというのは GoF が提唱したデザインパターンからの基本ですが、「Repository の実装を注入するインジェクタ」が trait である事でインジェクションの内容 だけでなく、 Repository をどのように扱うのかという詳細を UseXxxRepository にまとめて切り出す事が可能なので、各スコープが扱うレイヤに纏まりを感じました。
コンストラクタインジェクションに当たるパターンも言語機能で補う事ができて、暗黙 (implicit) パラメータを利用する事で静的に外部からインジェクションされる依存先を示す事ができます。
暗黙パラメータによるコンスタラクタインジェクションでももちろん処理をトレイトに切り出す事は可能ですが、インジェクションする対象が決定されるのは実行時になります。より上位のスコープから引き回す必要があるため、一体どこで宣言されたものがインジェクションされるのか見通しが立てづらいという難しさを感じました。オブジェクトが活動する環境をうまく定義して無闇矢鱈に引き廻さずに済むよう設計できれば汎用性が高くなるのでしょうが、それよりも UseXxxRepository の中で定義して明示する方がシンプルになります。抽象と具象
はっきり言ってモナドを利用したDIのパターンは理解が追いついていませんが、ご容赦頂きたく。
おわりに
Scala での DI についてもうちょっと考える
私としては CakePattern によるインジェクションが感覚的にしっくりきているという結論ですが、この Scala における CakePattern と Ruby on Rails における Concern を使った実装の本質的な違いって何なんでしょうね。
自社プロダクトではWebサーバ開発のメインには Ruby on Rails を利用しており、 Concern も認証周りで最大限活用しています。
入社後、キャッチアップの過程で Concern の振る舞いを追ってみましたが、import文によるミックスインを取りまとめて管理するための機能という理解をしています。その結果何が出来ているかというと、関心事 (concern) の内側にいるオブジェクトのインジェクションな訳で、対象としたスコープが Scala ではクラス限定な一方、 Rails ではモジュールにも適用できるより広い意味でのインジェクタである、というだけで基本同じ意味を持っている気がします。
Java系の経験を元に、 Ruby on Rails でクリーンアーキテクチャを実現しようとしたらどうなるんだろと、調べていた頃に読んだのが以下の記事。
Ruby/Rails 用 DI コンテナ Dee をつくった、あるいは Ruby のカルチャーについて
Ruby における DI コンテナの実装は、もちろん全く無いわけではないと思うんですが、ほとんど使われていない印象です。
アレコレ検索してみると、Ruby においては DI コンテナは不要である、みたいな記事もいくつか見つかります。
Matz も「DIってのは硬直した言語のための技術なんだ」と言っています。
「硬直的な言語」という言葉の意味はいろんな含みを感じますが、確かに Ruby の宣言されていない変数を平然とコードに記述して動作を期待できる作りというのは、めちゃくちゃ「伸縮」に優れている気がします。
対して Scala でのミックスインでは trait 側でミックスイン先の変数を参照するためには自分型アノテーションを記述する必要があるため、コンパイル言語としての制約を感じますね。
ざっくり言うと、ミックスインをナチュラルに扱える言語においてはDIのためのフレームワークは必須ではない、っていう事でしょうか。
DIコンテナという技術自体は格納したオブジェクトのライフサイクル管理という意味でそれはそれで役割があり、そういった特性を持った言語でも利用を検討すべき側面がありそうです。
後書き
今回はメインコンテンツは4時間くらいで書き上げましたが、なんとまぁ設定時間の倍です。なんとなーく、こういう順番で話を展開してあの話題を出すのにあそこのリンクを貼って〜というイメージを作って臨んだものの、実際の記述がぼんやりしたままという片手落ちにより文章を具体化する時間はあまり効率的ではなかったかなと。
ここまで読んでいただけた方、ありがとうございました。