はじめに
それに触発され、t_wada さんが訳されたテスト駆動開発を、現在学習中の Go 言語で取り組んでみました。
本記事中の引用は、特に断りがない限りこの本の引用になります。
※第Ⅰ部まで終了
リポジトリ
https://github.com/eyuta/golang-tdd
前提
筆者の Go の習熟度はA Tour of Goを終了したくらいです。
バージョン
go version go1.15.6 windows/amd64
テスト方法について
今回は、Go の標準の testing パッケージと、こちらサードパーティのassertパッケージを使用しています。
Go の標準の testing パッケージには、Assert が含まれておらず、推奨もされていません。
理由については、以下の記事が詳しいです。
ただ、今回は testing としてのテストではなく、checking としてのテストがメインであることから、手軽にテストケースを記述できる Assert パッケージを使用しています。
参考記事
testing パッケージの使い方は以下を参照しました。
本編
TDD について
TDD のルール
コードを書く前に、失敗する自動テストコードを必ず書く。
重複を除去する。
TDD のリズム
- まずはテストを 1 つ書く
- すべてのテストを走らせ、新しいテストの失敗を確認する
- 小さな変更を行う
- すべてのテストを走らせ、すべて成功することを確認する
- リファクタリングを行って重複を除去する
第 I 部 多国通貨
第 1 章 仮実装
第 1 章の振り返り
- 書くべきテストのリストを作った。
- どうなったら嬉しいかを小さいテストコードで表現した。
- 空実装を使ってコンパイラを通した。
- 大罪を犯しながらテストを通した。
- 動くコードをだんだんと共通化し、ベタ書きの値を変数に置き換えていった。
- TODO リストに項目を追加するに留め、一度に多くのものを相手にすることを避けた。
第 1 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
第 1 章終了時のコード
type Dollar struct {
amount int
}
func (d *Dollar) times(multiplier int) {
d.amount *= multiplier
}
func TestMultiCurrencyMoney(t *testing.T) {
t.Run("$5 * 2 = $10", func(t *testing.T) {
five := Dollar{5}
five.times(2)
assert.Equal(t, 10, five.amount)
})
}
第 2 章 明確な実装
第 2 章の振り返り
- 設計の問題点(今回は副作用)をテストコードに写し取り、その問題点のせいでテストが失敗するのを確認した。
- 空実装でさっさとコンパイルを通した。
- 正しいと思える実装をすぐに行い、テストを通した。
第 2 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
第 2 章終了時のコード
type Dollar struct {
amount int
}
func (d *Dollar) times(multiplier int) Dollar {
return Dollar{d.amount * multiplier}
}
func TestMultiCurrencyMoney(t *testing.T) {
t.Run("何度でもドルの掛け算が可能である", func(t *testing.T) {
five := Dollar{5}
product := five.times(2)
assert.Equal(t, 10, product.amount)
product = five.times(3)
assert.Equal(t, 15, product.amount)
})
}
第 3 章 三角測量
型のコンバージョンは、Type Assertionを利用した。
第 3 章の振り返り
- Value Object パターンを満たす条件がわかった。
- その条件を満たすテストを書いた。
- シンプルな実装を行った。
- すぐにリファクタリングを行うのではなく、もう 1 つテストを書いた。
- 2 つのテストを同時に通すリファクタリングを行った。
第 3 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
第 3 章終了時のコード
type Object interface{}
type Dollar struct {
amount int
}
func (d Dollar) times(multiplier int) Dollar {
return Dollar{d.amount * multiplier}
}
func (d Dollar) equals(object Object) bool {
dollar := object.(Dollar)
return d.amount == dollar.amount
}
func TestMultiCurrencyMoney(t *testing.T) {
t.Run("何度でもドルの掛け算が可能である", func(t *testing.T) {
five := Dollar{5}
product := five.times(2)
assert.Equal(t, 10, product.amount)
product = five.times(3)
assert.Equal(t, 15, product.amount)
})
t.Run("同じ金額が等価である", func(t *testing.T) {
assert.True(t, Dollar{5}.equals(Dollar{5}))
assert.False(t, Dollar{5}.equals(Dollar{6}))
})
}
第 4 章 意図を語るテスト
第 4 章の振り返り
- 作成したばかりの機能を使って、テストを改善した。
- そもそも正しく検証できていないテストが 2 つあったら、もはやお手上げだと気づいた。
- そのようなリスクを受け入れて先に進んだ。
- テスト対象オブジェクトの新しい機能を使い、テストコードとプロダクトコードの間の結合度を下げた。
第 4 章終了時のコード
// 変化なし
func TestMultiCurrencyMoney(t *testing.T) {
t.Run("ドルの掛け算が可能である", func(t *testing.T) {
five := Dollar{5}
assert.Equal(t, Dollar{10}, five.times(2))
assert.Equal(t, Dollar{15}, five.times(3))
})
t.Run("同じ金額が等価である", func(t *testing.T) {
assert.True(t, Dollar{5}.equals(Dollar{5}))
assert.False(t, Dollar{5}.equals(Dollar{6}))
})
}
第 5 章 原則をあえて破るとき
第 5 章の振り返り
- 大きいテストに立ち向かうにはまだ早かったので、次の一歩を進めるために小さなテストをひねり出した。
- 恥知らずにも既存のテストをコピー&ペーストして、テストを作成した。
- さらに恥知らずにも、既存のモデルコードを丸ごとコピー&ペーストして、テストを通した。
- この重複を排除するまでは家に帰らないと心に決めた。
第 5 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
- 5CHF*2=10CHF
- Dollar と Franc の重複
- equals の一般化
- times の一般化
第 5 章終了時のコード
type Franc struct {
amount int
}
func (f Franc) times(multiplier int) Franc {
return Franc{f.amount * multiplier}
}
func (f Franc) equals(object Object) bool {
franc := object.(Franc)
return f.amount == franc.amount
}
t.Run("フランの掛け算が可能である", func(t *testing.T) {
five := Franc{5}
assert.Equal(t, Franc{10}, five.times(2))
assert.Equal(t, Franc{15}, five.times(3))
})
t.Run("同じ金額のフランが等価である", func(t *testing.T) {
assert.True(t, Franc{5}.equals(Franc{5}))
assert.False(t, Franc{5}.equals(Franc{6}))
})
第 6 章 テスト不足に気づいたら
- Go には継承の概念が無いため、本章では composition を用いて実装する。
- Dollar, Franc を生成するためのコンストラクタにあたるものを用意した(以下を参考にした)。
Constructors and composite literals -
multiCurrencyMoney.go
をmoney.go
,dollar.go
,franc.go
に分割した -
multiCurrencyMoney_test.go
をmoney_test.go
に改名し、package 名をmoney_test
とした -
money.go
とmoney_test.go
の package 名が異なるため、プライベートメソッド(小文字のメソッド)が参照できなくなったので、equals
,times
メソッドをパブリックメソッドに変更した - パブリックメソッドにはコメントが必要になるので、簡単なコメントを追加した
参考: Godoc: documenting Go code
第 6 章の振り返り
このあたりから、言語仕様の違いによりコーディング内容が本と異なってくる
- Dollar クラスから親クラス Money へ段階的にメソッドを移動した。
- 2 つ目のクラス(Franc)も同様にサブクラス化した。
- 2 つの equals メソッドの差異をなくしてから、サブクラス側の実装を削除した。
第 6 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
- 5CHF*2=10CHF
- Dollar と Franc の重複
- equals の一般化
- times の一般化
- Franc と Dollar を比較する
第 6 章終了時のコード
量が増えてきたので、主な変更点のみ抜粋して表示する
全文: github
package money
// AmountGetter is a wrapper of amount.
type AmountGetter interface {
getAmount() int
}
// Money is a struct that handles money.
type Money struct {
amount int
}
// Equals checks if the amount of the receiver and the argument are the same
func (m Money) Equals(a AmountGetter) bool {
return m.getAmount() == a.getAmount()
}
func (m Money) getAmount() int {
return m.amount
}
package money_test
import (
"testing"
"github.com/eyuta/golang-tdd/money"
"github.com/stretchr/testify/assert"
)
func TestMultiCurrencyMoney(t *testing.T) {
t.Run("ドルの掛け算が可能である", func(t *testing.T) {
five := money.NewDollar(5)
assert.Equal(t, money.NewDollar(10), five.Times(2))
assert.Equal(t, money.NewDollar(15), five.Times(3))
})
}
第 7 章 疑念をテストに翻訳する
第 7 章の振り返り
- 頭の中にある悩みをテストとして表現した。完璧ではないものの、まずまずのやり方(getClass)でテストを通した。
- さらなる設計は、本当に必要になるときまで先延ばしにすることにした
- Money に新しく Name フィールドを追加した
- 上記の
getClass
の代替。struct の入れ子の場合、レシーバは常に Money になるので、type の比較ができないため
- 上記の
第 7 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
- 5CHF*2=10CHF
- Dollar と Franc の重複
- equals の一般化
- times の一般化
- Franc と Dollar を比較する
- 通貨の概念
第 7 章終了時のコード
全文: github
package money
// Accessor is a accessor of Money
type Accessor interface {
Amount() int
Name() string
}
// Money is a struct that handles money.
type Money struct {
amount int
name string
}
// Equals checks if the amount of the receiver and the argument are the same
func (m Money) Equals(a Accessor) bool {
return m.Amount() == a.Amount() && m.Name() == a.Name()
}
// Amount returns amount field
func (m Money) Amount() int {
return m.amount
}
// Name returns name field
func (m Money) Name() string {
return m.name
}
t.Run("同じ金額のドルとフランが等価ではない", func(t *testing.T) {
assert.False(t, money.NewFranc(5).Equals(money.NewDollar(5)))
})
func NewDollar(a int) Dollar {
return Dollar{Money{amount: a, name: "Dollar"}}
}
第 8 章 実装を隠す
第 8 章の振り返り
- 重複を除去できる状態に一歩近づけるために、Dollar と Franc にある 2 つの times メソッドのシグニチャを合わせた。
- Factory Method パターンを導入して、テストコードから 2 つのサブクラスの存在を隠した。
- サブクラスを隠した結果、いくつかのテストが冗長なものになったことに気がついたが、いまはそのままにしておいた。
- Go には抽象クラスの概念が無いため、Times メソッドについては一足先に実装もろとも Money に移行した
- それにより、Dollar, Franc の 2 つの構造体が使われなくなったが、一旦取っておくことにする
第 8 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
- 5CHF*2=10CHF
- Dollar と Franc の重複
- equals の一般化
- times の一般化
- Franc と Dollar を比較する
- 通貨の概念
- testFrancMultiplication を削除する?
第 8 章終了時のコード
全文: github
package money
// Accessor is a accessor of Money
type Accessor interface {
Amount() int
Name() string
}
// Money is a struct that handles money.
type Money struct {
amount int
name string
}
// NewDollar is constructor of Dollar.
func NewDollar(a int) Money {
return Money{
amount: a,
name: "Dollar",
}
}
// NewFranc is constructor of Dollar.
func NewFranc(a int) Money {
return Money{
amount: a,
name: "Franc",
}
}
// Times multiplies the amount of the receiver by a multiple of the argument
func (m Money) Times(multiplier int) Money {
return Money{
amount: m.amount * multiplier,
name: m.name,
}
}
// Equals checks if the amount of the receiver and the argument are the same
func (m Money) Equals(a Accessor) bool {
return m.amount == a.Amount() && m.name == a.Name()
}
// Amount returns amount field
func (m Money) Amount() int {
return m.amount
}
// Name returns name field
func (m Money) Name() string {
return m.name
}
package money
// Dollar is a struct that handles dollar money.
type Dollar struct {
Money
}
第 9 章 歩幅の調整
第 9 章の振り返り
- 大きめの設計変更にのめり込みそうになったので、その前に手前にある小さな変更に着手した。
- 差異を呼び出し側(FactoryMethod 側)に移動することによって、2 つのサブクラスのコンストラクタを近づけていった。
- リファクタリングの途中で少し寄り道して、times メソッドの中で FactoryMethod を使うように変更した。
- Franc に行ったリファクタリングを Dollar にも同様に、今度は大きい歩幅で一気に適用した。
- 完全に同じ内容になった 2 つのコンストラクタを親クラスに引き上げた。
currency field は第 7 章で作成した name field を currency に改名しただけになる
第 9 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
- 5CHF*2=10CHF
- Dollar と Franc の重複
- equals の一般化
- times の一般化
- Franc と Dollar を比較する
- 通貨の概念
- testFrancMultiplication を削除する?
第 9 章終了時のコード
全文: github
package money
// Accessor is a accessor of Money
type Accessor interface {
Amount() int
Currency() string
}
// Money is a struct that handles money.
type Money struct {
amount int
currency string
}
// NewMoney is constructor of Money.
func NewMoney(a int, c string) Money {
return Money{
amount: a,
currency: c,
}
}
// NewDollar is constructor of Dollar.
func NewDollar(a int) Money {
return NewMoney(a, "USD")
}
// NewFranc is constructor of Dollar.
func NewFranc(a int) Money {
return NewMoney(a, "CHF")
}
// Times multiplies the amount of the receiver by a multiple of the argument
func (m Money) Times(multiplier int) Money {
return Money{
amount: m.amount * multiplier,
currency: m.currency,
}
}
// Equals checks if the amount of the receiver and the argument are the same
func (m Money) Equals(a Accessor) bool {
return m.amount == a.Amount() && m.currency == a.Currency()
}
// Amount returns amount field
func (m Money) Amount() int {
return m.amount
}
// Currency returns name field
func (m Money) Currency() string {
return m.currency
}
t.Run("通貨テスト", func(t *testing.T) {
assert.Equal(t, "USD", money.NewDollar(1).Currency())
assert.Equal(t, "CHF", money.NewFranc(1).Currency())
})
第 10 章 テストに聞いてみる
第 10 章の振り返り
times メソッドについては既に共通化しているため、ログ出力用の String メソッドのみ実装した。
第 10 章終了時のコード
全文: github
func (m Money) String() string {
return fmt.Sprintf("{Amount: %v, Currency: %v}", m.amount, m.currency)
}
第 11 章 不要になったら消す
第 11 章の振り返り
- サブクラスの仕事を減らし続け、とうとう消すところまでたどり着いた。
- サブクラス削除前の構造では意味があるものの、削除後は冗長になってしまうテストたちを消した。
第 11 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5*2=$10
- amount を private にする
- Dollar の副作用どうする?
- Money の丸め処理どうする?
- equals()
- hashCode()
- null との等価性比較
- 他のオブジェクトとの等価性比較
- 5CHF*2=10CHF
- Dollar と Franc の重複
- equals の一般化
- times の一般化
- Franc と Dollar を比較する
- 通貨の概念
- testFrancMultiplication を削除する?
第 11 章終了時のコード
dollar.go, franc.go,ファイルを削除した。
全文: github
第 12 章 設計とメタファー
第 12 章の振り返り
- 大きいテスト($5 + 10 CHF)を分解して。進み具合がわかる小さいテスト($5+$5)を作成した。
- これから行う計算のためのメタファーについて深く考えた。
- テストがコンパイルできるところまで早足で進んだ。
- テストを通した。
- 本当の実装を導くためのリファクタリングを楽しみにしつつ、少し不安も感じている。
第 12 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5 + $5 = $10
第 12 章終了時のコード
全文: github
// Plus adds an argument to the amount of receiver
func (m Money) Plus(added Money) Expression {
return NewMoney(m.amount+added.amount, m.currency)
}
package money
// Bank calculates using exchange rates
type Bank struct {
}
// Reduce applies the exchange rate to the argument expression
func (b Bank) Reduce(source Expression, to string) Money {
return NewDollar(10)
}
package money
// Expression shows the formula of currency (regardless of the difference in exchange rate)
type Expression interface {
}
t.Run("ドル同士の足し算が可能である", func(t *testing.T) {
five := money.NewDollar(5)
sum := five.Plus(five)
bank := money.Bank{}
reduced := bank.Reduce(sum, "USD")
assert.Equal(t, money.NewDollar(10), reduced)
})
第 13 章 実装を導くテスト
第 13 章の振り返り
- 重複を除去出来ていないので、TODO リストの項目を「済」にしなかった。
- 実装の着想を得るためにさらに先に進むことにした。
- 速やかに実装を行った(Sum のコンストラクタ)
- キャストを使って 1 カ所で実装した後で、テストが通る馬で本来あるべき場所にコードを移した。
- ポリモフィズムを使って、明示的なクラスチェックを置き換えた。
第 13 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5 + $5 = $10
- $5 + $5 が Money を返す
- Bank.reduce(Money)
- Money を変換して換算を行う
- Reduce(Bank, String)
第 13 章終了時のコード
全文: github
t.Run("ドル同士の足し算が可能である", func(t *testing.T) {
five := money.NewDollar(5)
result := five.Plus(five)
sum := result.(money.Sum)
assert.Equal(t, five, sum.Augend)
assert.Equal(t, five, sum.Added)
})
t.Run("Sumで足されるお金の通貨が同じなら、足し算の結果が同じになる", func(t *testing.T) {
sum := money.Sum{
Augend: money.NewDollar(3),
Added: money.NewDollar(4),
}
bank := money.Bank{}
result := bank.Reduce(sum, "USD")
assert.Equal(t, money.NewDollar(7), result)
})
t.Run("moneyをreduceしても、reduceに渡す通貨が同じであれば同じ値が返る", func(t *testing.T) {
bank := money.Bank{}
result := bank.Reduce(money.NewDollar(1), "USD")
assert.Equal(t, money.NewDollar(1), result)
})
// Plus adds an argument to the amount of receiver.
func (m Money) Plus(added Money) Expression {
return Sum{
Augend: m,
Added: added,
}
}
// Reduce applies the exchange rate to receiver.
func (m Money) Reduce(to string) Money {
return m
}
// Reduce applies the exchange rate to the argument expression
func (b Bank) Reduce(source Expression, to string) Money {
return source.Reduce(to)
}
// Expression shows the formula of currency (regardless of the difference in exchange rate)
type Expression interface {
Reduce(string) Money
package money
// Sum 合計は通貨の加算を行います
type Sum struct {
Augend Money
Added Money
}
// Reduce applies the exchange rate to the result of the addition
func (s Sum) Reduce(to string) Money {
amount := s.Augend.amount + s.Added.amount
return NewMoney(amount, to)
}
第 14 章 学習用テストと回帰テスト
第 14 章の振り返り
- 必要になると予想されたパラメータ追加をすぐに行った。
- コードとテストの間のデータ重複をくくりだした。
- 内部実装で使うためだけのヘルパークラスを個別のテスト無しで作成した。
- リファクタリング中にミスを犯したが、問題を再現するテストを追加して、着実に前進した。
今回、本にあるような Pair.go ファイルを作成せず、Pair struct のみ Bank ファイルに記述した。
Java オブジェクトのequals
による比較は等値比較だが、Go の struct の==
による比較は等価比較になるため。
ただし、struct に map や slice といった等価比較できないフィールドが存在する場合は、コンパイル時にエラーになる。
そういった場合はreflect.DeepEqual
を使って比較を行う
参考:How to compare if two structs, slices or maps are equal?
type s struct {
a int
}
s1 := s{1}
s2 := s{1}
fmt.Println(s1 == s2) // true
type s struct {
a int
b []int
}
s1 := s{1, make([]int, 0)}
s2 := s{1, make([]int, 0)}
fmt.Println(reflect.DeepEqual(s1, s2)) // true
fmt.Println(s1 == s2) // invalid operation: s1 == s2 (struct containing []int cannot be compared)
ちなみに、assert.Equal
も内部でreflect.DeepEqual
を使用している。
// This function does no assertion of any kind.
func ObjectsAreEqual(expected, actual interface{}) bool {
if expected == nil || actual == nil {
return expected == actual
}
exp, ok := expected.([]byte)
if !ok {
return reflect.DeepEqual(expected, actual) // ここ
}
act, ok := actual.([]byte)
if !ok {
return false
}
if exp == nil || act == nil {
return exp == nil && act == nil
}
return bytes.Equal(exp, act)
}
第 14 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5 + $5 = $10
- $5 + $5 が Money を返す
- Bank.reduce(Money)
- Money を変換して換算を行う
- Reduce(Bank, String)
第 14 章終了時のコード
全文: github
t.Run("1 CHF = $2", func(t *testing.T) {
bank := money.NewBank()
bank.AddRate("CHF", "USD", 2)
result := bank.Reduce(money.NewFranc(2), "USD")
assert.Equal(t, money.NewDollar(1), result)
})
t.Run("同量テスト", func(t *testing.T) {
bank := money.NewBank()
assert.Equal(t, 1, bank.Rate("USD", "USD"))
})
// Reduce applies the exchange rate to receiver.
func (m Money) Reduce(b Bank, to string) Money {
rate := b.Rate(m.currency, to)
return NewMoney(m.amount/rate, to)
}
// Bank calculates using exchange rates
type Bank struct {
rates map[Pair]int
}
// Pair associates two currencies
type Pair struct {
from, to string
}
// NewBank is a constructor of Bank
func NewBank() Bank {
b := Bank{}
b.rates = make(map[Pair]int)
return b
}
// Reduce applies the exchange rate to the argument expression
func (b *Bank) Reduce(source Expression, to string) Money {
return source.Reduce(*b, to)
}
// AddRate adds exchange rate
func (b *Bank) AddRate(from, to string, rate int) {
b.rates[Pair{from: from, to: to}] = rate
}
// Rate adds exchange rate
func (b *Bank) Rate(from, to string) int {
if from == to {
return 1
}
p := Pair{from: from, to: to}
return b.rates[p]
}
// Expression shows the formula of currency (regardless of the difference in exchange rate)
type Expression interface {
Reduce(Bank, string) Money
}
// Reduce applies the exchange rate to the result of the addition
func (s Sum) Reduce(b Bank, to string) Money {
amount := s.Augend.amount + s.Added.amount
return NewMoney(amount, to)
}
第 15 章 テスト任せとコンパイラ任せ
第 15 章の振り返り
- こうなったら良いというテストを書き、次にまず一歩で動かせるところまでそのテストを少し後退させた。
- 一般化(より抽象度の高い型で宣言する)作業を、末端から開始して頂点(テストケース)まで到達させた。
- 変更の際にコンパイラに従い(fiveBucks 変数の Expression 型への変更)、変更の連鎖を 1 つずつ仕留めた(Expression インターフェースへの Plus メソッドの追加等)。
どうでもいいが、Dollar の代わりに Bucks が使われることを初めて知った。
第 15 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5 + $5 = $10
- $5 + $5 が Money を返す
- Bank.reduce(Money)
- Money を変換して換算を行う
- Reduce(Bank, String)
- Sum.Plus
- Expression.Times
第 15 章終了時のコード
全文: github
t.Run("$5 + 10 CHF = $10 (レートが2:1の場合)", func(t *testing.T) {
fiveBucks := money.Expression(money.NewDollar(5))
tenFrancs := money.Expression(money.NewFranc(10))
bank := money.NewBank()
bank.AddRate("CHF", "USD", 2)
result := bank.Reduce(fiveBucks.Plus(tenFrancs), "USD")
assert.Equal(t, money.NewDollar(10), result)
})
// Times multiplies the amount of the receiver by a multiple of the argument
func (m Money) Times(multiplier int) Expression {
return NewMoney(m.amount*multiplier, m.currency)
}
// Plus adds an argument to the amount of receiver.
func (m Money) Plus(added Expression) Expression {
return Sum{
Augend: m,
Added: added,
}
}
// Expression shows the formula of currency (regardless of the difference in exchange rate)
type Expression interface {
Reduce(Bank, string) Money
Plus(Expression) Expression
}
// Reduce applies the exchange rate to the result of the addition
func (s Sum) Reduce(b Bank, to string) Money {
amount := s.Augend.Reduce(b, to).amount + s.Added.Reduce(b, to).amount
return NewMoney(amount, to)
}
// Plus adds an argument to the amount of receiver.
func (s Sum) Plus(added Expression) Expression {
return Sum{}
}
第 16 章 将来の読み手を考えたテスト
第 16 章の振り返り
- 将来読む人のことを考えながらテストを書いた
- これまえのプログラミングスタイルと TDD との比較を自分自身で行うことが大事だと伝えた。
- 再び連鎖的に波及する定義変更を行い、コンパイラに導かれながら修正を行った。
- 最後に簡単な実験を行い、うまく機能しないと分かっていたので破棄して引き返した。
文中で、
テストを書くのは、(中略) いま考えていることを将来の仲間に伝えるロゼッタストーンの役割も担ってほしいからだ。
とあるが、この場合のロゼッタストーンとはone that gives a clue to understanding
、日本語で理解の手がかりを与えるもの
という意味があるそうだ。
参考: Rosetta stone
元々はエジプトのロゼッタで発見された古代の石碑のこと。
第 16 章の TODO リスト
- $5+10CHF=$10(レートが 2:1 の場合)
- $5 + $5 = $10
- $5 + $5 が Money を返す
- Bank.reduce(Money)
- Money を変換して換算を行う
- Reduce(Bank, String)
- Sum.Plus
- Expression.Times
第 16 章終了時のコード
全文: github
t.Run("$5 + 10 CHF + $5 = $15 をSum structを使って行う", func(t *testing.T) {
fiveBucks := money.Expression(money.NewDollar(5))
tenFrancs := money.Expression(money.NewFranc(10))
bank := money.NewBank()
bank.AddRate("CHF", "USD", 2)
sum := money.Sum{Augend: fiveBucks, Added: tenFrancs}.Plus(fiveBucks)
result := bank.Reduce(sum, "USD")
assert.Equal(t, money.NewDollar(15), result)
})
t.Run("($5 + 10 CHF) * 2 = $20 をSum structを使って行う", func(t *testing.T) {
fiveBucks := money.Expression(money.NewDollar(5))
tenFrancs := money.Expression(money.NewFranc(10))
bank := money.NewBank()
bank.AddRate("CHF", "USD", 2)
sum := money.Sum{Augend: fiveBucks, Added: tenFrancs}.Times(2)
result := bank.Reduce(sum, "USD")
assert.Equal(t, money.NewDollar(20), result)
})
// Expression shows the formula of currency (regardless of the difference in exchange rate)
type Expression interface {
Reduce(Bank, string) Money
Plus(Expression) Expression
Times(int) Expression
}
// Plus adds an argument to the amount of receiver.
func (s Sum) Plus(added Expression) Expression {
return Sum{
Augend: s,
Added: added,
}
}
// Times multiplies the amount of the receiver by a multiple of the argument
func (s Sum) Times(multiplier int) Expression {
return Sum{
Augend: s.Augend.Times(multiplier),
Added: s.Added.Times(multiplier),
}
}