Part1 Moneyオブジェクトの例 - noriyagi/TestDrivenDevelopment GitHub Wiki

1.複数通貨のMoney

ますはリストからテストを書く。
タスクを選ぶ基準は「小さく始める」ということ。
最初のテストの時点でDollarクラスのメンバとメソッドが頭の中で設計済みになっているようだ。

  1. コンストラクタに数値を渡してDollarオブジェクトを生成する。
    貨幣によってオブジェクトを分ける事によって単位を吸収する。(Dollarはドルを、Yenは円を扱うクラスというように。dollar*90 = yen)
  2. timesメソッドが乗算を担当するメソッド。計算結果はamountに格納。
  3. amountは計算結果を保持するフィールド。

TDDサイクル

  1. テストを少しだけ作成する。
  2. 全てのテストを実行し、失敗を確認する。
  3. 小さな修正を行う。
  4. テストを実行し、成功させる。
  5. 重複を取り除くためにリファクタリングを行う。

2.オブジェクトの退化

TDDサイクルその2

  1. テストを作成する。
    コード内に操作がどのように出現するかを頭の中で考える。
    ストーリーは書いてあるのでそこからインターフェースを考案する。
  2. テストをパスさせる。
    素早くグリーンバーに変えることが最優先となる。
    明確でシンプルな解決策をコードにする。
    複雑なものしかない場合はとにかくグリーンバーにする。
  3. コードを正しくする。
    持ち込んだ重複を取り除く。
素早くグリーンにするためのテクニック
  1. 仮実装
    定数を返し、本物のコードを得るまで、徐々に変更していく。
  2. 明白な実装
    実際の実装を記述する。
### 3.全てに対する等価 別名参照の問題を解決するために、バリューオブジェクトを使用する。
暗黙的意味として、新しいオブジェクトを返さなくてはならないこと、equalsを実装すべきであることがあげられる。
→equals()はオブジェクト(通貨インスタンス)を受け取り、amountメンバ同士を比較している。
 通貨の比較:金額が同じかどうか、ということ。

2つ以上の例がある場合だけ、コードを一般化する。ここでは三角推量を用いる。
したがって、$5=$5 に加え、$5!=$6を例に加え一般化する。

4.プライベート化

equals()を定義したことで、テストを書き換えることができる。
Dollarオブジェクト同士を比較したい。
→amountインスタンス変数をDollarクラスのみで使用することになるためprivate化できる。

ここで行った事
開発したばかりの機能(equals()を使用してテストを改善した) テスト対象クラスのオブジェクトの新機能を使用してテストとコードの結合度を削減した。

5.「フランク」に話す

最初のテスト「$5+10CHF=$10」のような複数通貨の組み合わせを実装したいが、
そこに至るステップは以前急激な飛躍に見える。(TDDの理念として、小さくまわすことが求められている)
まず、フラン通貨を表現するオブジェクトを実装する。
Dollar、Franc間で多くの重複が見られるので、次のステップで重複を取り除く。

6.再度、全てに対する等価性

5.では新しいテストケースを動作させるために大量の重複を生み出した。
一般化により、コードをきれいにする。
コードをきれいにする→リファクタリング には必ずテストコードが要る。テストの無いコードをリファクタリングした場合、操作によって何が起きているのは把握できない。(進んでいるのか、壊れているのか)
リファクタリングをするなら、まずは変更箇所についてのテストを作成することから始めること。

  1. 共通部を取り出して、スーパークラスを作成する。
  2. 抽出できそうなメソッドの変数クラスをスーパークラスに変更していく。(Dollar→Money、Franc→Money)
  3. 一時変数も変更すると同一内容のメソッドになる。
  4. メソッドをスーパークラスに移動する。

7.りんごとみかん

DollarとFrancを正しく比較できていない事が判明。
→amountを比較していただけだったため。
ここではamountとクラスを比較することで対処したが、本来は財務のドメインを基準に判定したい。
→しかし、未だ通貨の基準がないためやむ終えず対応している。(TDD原則に従っている)

