nashcft's blog

時々何か書く。

最近書いてないやつ

別段忙しくてニャーンであるとかそういう状態ではないのだけど全然日記的なもの書けてなかったり GitHub ガーデニングも真っ白殺風景になってたりしていますが私は元気です。

何していたかというと会社では毎日無理ない程度に進捗を出してるだけなのに帰宅すると疲れ果ててひたすら寝るということをしていたり起きている間は ÉKRITS Books I 読んでたりしていて、先月色々あった分の疲れを身体が認識し始めたのかなーと適当に考えて静かに過ごしているというかそうしかできないというか。
そういえば Packt Publishing がセールで電子書籍を $5 で売ってて、まだやってるっぽいので気になるけど Packt の本のクオリティに不安があって躊躇してる的なのがあったらいいタイミングだと思う。

www.packtpub.com

私はというと気がついたら11冊買ってたのでセールは恐ろしい。

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

前回

今回は11章まで進める。前回からの Money, Dollar, Franc にまつわるリファクタリングの続きで、今回分で一区切りがつく内容となっている。

現在のコードは以下の通り:

MoneyTest.kt

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))

        assertTrue(Franc(5) == Franc(5))
        assertFalse(Franc(5) == Franc(6))

        assertFalse(Dollar(5).equals(Franc(5)))
    }

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

Money.kt

data class Dollar(private val amount: Int) : Money(amount)
data class Franc(private val amount: Int) : Money(amount)

sealed class Money(private val amount: Int) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier)
        is Franc -> Franc(amount * multiplier)
    }
}

第8章 実装を隠す

この章ではまず times() 関数を纏める準備として返却値の型を Money に変更しているが、これは既に第6章で実装はともかく Moneytimes() 関数を移すところまで達成している。

次は Money のサブクラスのインスタンスを生成する時に直接それらを参照しないよう、それぞれの static factory method を作成する。Kotlin では Java のようにクラスに static method を設けることはできないが、同じような挙動をする関数を作る方法はいくつか用意されている*1。今回は companion object を使用する方法で実装する (以下のコードは Money 内部のみを抜粋したもの)。

sealed class Money(private val amount: Int) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier)
        is Franc -> Franc(amount * multiplier)
    }

    companion object {
        fun dollar(amount: Int): Money = Money.Dollar(amount)
        fun franc(amount: Int): Money = Money.Franc(amount)
    }
}

テストの方は直接 DollarFranc のコンストラクタを呼んでいた部分が factory method に置き換わっただけなので割愛する。

この変更で直接 Money のサブクラスを呼ぶ必要がなくなり、これらを削除する準備が進んだ。

TODO リストに1つ項目を追加して次の章に進む:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] Dollar と Franc の重複
  • [x] equals の一般化
  • [ ] times の一般化 -> WIP
  • [x] Franc と Dollar を比較する
  • [ ] 通過の概念
  • [ ] testFrancMultiplication を削除する?

第9章 歩幅の調整

この章ではこれまでサブクラスで表していた通貨の概念を別の方法で表現することで、サブクラスを消す準備を万全にしていく。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] Dollar と Franc の重複
  • [x] equals の一般化
  • [ ] times の一般化 -> WIP
  • [x] Franc と Dollar を比較する
  • [ ] 通過の概念
  • [ ] testFrancMultiplication を削除する?

この章は同じことを表現するのにやや寄り道っぽいことをしていて既読組からしたら冗長に感じるかもしれないが、「過程を眺める」のがこのシリーズの動機なので、章のタイトルになっている「歩幅の調整」の様子も残しておきたい。

まずはテストの追加。

class MoneyTest {

    // ...

    @Test
    fun testCurrency() {
        assertEquals("USD", dollar(1).currency())
        assertEquals("CHF", franc(1).currency())
    }

}

メソッド呼び出しでどの通貨かわかるようにしたいようなので、Moneycurrency() 関数を追加する。書籍の方ではサブクラスにも実装を追加しているが、私の実装では sealed class と when 式によって Money クラス内の変更だけで済む。

sealed class Money(private val amount: Int) {

    fun currency() = when (this) {
        is Dollar -> "USD"
        is Franc -> "CHF"
    }

    // ...
}

