せっかく『テスト駆動開発』を読むのだから写経をしながら、それにただ写経するだけというのもなんだし他の言語でやろうということで学習がてら 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
早速テストクラスを作る。
class MoneyTest {
@Testfun testMultiplication() {
val five: Dollar = Dollar(5)
five.times(2)
assertEquals(10, five.amount)
}
}
このレベルだとJavaと殆ど差異がない。ここでの Kotlin 的要素といえば以下のようなものがある:
- デフォルトの公開レベルは
public
(class
や fun
の前に修飾子をつけていない) - 変数宣言時に
val
をつける
val
/var
があり、前者は再代入不可、後者は再代入可
- 型は後置表記
- 型推論が効くので省略が可能。以後明示する必要がなければ全部省略する
以後進行にあまり関係のない Kotlin 要素はスルーしていくので各自適当にググってください。
最初のテストを書いたところで TODO リスト更新:
- [ ]
$5 + 10CHF = $10
(レートが 2:1 の場合) - [ ]
$5 * 2 = $10
- [ ]
amount
を private に - [ ]
Dollar
の副作用をどうする? - [ ]
Money
の丸め処理をどうする?
まずはコンパイルエラーを直す。 Dollar
クラスに amount
と times(Int)
があれば良い。
class Dollar(var amount: Int) {
fun times(multiplier: Int) {}
}
Kotlin はコンストラクタで引数に受けた値をそのままフィールドに代入するだけ、という場合はクラス名の後の()
に修飾子つきの引数を記述するとそのまま値を入れてくれる。今回 amount
はそれに当てはまるので Dollar(var amount: Int)
となった。
とりあえずコンパイルが通って、次はテストが red になる。
テストを通すには amount
が 10
を返してくれればいいのでとにかく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 パターンのお話から始まり、Dollar
を Value 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)
overridefun equals(other: Any?) = amount == (other as? Dollar)?.amount
}
class MoneyTest {
@Testfun testEquality() {
assertTrue(Dollar(5) == Dollar(5))
assertFalse(Dollar(5) == Dollar(6))
}
}
上記のコードで Dollar
同士を equals()
ではなく ==
で比較するように実装しているが、Kotlin の ==
は Javaの ==
とは異なり equals()
を用いた比較の糖衣構文でだからある。ちなみに equals()
と ==
は全く同じ機能ではなく、リファレンスによると a == b
は a?.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 の項目も潰せるようになる:
dataclass 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 {
@Testfun testMultiplication() {
val five = Dollar(5)
assertEquals(Dollar(10), five.times(2))
assertEquals(Dollar(15), five.times(3))
}
@Testfun testEquality() {
assertTrue(Dollar(5) == Dollar(5))
assertFalse(Dollar(5) == Dollar(6))
}
}
dataclass Dollar(privateval amount: Int) {
fun times(multiplier: Int) = Dollar(amount * multiplier)
}
ここまでのまとめ
『テスト駆動開発』の第4章までを Kotlin で進めてみた。ここまでは基本的に Javaと変わりなく、違いといえば言語間の記述方法の違いと data class くらいなので、基本的な流れが変わらない分この記事単体では面白さに欠けるものかも知れない。これはクラス単体の表現の仕方に Javaと Kotlin で変わりがないことの表れだとも言える。
次回以降は複数のオブジェクトの関係や抽象化などの話が加わり、Kotlin でできる表現の方法と Javaのそれとの違いが見えるようになって、環境の違いによる思考や実装の変化というこの一連の記事で示したいことを見せることができる、はず。