(2021/12/24追記)
この記事に記載の方法は、有効活用できる場面がどうも限定的であるようです。
詳細は文末に追記しますが、愚直にjava.time.Clockをメンバ変数に追加する方法が、いちばん使いまわしが良さそうです。
プログラマはザ・ワールドが使いたい
現在日時を取得利用するプログラムでは、
- テストのときは取得日時を固定したい
というありふれた要望がつきまとう。
Kotlinで日時を扱うならjava.timeライブラリを使うことが多いが、java.timeライブラリで『テストのときは日時固定』を実現する場合、ちょっと面倒だ。
実現方法について、いくつかのサイトは java.time.Clock
を現在日時の取得時に渡せばOK、と案内している1。
// import java.time.Clock
class Something(
val clock: Clock // DIとかでもってくる
) {
fun doSomething() {
val today = LocalDate.now(clock) // clockのインスタンスを渡す
//...後略
}
}
方法としては正しそう…だけど、このインスタンスを参照するためのフィールドを、すべての現在日時の利用箇所に追加するのは、あまりスマートではなさそうである。
ところでKotlinには**interfaceの委任(interface delegation)**というテクニックがある。
これを利用して、別のアプローチがとれないか考えてみる。
委任してみる
1. interfaceと委任オブジェクトをつくる
// import java.time.Clock
interface ClockReference {
val clock: Clock
}
class ClockProvider : ClockReference, KoinComponent {
override val clock: Clock by inject()
}
java.time.Clock
型のフィールドを持つinterfaceを定義し、そのインターフェースを実装したclassを作る2。
objectはClock
型のフィールドをoverrideする。初期化はDIでおこなう。
サンプルでは、DIフレームワークとしてKoinを使用する。
KoinComponent
の実装は、依存注入の対象であることの宣言。by inject() は、DIにより依存を注入することの宣言。
2. Clockの利用クラスでinterfaceの実装を宣言、実装を委任
interface ISomething {
fun doSomethingWithDate()
}
class Something : ISomething, ClockReference by ClockProvider() {
override fun doSomethingWithDate() {
val today = LocalDate.now(clock) // ClockReferenceのフィールドを参照
println("Today is $today. Everything is fine.")
}
}
Clockの利用クラスにClockReference
の実装と、実装の委任を宣言する(ClockReference by ClockProvider()
)。
これでフィールドを追記することなく、Clock型のインスタンスを自由に扱えるようになる。
Clock型のインスタンスは、ClockProvider経由でDIで提供される。
テスト用のインスタンスをDIコンテナに登録すれば、時間を固定したClockや、Mockk等のライブラリによるMockを差し込むことが可能になる。
動作確認コード
fun main() {
prepareDi() // diコンテナにinstance登録。詳細は割愛
val something: ISomething = Something()
something.doSomethingWithDate()
// 実行結果:Today is {実行日}. Everything is fine.
}
気をつけたいポイント
-
ClockReference
の実装クラス(前掲の例でいうSomething)からClockへの依存が見えにくい。- もともと*LocalDate.now()やSystem.currentTimeMillis()*などの利用に制限がなかったなら、もともとシステム内時計への依存はスルーしていたわけなので、この場合はあまり問題ではなさそう。他方、暗黙裡の依存は基本的に排除する方針なら、この方法は適合しない。
-
ClockReference
の実装クラスは、クラス外部にclockフィールドが露出する。- インターフェース上のフィールドはpublicでしか宣言できないためだ。Clockのインスタンスは最低限かつimmutableなApiしか持たないので、実装クラス外からの参照も致命傷にはならなそうではある。このまま採用できるかは、アプリの開発方針だとかによる。
-
ClockProvider
は特定のDIフレームワークに依存する。- …ただ、DIフレームワークが変わったとして、
ClockReference
の利用クラスに変更の影響は殆どないはずではある。利用クラスはClockReference上のclockフィールドを利用するだけであって、フィールドがどのように提供されるかには関知しないからだ。とはいえ、DIに依存したクラスが、DIと分離されたクラスに登場するとヒヤリとする。この採用可否も、開発方針による。
- …ただ、DIフレームワークが変わったとして、
むすび
-
テストで時間止めてえな、と調べて読んだ『java.time.Clockで現在時刻を扱うUT』に触発されて書きました。
(minimal)cake pattern、として披露できるほど理解できていないので、Kotlin Bootcampで利用されていた「インターフェースの委任」というワードに逃げました。 -
Clock
はいろんなところで暗黙裡に利用されていたりするので、テスト時のClockのモッキングにはしばしば難があります(例:apiの呼び出し回数が把握しにくい)。
Clockのフィールドをinterface上に配置する代わりに、*now()*などの取得結果を返すApiをインターフェース上に整備するのも一案かもしれません。 - (書いといてアレですが)デメリットも目立つので、普通にフィールド足したらエエんとちゃうのォ~という気持ちの間で揺れています。ゴイケンオマチシテマ
当座の運用を経て (2021/12/24追記)
Clockを**『普通にフィールド(コンストラクタ引数)に足す』**のが、いちばん使いまわしやすそうです。
この記事の方法だと、ClockProviderの初期化がDI以外でできません。
テスト時にも同じDIを使うなら良いですが、別の方法でモックを差し込みたい場合、導線がないので詰みます。
得られるメリットにたいして、失う拡張性が大きすぎるように思います。
記事執筆時にDIアリのテストばかり書いており、一見アタリマエながら気づけませんでした。先んじて詰んだかた、万一いたら申し訳ない…。
Clockのスマートな扱いかたでこんなのあるよ、というのあればぜひぜひお知らせください…🙏
もし万一この方法に活路があるなら、その情報もぜひ。
-
たとえば:stackoverflow ↩
-
objectで実装してしまうと、たとえば自動テストを連続実行する場合に不都合が発生する。objectで生成されるシングルトンなインスタンスが、複数のテストで共有されてしまうのだ。テストごとにモックを差し替えるなど、基本的なニーズにこたえられない。 ↩