実装した後、メソッドじゃなくてフィールド変数でよくない? となり、書籍も実際そういう流れなのでそのようにしていく。これは実際 Java のコードでは currency() は消えていないのだけど、これが実質 currency をそのまま返す getter になってて、フィールドの値をそのまま返す getter だけ生えてるって val で定義したフィールドそのものじゃんとなり、 Kotlin 的にはフィールドにアクセスする記法が普通っぽいのでそれに従うという判断による。カスタム getter/setter を作っても使い方はフィールドアクセスと一緒だし。

まずは data class の方から。

data class Dollar(private val amount: Int,
                  val currency: String = "USD") : Money(amount)
data class Franc(private val amount: Int, 
                 val currency: String = "CHF") : Money(amount)

Kotlin では関数の引数やコンストラクタに書かれたプロパティにデフォルト値を設定できるので上記のような書き方ができる。これだけだと factory method が Money 型として返している都合上使っている側はキャストしないと currency を参照できないので Money にも currency を生やす。

data class Dollar(private val amount: Int,
                  override val currency: String = "USD") : Money(amount, currency)
data class Franc(private val amount: Int, 
                 override val currency: String = "CHF") : Money(amount, currency)

sealed class Money(private val amount: Int, open val currency: String) {
    // ...
}

コンストラクタの中では abstract にはできないので open にして、サブクラスの currency には override をつけ、スーパークラスに渡すように変更した。わざわざ override させる意義はないのだけど、スーパークラスのフィールドが private じゃないのでサブクラスの同名フィールドはアクセス範囲を狭められないし、かといって使わないからってサブクラスのフィールド名を雑なものにするのもちょっと... という気持ちもあり、まああとで消すのだし今はいいかということでこのまま進める。

次は通貨の種類を表す文字列を 外部から渡すようにしてコンストラクタの差異 (= デフォルト引数) を無くそうというもの。

data class Dollar(private val amount: Int,
                  override val currency: String) : Money(amount, currency)
data class Franc(private val amount: Int, 
                 override val currency: String) : Money(amount, currency)

sealed class Money(private val amount: Int, open val currency: String) {

    // ...

    companion object {
        fun dollar(amount: Int): Money = Money.Dollar(amount, "USD")
        fun franc(amount: Int): Money = Money.Franc(amount, "CHF")
    }
}

これによってサブクラスの currency に対するデフォルト値がなくなったため times() 関数内でも何か値をあげるようにしないといけなくなり、それを回避するために書籍に倣って factory method を呼ぶように変更する

sealed class Money(private val amount: Int, open val currency: String) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> dollar(amount * multiplier)
        is Franc -> franc(amount * multiplier)
    }

    // ...
}

これでこの章でやることは終わり。途中 Money.kt の中身は変更に関係ない部分を省略して記述したため、小さいファイルとはいえ一応全体を載せておく。

data class Dollar(private val amount: Int,
                  override val currency: String) : Money(amount, currency)
data class Franc(private val amount: Int, 
                override val currency: String) : Money(amount, currency)

sealed class Money(private val amount: Int, open val currency: String) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> dollar(amount * multiplier)
        is Franc -> franc(amount * multiplier)
    }

    companion object {
        fun dollar(amount: Int): Money = Money.Dollar(amount, "USD")
        fun franc(amount: Int): Money = Money.Franc(amount, "CHF")
    }
}

第10章 テストに聞いてみる

この章の目標は times() メソッドを Money クラス内に引き上げることなのだが、ご存知の通り既に Money にいるので、やることは times() の実装を修正するだけである。具体的には factory method を呼ぶようにしたのをやめて、コンストラクタに自身の currency を与えるようにする。

sealed class Money(private val amount: Int, open val currency: String) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier, currency)
        is Franc -> Franc(amount * multiplier, currency)
    }

    // ...
}

これで times() の分岐後の処理が同じ形になったので、分岐の意味も殆ど無くなったし分岐を消して Money を返すようにする... と言いたいところだが、 Money は sealed class なので直接インスタンス化できない。ここでは一度見送って、次の章に進む。
ところでこの章で追加される予定だったテストケースは equals() を自分で実装してないしどうしようもないのでパスした。

第11章 不要になったら消す

