nashcft's blog

時々何か書く。

Kotlin で『テスト駆動開発』を進める (第1章 - 第4章)

せっかく『テスト駆動開発』を読むのだから写経をしながら、それにただ写経するだけというのもなんだし他の言語でやろうということで学習がてら Kotlin で取り組んでいたのだが、これが中々面白かったので取り組んだ過程を、ある程度整理した上で*1書いてまとめる事にした。

大雑把にいうと以下の記事に影響を受けており、章の区切りなどもこちらに沿うようにしている。

qiita.com

レポジトリは↓

github.com

なぜ過程を書くのか?

もともと『テスト駆動開発』の内容は、新訳版の翻訳者である t_wada さんのブログ記事にもある通り、著者 Kent Beckテスト駆動開発の手法を用いながらインクリメンタルにプログラムを書き進めていく時の思考の推移を辿っていくような構成になっている。

本書の第I部、第II部は、ペアプログラミングのような語り口で、そのときどきの思考の言語化を挟みながら、コードがだんだん育っていくという構成を採っています。

この「思考」が、使用した言語によって変化させられる様であるとか、その結果として書き上げられるコードの形にも表れるところがとても興味深く感じられたので、私自身が辿った思考の流れも残しておけば誰かにとって参考になったり興味深い読み物になったりするのではないかなあと思ったからである。

進め方

Qiita の記事の方針に沿って本の内容と変わらないところは TODO リストとコードだけで進め、Kotlin特有の何かや思考が本の内容と異なった部分に関してはそれについて書いていく。

事前準備

プロジェクトのセットアップは各自適当にやってください。私はこれを参考に IntelliJ IDEA で build.gradle を書いていたら色々サジェストしてくれたので結果的に このようになった。

第1章 仮実装

最初の TODO リストは以下の通り:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [ ] $5 * 2 = $10

早速テストクラスを作る。

// MoneyTest.kt
// package, import は省略

class MoneyTest {

    @Test
    fun testMultiplication() {
        val five: Dollar = Dollar(5)
        five.times(2)
        assertEquals(10, five.amount)
    }
}

このレベルだとJavaと殆ど差異がない。ここでの Kotlin 的要素といえば以下のようなものがある:

  • デフォルトの公開レベルは public (classfun の前に修飾子をつけていない)
  • 変数宣言時に val をつける
    • val/var があり、前者は再代入不可、後者は再代入可
  • 型は後置表記
    • 型推論が効くので省略が可能。以後明示する必要がなければ全部省略する

以後進行にあまり関係のない Kotlin 要素はスルーしていくので各自適当にググってください。

最初のテストを書いたところで TODO リスト更新:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [ ] $5 * 2 = $10
  • [ ] amount を private に
  • [ ] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?

まずはコンパイルエラーを直す。 Dollar クラスに amounttimes(Int) があれば良い。

// Dollar.kt
class Dollar(var amount: Int) {

    fun times(multiplier: Int) {}
}

Kotlin はコンストラクタで引数に受けた値をそのままフィールドに代入するだけ、という場合はクラス名の後の() に修飾子つきの引数を記述するとそのまま値を入れてくれる。今回 amount はそれに当てはまるので Dollar(var amount: Int) となった。

とりあえずコンパイルが通って、次はテストが red になる。
テストを通すには amount10 を返してくれればいいのでとにかく10を返すようにする。
テストが green になったらリファクタリングtimes() のなかで amount を変更するようにする。

class Dollar(var amount: Int) {

    fun times(multiplier: Int) {
        amount = 5 * 2
    }
}

数字のハードコーディングになっているところをメンバや引数に置き換えて重複を排除していく。この辺は本の内容と一緒。

class Dollar(var amount: Int) {

    fun times(multiplier: Int) {
        amount *= multiplier
    }
}

この間コードを修正するたびにテストを実行して green であることを確認し続ける。ここまでくれば TODO リストの項目を1つ完了にできる。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [ ] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?

第2章 明白な実装

本の内容と変わりがないのでスキップ。

class Dollar(val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}

amount に再代入しなくなったので var から val に、また times() は値を返却する1行の関数になったのでブロック表記から式表記に変更できた。