この章で行ったこと

  1. 悩ましい障害をテストに変換した(Franc、Dollarの比較)
  2. 合理的だが完全でない方法でテストを動作させた(getClass()を使用している)
  3. よりよい動機付けを得るまでこれ以上の設計を導入しないことを決めた。(シンプルなステップでないため)
### 8.オブジェクトの生成 times()の実装が似ていることに注目する。
Franc、Dollarがサブクラスとして存在を正当化できるほどの操作を行っていない。
→削除の方向に向かっている。
→サブクラスへの直接参照を減らせばサブクラスの削除に近付ける。
テスト中のDollarをMoneyに変更していくと、times()の使用箇所で「Moneyにtimes()が実装されていない」ことが分かる。
ここでは一度、Moneyを抽象クラスに、times()を抽象メソッドにする。(後々、times()はスパークラスで実装するつもり)
テストコードにファクトリメソッドを適用すると分かるが、Dollarというサブクラスの存在を使用側に知らせないようにできてる。

この章で行った事

  1. メソッド(times())のシグニチャを一致させることで徐々に重複を取り除いた。
  2. メソッド宣言をスーパークラスにまとめた
  3. ファクトリメソッドを導入する事によってサブクラスの存在をテストコードから分離した。
  4. サブクラスを削除すると不要になるテスト(Francのmultiplicationテスト)を見つけた
### 9.生きている時(times) 通貨の概念について取り組む。
dollar、francを生成し、通貨属性を取得するテストを作成する。
  1. currency()を実装する。ここでは定数文字列を返却しておけばいい。(まずは)
  2. 両方の実装を同じにしたい(親に移す事を考えつつ作業するという事)のでサブクラスのコンストラクタに定数文字列を持たせる。
  3. 両方のクラスに反映する。
  4. currency()は実装が一致しているので親クラスに移動する。
  5. 定数文字列をファクトリに移動すればコンストラクタが一致する事に気付く。
  6. コンストラクタに引数(通貨属性をもらう)を追加する。
  7. コンストラクタを呼び出しているところが壊れるのでnullをわたしておく。
  8. times()が未だにコンストラクタを使用していたのでファクトリを使用させる。
  9. ファクトリメソッドがコンストラクタを呼び出すときに、それぞれの通貨属性を渡すようにする。
  10. Dollarにも反映する。
  11. コンストラクタの実装が一致したので、親クラスに移動する。

この章で行った事

  1. 呼出し元(ファクトリメソッド)に相違点を移動する事によって、コンストラクタの実装を一致させた。
  2. times()にファクトリメソッドを使用するリファクタリングを行った。
  3. 同一のコンストラクタをスーパークラスに移動した。

10.興味深い時(times)

times()の実装は似ているが、まだ同一ではないので整理していく。

  1. ファクトリメソッドの返却をインライン化してみる。(Franc,Dollarのファクトリをそれぞれ呼び分けているため、返却値が貨幣に依存する(DollarはMoney.dollarをFrancはMoney.francを呼び出している))
    ex)Dollar
    「return Money.dollar(amount * multiplier)」
    →「return new Dollar(amount * multiplier, "USD")」
  2. "USD"をcurrentインスタンス変数に置き換える。(currentはインスタンス生成時に設定されている。)
  3. times()がMoneyを返却するようにする。
    ex)Dollar
    「return new Money(amount * multiplier)」
  4. テストが失敗するが、equals()がクラスの比較を行っているのが原因。貨幣(currency)を比較させる。
  5. 2クラス間でtimes()の実装が一致したので親クラスに移動する。