もう DollarFranc は必要なくなったので削除し、直接 Money を使えるようにする。Dollar たちと同じように使えるようにするため、 Money は sealed class から data class へ変更する。

data class Money(private val amount: Int, val currency: String) {

    fun times(multiplier: Int) = Money(amount * multiplier, currency)

    companion object {
        fun dollar(amount: Int) = Money(amount, "USD")
        fun franc(amount: Int) = Money(amount, "CHF")
    }
}

とてもスッキリした。あとは不要になっているテストを削除する。対象は

  • testEquality() 内で Franc 同士の比較をしていた assertion (USD, CHF を表現するクラスが同一になったので重複する検査となったため)
  • testFrancMultiplication() (これも重複)

そして USD と CHF の比較が失敗するという assertion で何も書き足さなくても == が使えるようになったのでそのように変更する。

これで Moneyリファクタリングが終わり、本題である通貨の足し算に進めるようになった。

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [x] Dollar と Franc の重複
  • [x] equals の一般化
  • [x] times の一般化
  • [x] Franc と Dollar を比較する
  • [x] 通過の概念
  • [x] testFrancMultiplication を削除する?

ここまでのまとめ

第11章終了時点でのコード:

MoneyTest.kt

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))

        assertFalse(dollar(5) == franc(5))
    }

    @Test
    fun testCurrency() {
        assertEquals("USD", dollar(1).currency)
        assertEquals("CHF", franc(1).currency)
    }
}

Money.kt

data class Money(private val amount: Int, val currency: String) {

    fun times(multiplier: Int) = Money(amount * multiplier, currency)

    companion object {
        fun dollar(amount: Int) = Money(amount, "USD")
        fun franc(amount: Int) = Money(amount, "CHF")
    }
}

ここまで Money にまつわるリファクタリングを続けてきたが、実装の過程においては「times() の一般化」は sealed class で実装したことによって殆どやることがなかった点と、 currency の実装周りで Java 版とは異なる苦労をしたのが私の中で印象的だった一方、出来上がったコードは第4章時点のものに近いもの、つまり Java 版とあまり変わらないものに落ち着いているのも興味深い。次回以降も極力 Kotlin の機能を使うようにして Java 版との違いを見ていくようになっているのでお楽しみに。
ところで Elm 版の記事では今回追加した通貨の概念を型として表現するように変更を加えており、 Kotlin*2では enum class を用いて同様の表現ができるのだが、写経をやってた当時色々考えて結局最後に変更するまで文字列のままでいた。これは本筋とは関係ないので前回の Money を sealed class で実装した時のこととまとめて一連の記事の最後に振り返りみたいな感じで触れるつもり。まあ同じ理由だし、 sealed class はクラスの enum のようなものだし。

続き:

*1:この記事で採用している companion object の他に package level function として定義する方法もある

*2:というか Java もできるのだが

週末までにやったこと

週の頭に今週これやろうとメモってたことに対する振り返り

TODO に書いてあったこと

Binder transaction buffer のサイズについて再調査 & 記事の加筆修正

やってない 😇
来週末までに終わらせる。

TDD_kotlin: 14章を終わらせる

第I部の終盤だし1章1章が結構かかるかなーという予想に反して分量が少なかったので勢いで進めてたら第I部終わった。

Kotlin で『テスト駆動開発』をやった過程についてブログを書く (1~4章?)

7章まで書いた:

"Reactive Android Programming" の写経レポジトリを作成して chapter 1 まで終わらせる

レポジトリを作ってプロジェクトのセットアップをするところまでは終わらせた。Chapter 1 の内容に対する進捗としては半分。
欲を出してこっちも Kotlin でやろうとして色々調べていたら時間がかかってしまった。

書いてないことでやったこと

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

前回 nashcft.hatenablog.com

今回は第5章から第7章まで進める。
これまでの過程で書いたコードは以下の通り。前回の脚注で触れているが実際に私が取り組んだ時のものに対して順序の再構成やコードのブラッシュアップなどを行っているため、レポジトリのコードやコミットの流れとは異なっている部分がある。

MoneyTest.kt

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))
    }
}

Dollar.kt

