2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Kotlin] インターフェースの委任によるjava.time.Clockの利用

Last updated at Posted at 2021-12-04

(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と分離されたクラスに登場するとヒヤリとする。この採用可否も、開発方針による。

むすび

  • テストで時間止めてえな、と調べて読んだ『java.time.Clockで現在時刻を扱うUT』に触発されて書きました。
    (minimal)cake pattern、として披露できるほど理解できていないので、Kotlin Bootcampで利用されていた「インターフェースの委任」というワードに逃げました。
  • Clockはいろんなところで暗黙裡に利用されていたりするので、テスト時のClockのモッキングにはしばしば難があります(例:apiの呼び出し回数が把握しにくい)。
    Clockのフィールドをinterface上に配置する代わりに、*now()*などの取得結果を返すApiをインターフェース上に整備するのも一案かもしれません。
  • (書いといてアレですが)デメリットも目立つので、普通にフィールド足したらエエんとちゃうのォ~という気持ちの間で揺れています。ゴイケンオマチシテマ:innocent:

当座の運用を経て (2021/12/24追記)

Clockを**『普通にフィールド(コンストラクタ引数)に足す』**のが、いちばん使いまわしやすそうです。

この記事の方法だと、ClockProviderの初期化がDI以外でできません。
テスト時にも同じDIを使うなら良いですが、別の方法でモックを差し込みたい場合、導線がないので詰みます。
得られるメリットにたいして、失う拡張性が大きすぎるように思います。

記事執筆時にDIアリのテストばかり書いており、一見アタリマエながら気づけませんでした。先んじて詰んだかた、万一いたら申し訳ない…。
Clockのスマートな扱いかたでこんなのあるよ、というのあればぜひぜひお知らせください…🙏
もし万一この方法に活路があるなら、その情報もぜひ。

  1. たとえば:stackoverflow

  2. objectで実装してしまうと、たとえば自動テストを連続実行する場合に不都合が発生する。objectで生成されるシングルトンなインスタンスが、複数のテストで共有されてしまうのだ。テストごとにモックを差し替えるなど、基本的なニーズにこたえられない。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?