この章で行った事

  1. 2つのtimes()メソッドを一致させた。まずインライン化し、次に定数を変数に置き換えた。
  2. デバッグのために、テストなしでtoString()を書いた。
  3. (DollarでなくMoneyを返すという)変更を試し、"テストに"動作するか判断させた。
    コンピュータにさせた方が早いことは、試すべきであるという教訓。頭で考えなくて良い事もある。
  4. 実験から1つ戻り、別のテストを作成した。テストを動作させる事で、元の実験を動作させた。
    テストの無い実装はやらない!の保守的ステップを導入している。テストを先に書くのが基本型であるため。

11.諸悪の根源

2つのサブクラスDollar、Francはコンストラクタだけを持つ。
しかし、それだけではサブクラスを持つ理由にはならないので、削除したい。
サブクラスへの参照をコードの意味を変えずにスーパークラスへの参照に置き換える。

  1. Money.franc()のreturn new Franc(…)をreturn new Money(…)にする。
    戻りの型は既にMoneyとなっている。
  2. Money.dollar()も同様に修正する。
  3. Franc,Dollar(子)への参照が全てMoney(親)に移ったため、2つのサブクラスを削除する。
  4. 今回の修正に伴い不要になるテストを削除する。

この章で行った事

  1. サブクラスの内容を空にして削除した。
  2. 古いコードでは意味を成すが、新しいコードでは冗長なテストを削除した。

12.ついに、加法

加法全体のストーリーはまだ分からないので、(小さな作業の)$5+$5=$10から始める。

テストを慎重に設計する。
複数通貨をどのように表現するか。

  1. Moneyのように動作するが、2つの合計を表現するオブジェクトを作成する。
  2. 「Expression:式」という概念をそれに当てる。
    以降、表立って操作の中心に据えられるのはExpressionとなる。
    1. 「($2+3CHF)*5」といった表現の場合、
      $2,3CHFは原始的な形態(Money)
      「…」という一連の固まりを操作(Expression)
      と見る。
  3. Expressionに為替レートを適用することにより通貨を変換するが、
    それを適用するのを「銀行(Bank)」であるとする。
  4. なぜ銀行に責任を持たせるのか?(先見があっての判断ではあるが)
    1. Expressionは操作の中心である。
      中心にあるオブジェクトが、できる限り周囲の世界について無知であるようにしたい。
      このことにより柔軟性が保たれる。(テスト、再利用、理解の容易性を保つ。)
    2. Expressionが関わる操作が多く存在すると想像できる。
      全ての操作をExpressionに集めると、肥大化してしまう。
  5. 設計にもどる。Moneyの合計は操作の結果であるからして、Expressionになるべきである。
  6. Expressionの定義は、インターフェースとする。(軽量であるため)
  7. Money.plus()はExpressionを返すようにする。
  8. 空のBankクラスを定義する。
  9. テストに際してはreduce()スタブが要る。(null返却)
  10. テストは失敗する。返却値をnewしてテストを成功させる。(成功させるための最小限の実装でよい。)

この章で行ったこと

  1. 大きいテスト(2つの種別の貨幣の加算)を小さいテスト(同じ種別の貨幣の加算)に縮小した。
  2. テスト作成に際して、複数貨幣の扱いについてメタファを用いて熟考した。
    財布のように複数貨幣を入れる事ができ、かつ、その財布は合計額を保持できる。(ドルでは$x、フランではyCHF分もっている。といった感じに)
    これをExpressionという操作にしたものについて考えた。
  3. 新しいメタファに基づいて前のテストを書き換えた。
  4. テストを迅速にコンパイルできるようにした。
  5. リファクタリングの準備を整えた。

13.動作

まず、前章で実装したコードにはデータ重複がある。
Bankクラスの仮実装(Money.dollar(10))とテストクラスの(five.plus(five))は同じである。