data class Dollar(private val amount: Int) {

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

第5章 あえて原則を破るとき

これまでは1種類の通貨に関して注目していたが、この章から本題となる「多国通貨の足し算」を行えるようにするための準備が始まる。
まずはもう1つの通貨としてフラン (Franc) を追加し、これまで扱ってきたUSドルと同じように動作させたい:

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

この章で行うことは Franc クラスに対して Dollar に対して書いたものと同様のテストを追加し、 Dollar をコピペして Franc クラスを作るというもの。短時間で確実に前進できる手段を選ぶためならばこんな行いも許容される。ただしその後きっちりリファクタリングをするならば、だが。

class MoneyTest {

    // ...

    @Test
    fun testFrancMultiplication() {
        val five = Franc(5)
        assertEquals(Franc(10), five.times(2))
        assertEquals(Franc(15), five.times(3))
    }
}
data class Franc(private val amount: Int) {

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

さて、これまでは殆どの場合テストを書くところからコードをリファクタリングするところまでをセットに進めてきたが、この点に関してはこれから第11章にかけてのより大きな流れでリファクタリングをしていくことになるので、とにかく先に進もう。

犯した罪を追加した現時点での TODO リスト:

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

第6章 テスト不足に気づいたら

この章では前章で作ってしまった重複を消していくために、まず共通の親クラスを作りそれを継承して equals() の一般化を行う、という進め方をする。
ところで私は Dollar, Franc とも data class として実装したので、equals() を自分で実装しておらず共通化するものがないのだった。

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

とはいえ後のことも考えて、書籍の内容に則り Money クラスを作成し DollarFranc に継承させ、ついでに本章で追加される assertion も足しておく。

// Dollar.kt
data class Dollar(amount: Int) : Money(amount) {

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

// Franc.kt
data class Franc(amount: Int) : Money(amount) {

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

// Money.kt
open class Money(private val amount: Int)

Kotlin のクラスはデフォルトで final なので継承できるようにするためには open を足しておく必要があることに注意。

ところで実際に写経をやっていた時は書籍と Elm 版記事の両方を開きながら進めていて、この章に取り組んでいた時に Elm 版の同じ章のところを見返してあー Union Type いいよなーパターンマッチいいよなー使いたいなーってなって探したら sealed class というのがあってそのものズバリな感じの使用感だったのでそれを使った。これによって times() の宣言自体は Money の中で行えるため、 Elm 版と同じく共通化を一歩先に進められるようになる。

// Money.kt
package money

data class Dollar(private val amount: Int) : Money(amount)
data class Franc(private val amount: Int) : Money(amount)

sealed class Money(private val amount: Int) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier)
        is Franc -> Franc(amount * multiplier)
    }
}

Sealed class の主な特徴は以下の通り:

  1. 宣言されたファイルの中でしか sealed class を継承することができない
  2. Constructor がデフォルトで private で、private でない constructor を持つことができない
  3. when 式と合わせて Union Type っぽくパターンマッチすることができる
    • when 式で検査対象に sealed class を渡して is <class> で分岐させると、その sealed class を継承したクラスだけ並べれば網羅されたことになり、 else を書く必要がない

上のコードでは times() は 3. によって関数内で when 式を用いて自身をパターンマッチにかけ、マッチしたクラスに計算を適用したものを返すように書くことで1つの式にまとめ上げることができ、 Money の関数として持ち上げることに成功している。パターンマッチのケースも Money を継承した DollarFranc だけでよい。このような方針で2017年現在の*1 Java でやろうとすると times() メソッド内では instanceof を用いた分岐フローが積み重なる嫌なコードになってしまうのでそんなアプローチを採用するわけにはいかず、まだ Money に持ってくることはできない。
また、上記以外にも 2. によって Money クラスを直接インスタンス化して使用することを防いでおり、ユーザが不用意な使用をできないようにもなった。これはこの一連の記事の最後で小話に使うかもしれない。

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

第7章 疑念をテストに翻訳する

DollarFranc を比較するという内容。ここも data class としてそれぞれを実装したため異なるクラスで比較しても失敗するようになっている。
ところでここでテストを追加する時、 Dollar(5) == Franc(5) と記述すると以下のメッセージと共にコンパイルエラーが発生することに気がつく。

Operator '==' cannot be applied to 'Dollar' and 'Franc'

これを Dollar(5).equals(Franc(5)) とするとコンパイルが通るし結果も本章で期待するものになる。== は前回書いたように a?.equals(b) ?: (b === null) の糖衣構文のようなものだがどうやら完全に同じ挙動をするわけではなく、直系の親子クラスでないとコンパイルレベルで受け付けてくれないようだ。気持ち悪いけど今回はここだけ as Money をつけるなり equals() を使うなりしておくことにする。

ここまでのまとめ

現在のコードの様子:

MoneyTest.kt

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))

        assertTrue(Franc(5) == Franc(5))
        assertFalse(Franc(5) == Franc(6))

        assertFalse(Dollar(5).equals(Franc(5)))
    }

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