TODO リスト更新:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?

3章 三角測量

Value Object パターンのお話から始まり、DollarValue Object として扱うための TODO (equals(), hashCode()) がリストに項目が追加される。
これらの事情は Kotlin も一緒なので本のコードとほぼ同様に equals() 実装して、最後にまた TODO リストを更新。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [ ] hashCode()
  • [x] null との等値性比較
  • [ ] 他のオブジェクトとの等値生比較
class Dollar(val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)

    override fun equals(other: Any?) = amount == (other as? Dollar)?.amount
}
class MoneyTest {

    // ...

    @Test
    fun testEquality() {
        assertTrue(Dollar(5) == Dollar(5))
        assertFalse(Dollar(5) == Dollar(6))
    }
}

上記のコードで Dollar 同士を equals() ではなく == で比較するように実装しているが、Kotlin の ==Java== とは異なり equals() を用いた比較の糖衣構文でだからある。ちなみに equals()== は全く同じ機能ではなく、リファレンスによると a == ba?.equals(b) ?: (b === null) と同等の判定を行うとのこと。== に関しては後でちょっと話題になる。
あと Kotlin は基本的に null を扱う場合は ? が最後についた型や演算子を使ってNPEを吐かないように実装しないとコンパイラに怒られ、結果的に equals() の実装は null との等値性比較をクリアしている。

ところで Value Object を作るたびに equals() とか hashCode() とかいちいち実装しなきゃいけないとか等値性比較で色々考慮しなきゃいけないの大変面倒で、Kotlin に来てまでジャバ界のつらみを背負いたくないとなるが、そこはさすが最近の言語ということで、 data class というものがあり、これは以下の特徴を持っている:

  • Data class として定義するとそのクラスに適した equals(), hashCode() が自動的に付与される
  • toString()"<Class name>(<field>=<value>[, <field>=<value>...])" という出力のものが実装される

Dollar をただの class から data class に変更すると、テストをそのままにさっき実装した equals() を捨てることができ、さらにおまけでこの章の最後に追加された TODO の項目も潰せるようになる:

data class Dollar(val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}
  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [ ] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値生比較

Data class, Value Object を作る手間がだいぶ省けるので地味に嬉しい機能だ。

第4章 意図を語るテスト

ここはやることに変わりなし。 Dollar(val amount: Int)Dollar(private val amount: Int) とすると amount の公開レベルを private にできる。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値生比較

この時点でのコード:

class MoneyTest {

    @Test
    fun testMultiplication() {
        val five = Dollar(5)
        assertEquals(Dollar(10), five.times(2))
        assertEquals(Dollar(15), five.times(3))
    }

    @Test
    fun testEquality() {
        assertTrue(Dollar(5) == Dollar(5))
        assertFalse(Dollar(5) == Dollar(6))
    }
}
data class Dollar(private val amount: Int) {

    fun times(multiplier: Int) = Dollar(amount * multiplier)
}

ここまでのまとめ

テスト駆動開発』の第4章までを Kotlin で進めてみた。ここまでは基本的に Java と変わりなく、違いといえば言語間の記述方法の違いと data class くらいなので、基本的な流れが変わらない分この記事単体では面白さに欠けるものかも知れない。これはクラス単体の表現の仕方に Java と Kotlin で変わりがないことの表れだとも言える。
次回以降は複数のオブジェクトの関係や抽象化などの話が加わり、Kotlin でできる表現の方法と Java のそれとの違いが見えるようになって、環境の違いによる思考や実装の変化というこの一連の記事で示したいことを見せることができる、はず。

続き:

nashcft.hatenablog.com

*1:実際に取り組んだ時は通り過ぎて暫くしてから、またこれを書いている最中に気づいたりより Kotlin っぽい書き方を知ったりしてコードを書き直したり記事の内容に盛り込んだりということがままあったので順番通りに書こうとすると脱線が激しく記事としてまとまりがなくなるため、それらを必要な地点で記述できるように並び順を直している。そのためレポジトリのコミットログとこのまとめの内容に乖離が発生しており、大雑把にいうと GitHub のレポジトリは1周目、この記事は2周目の世界として見ていただきたい。