加算、変換の動作を精査していく。 前章でメタファとして式(Expression)を導入したのが活きる。

  1. Money.plus()は実際のExpression(ここではSumとする、「実際」といっているのはこれがインターフェースだから)を返却する必要がある。
  2. Sumクラスを実装する。(加数、被加数をフィールドに持つ。まさに式である。)
  3. Bank.reduce()にSumを渡すよう変更
    1. Bank.reduce()にSumを渡す。
    2. Sum中の通貨が全て同じ、ターゲットの通貨も同じ仮定
    3. Sum中の通貨の合計を金額としてもつMoneyを返却する。
  4. Bank.reduce()は以下の点で変更の余地がある。
    1. Sumへのキャストがある。Expression単位で動作させなければならない。
    2. フィールドへの2段階参照がある。(ex. sum.addend.amount)
  5. メソッドの本体をSumクラスに移動する。(2段階参照への対応)
  6. Bank.reduce()はSum.reduce()を呼び出すように変更する。
    次に、Moneyが引数の場合を考える。
  1. Bank.reduce()の処理で、「Moneyクラスのインスタンスなら引数をそのまま返却する。」という処理を実装する。
  2. クラスをチェックしている箇所を見かけたら、ポリモーを考える。
    1. Moneyにもreduce()を実装する。
    2. Expressionインターフェースにreduce()を追加できる。
  3. キャストとクラスチェックを削除できる。

この章で行ったこと

  1. 実装を実現するために逆向きの作業(とりあえずの仮実装を変数に置き換えること。これまではそれが自明かつ、最終的な実装であった。)をするのではなく、そのままにして前進した。
  2. Sumのテスト作成、実装。
  3. 実装の速度を上げた。(仮実装をしないで自明な実装をいきなり済ませた)
  4. コードをキャストで実装してテストを動作させながら、本来ある場所を探した。
  5. クラスチェックを除去するために多相性を導入した。
### 14.変化 通貨の変換を考える。

「2フランもっているが1ドルに変換してほしい」というケースを作成

  1. フランをドルに変換するときは2で割る。(Money.reduce()に仮実装)
  2. Moneyが為替レートを扱っている。本来はBankのみが為替レートに注意を払うべきである。
  3. Expression.reduce()にBank(通貨変換担当)を渡す必要がある。
  4. Expressionを実装しているクラスで引数を変更する。
  5. Money.reduce()にてrateを扱っているのでBank.rate()を実装して移動させる。
  6. 依然として、テストコードとBank.reduce()間で'2'という数字が重複している。
  7. '2'を取り除くには、Bank中にレート表を保持する必要がある。
    1. 変換前後の通貨単位をキーとして、レートを保持するテーブルを用意する。
    2. 2つの通貨を含む2要素の配列の実態をキーとしなくてはならない。(配列は参照渡しなのでequals()で等価性の検査ができない)
    3. キーに用いるPairクラスを実装する。
    4. キーとして使用するためequals()、hashCode()を実装する。
    5. Bankにハッシュテーブルを作成する。
    6. レートを設定するaddRate()を実装する。
    7. Bank.rate()はテーブルを参照する実装に変更する。
    8. 変換前後が同一通貨単位の場合、'1'を返却するよう変更する。

この章で行ったこと

  1. 必要な引数を追加した。
  2. コードとテスト間のデータ重複を抜き出した。
  3. リファクタリングのミスを犯したが、問題を分割するためのテストを作成して前進した。(問題は分割せよ)

15.通貨の混合

最初に思いついたテスト「$5 + 10CHF = $10」に取りかかる。

  1. Sum.reduce()が引数を変換していないことが分かる。
  2. augend,addendを加算前にreduce()により変換するように変更する。
  3. Expressionに変更可能な箇所を変更していく。

この章で行った事

  1. テストを作成し前進した。
  2. 抽象的な宣言を用いて、テストケースへ凡化した。

16.最後に抽象化

タスクを全て終わらせる。

この章で行った事

  1. 残っていたタスクを消化した。
特に新しい操作も無く、コンパイラとテストに任せて修正しただけなので詳細は割愛する。
⚠️ **GitHub.com Fallback** ⚠️