Money.kt

package money

data class Dollar(private val amount: Int) : Money(amount)
data class Franc(private val amount: Int) : Money(amount)

sealed class Money(private val amount: Int) {

    fun times(multiplier: Int) = when (this) {
        is Dollar -> Dollar(amount * multiplier)
        is Franc -> Franc(amount * multiplier)
    }
}

TODO リスト:

  • [ ] $5 + 10CHF = $10 (レートが 2:1 の場合)
  • [x] $5 * 2 = $10
  • [x] amount を private に
  • [x] Dollar の副作用をどうする?
  • [ ] Money の丸め処理をどうする?
  • [x] equals()
  • [x] hashCode()
  • [x] null との等値性比較
  • [x] 他のオブジェクトとの等値性比較
  • [x] 5CHF * 2 = 10CHF
  • [ ] DollarFranc の重複
  • [x] equals の一般化
  • [ ] times の一般化 -> WIP
  • [x] FrancDollar を比較する
  • [ ] 通過の概念

今回は通貨のクラスを1つ増やし、それらに共通の親クラスを作って継承させるところまで進めた。第6章では書籍に載ってるコードの構成から離れて sealed class を用いた実装を行ってみた。今回は主な特徴の紹介程度の使い方ではあるが、その便利さから今後も使われることになるので覚えておいてほしい。

続き:

*1:Java もパターンマッチを実装する提案 (http://openjdk.java.net/jeps/305) が上がっていて、早くて 10 から実装されるかもしれない

穏やかに週末をむかえる

直近の日記としてのブログで忙しいとかつらいとかニャーンとか言ってた原因の切羽詰まった状況が木曜で終わり、その開放感に任せてTDD写経のブログを書き上げた後、とても穏やかな気持ちでむかえた今週最後の平日はそれなりな進捗と共にとても穏やかに過ぎ、帰る前にちょっとバタバタしたのをとても穏やかな気持ちで対応して帰りしなに寄り道してラーメンを食べて帰宅して諸々の後に今がありこれから寝ます。Rxの勉強はやり損ねました。土曜日は幕張までガルパンを観に行きます。おわり。

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周目の世界として見ていただきたい。

+メモ書き

前回のブログで余裕があるとか言ったな、あれは嘘だ。嘘だったんだ... Binder transaction buffer の記事修正は今週末にやろう...

先週も結局ヒイヒイ言いながら開発タスクをやっつけてて、今週も厳しそうな雰囲気がある。そんなこんなもあり昨日今日とバス係数についてふわふわと考えているような状態になっていて、それは色々な事があるからです。

ところで先月頭に始めた『テスト駆動開発』の写経会、最近は参加者各位の多忙により全く進められておらず、とはいえ皆の予定が合うまで延期というのではつまらなくなってしまい先週末から1人で進め始めてしまう事にした。

github.com

レポジトリ名の通り Kotlin で進めているのだが割と面白いことになっていると感じているのでElmで進めている方がやっているようにブログに過程をまとめようと思う。章の区切りも合わせようかな。第一弾は今週末までに書く。

今回ブログを書いたのは日記的なものを書くというよりもTODOを書き残しておこうという理由で、今週末までにやることを以下にまとめておく

  • Binder transaction buffer のサイズについて再調査 & 記事の加筆修正
  • Kotlin で『テスト駆動開発』をやった過程についてブログを書く (1~4章?)
  • TDD_kotlin: 14章を終わらせる
  • "Reactive Android Programming" の写経レポジトリを作成して chapter 1 まで終わらせる

オッ、なんか増えましたね...