かとじゅんの技術日誌

技術の話をするところ

トレイトで契約を表現してみる

Javaで契約をサポートする
Googleが開発したJavaデバッグを簡単にする新技術「cofoja」
は記憶に新しいところですが、
まぁ、そこまでこだわるところでもないのですが、アノテーションに記述する事前条件や事後条件が文字列なのでちょっと残念かなと。

Scalaのトレイトを使えば同じようなことができるはずなので、勢いでやってみた。後悔はしていない。たぶん。
以下がコード。

package test

import org.scalatest.FunSuite

class ListMergeTest extends FunSuite {

  // インターフェイス
  trait ListMerge {
    def merge(left: List[Int], right: List[Int]): List[Int]
  }

  // 契約
  trait ListMergeContract extends ListMerge {
    abstract override def merge(left: List[Int], right: List[Int]): List[Int] = {
      require(left == left.sort(_ < _))
      require(right == right.sort(_ < _))
      super.merge(left, right) ensuring (_ == (left ++ right).sort(_ < _))
    }
  }

  // よい実装
  class SimpleListMerge extends ListMerge {
    def merge(left: List[Int], right: List[Int]): List[Int] = {
      (left ++ right).sort(_ < _)
    }
  }

  // よくない実装
  class BadListMerge extends ListMerge {
    def merge(left: List[Int], right: List[Int]): List[Int] = {
      left ++ right
    }
  }

  test("leftが事前条件を破っているケース") {
    val target = new SimpleListMerge with ListMergeContract

    intercept[IllegalArgumentException] {
      target.merge(List(3, 1), List(2, 4))
    }
  }

  test("rightが事前条件を破っているケース") {
    val target = new SimpleListMerge with ListMergeContract

    intercept[IllegalArgumentException] {
      target.merge(List(1, 3), List(4, 2))
    }
  }

  test("事後条件を破っているケース") {
    val target = new BadListMerge with ListMergeContract

    intercept[AssertionError] {
      target.merge(List(1, 3), List(2, 4))
    }
  }

  test("正常に動作するケース") {
    val target = new SimpleListMerge with ListMergeContract
    val left = List(1, 3)
    val right = List(2, 4)
    val result = target.merge(left, right)

    assert(result == (left ++ right).sort(_ < _))
  }


}

ListMergeContractのが契約を表すトレイトです。事前条件はrequireで表現しています。違反している場合はIllegalArgumentExceptionがスローされます。事後はensuringでAssertionErrorがスローします。

メリットとしては、契約がオブジェクトになっているので、理解しやすいというのはあると思います。ただ、AOPみたいに契約を織り込むことになるので、インスタンス化するときに "with ListMergeContract" を指定するので、忘れてしまうとか、付けるのが面倒ですね。

忘れるのがいやなら、コンパニオンオブジェクトを使いましょう。

  object SimpleListMerge {
    def apply() = new SimpleListMerge with ListMergeContract
  }

  test("ファクトリを使う方法") {
    val target = SimpleListMerge() // SimpleListMerge.apply()を呼び出す
    val left = List(1, 3)
    val right = List(2, 4)
    val result = target.merge(left, right)

    assert(result == (left ++ right).sort(_ < _))
  }

ファクトリを導入しておけば、この方法による計算コストがパフォーマンスに悪影響を与えるなら、with ListMergeContractを外してしまえばいい。そもそも、プロダクトコードに含める必要がなければ、テストクラスだけでwith ListMergeContractを使えばいい。
という感じだと思います。

以上、ご参考まで。

あわせて読みたい
ScalaにAOPをやらせてみる - じゅんいち☆かとうの技術日誌