インターフェースで副作用を分離する

techlife.cookpad.com

この記事、わざわざgemを作ったりしていて、まあそのgem自体は便利そうだからいいと思うんですが、時刻以外の場合はどういうアプローチをしてるんでしょうか。そのたびにgemを作るんでしょうか。 そんなに悩まなくとも、オブジェクト指向の引き出しがあれば一瞬で片付く問題です。

package com.example

import java.time.LocalDateTime

// Clockインターフェース
trait Clock {
  def now(): LocalDateTime
}

// デフォルトのClock実装
trait DefaultClock extends Clock {
  def now(): LocalDateTime = LocalDateTime.now()
}

// 時刻を取得する時にはClockインターフェースを通して取得する
class SpecialContent(clock: Clock) {
  def enabled: Boolean = clock.now()
    .isAfter(LocalDateTime.of(2016, 7, 1, 0, 0, 0))
}
package com.example

import java.time.LocalDateTime

import org.scalatest.FunSuite

// 固定された時間を返すClockインターフェースの実装
class FixedClock(fixedDateTime: LocalDateTime) extends Clock {
  override def now(): LocalDateTime = fixedDateTime
}

class SpecialContentTest extends FunSuite {

  test("disabled at 2016-06-30 23:59:59") {
    val content = new SpecialContent(
      new FixedClock(LocalDateTime.of(2016, 6, 30, 23, 59, 59)))
    assert(!content.enabled)
  }

  test("enabled at 2016-07-01 00:00:01") {
    val content = new SpecialContent(
      new FixedClock(LocalDateTime.of(2016, 7, 1, 0, 0, 1))))
    assert(content.enabled)
  }

}

java.time.Clock というものが存在するので自分でClockを定義する必要はないんですが、そういうのがあるないの問題じゃないので。

Java/Scalaの世界とLLな世界を行ったり来たりして文化の違いに戸惑ったことがあるのですが、 こういう外部入力とかが絡んだ場合、Java/ScalaおじさんはOOPっぽくインターフェースを使って分離したりしようとするのに対して、 LLの世界だとクックパッドの記事中にも触れられているようにtimecopみたいにメソッドごと置き換えてしまうとか、個別のアプローチを取ることが多かったです。 オブジェクト指向は無駄だと主張する人が多いんですよねえ。

またScalaな世界でもOOPではなくモナドがどうという話になることがあります。 確かに参照透過であることは良いんですが別にファンクショナルなアプローチに頼らなくてもテストは書けるし、参照透過でもテストが辛いこともあるのでまあみんな仲良くしましょう。