はじめに
前回の第2章では、Dollar を Value Object(値オブジェクト) パターンに適用しました。
これによって副作用の問題は解決しましたが、新たな課題が浮上しました。それは「5ドルと5ドルは等しいのか?」という問題です。
前回の記事は以下のリンクから
第3章 三角測量
作成するコードについて
Rubyにおいて、別のインスタンスであれば(たとえ中身の金額が同じでも)デフォルトでは == は false を返します。
# どちらも $5
d1 = Dollar.new(5)
d2 = Dollar.new(5)
d1 == d2 # => false (インスタンスIDが違うため)
これを「値が同じなら等しい(true)」と判定できるように修正します。
値オブジェクトは、5ドルのオブジェクトは永遠に5ドルのままであり、7ドルが必要になった場合は新たにオブジェクトを生成する、という不変性を持ちます。
また、Dollarオブジェクトをハッシュ(Hash)のキーとして使用する場合、== と同時に hash メソッドなども必要になります。今回はまず基本となる等価性から着手します。
【現在のToDoリスト】
- $5 + 10 CHF = $10 (レートが2:1の場合)
- $5 * 2 = $10
- amountをprivateにする
- Dollarの副作用どうする?
- Moneyの丸め処理どうする?
- == (\$5 同士の比較) ← 今からここに着手
- hash メソッドの定義
1. 失敗するテストを書く (Red)
等価性を検証するテストを追加します。
まずは\$5オブジェクトが\$5オブジェクトと等しいかどうかを検証するテストを書きます
spec/dollar_spec.rb
# frozen_string_literal: true
require 'dollar'
RSpec.describe 'Dollar' do
# ... (掛け算のテストは省略) ...
it 'equality' do
# $5 と $5 は等しい(True)
expect(Dollar.new(5)).to eq Dollar.new(5)
end
end
2. 実装で一旦テストを通す (Green?)
最短でテストを通すために、あえて極端な「仮実装」をしてみます。
Dollarクラスにtrueを返すだけの処理を実装してみます。
lib/dollar.rb
# frozen_string_literal: true
class Dollar
# ... (省略) ...
# 等価性の定義
def ==(other)
true
end
end
これで bundle exec rspec を実行すると、当然 Green になります。 しかし、これでは $5 == $6 も true になってしまい、正しい実装とは言えません。
3. TDDのテクニック:三角測量
ここで、実装を「一般化」するために、もう一つのテストケースを追加します。これが 「三角測量」 です。
「\$5 と \$6 は等しくない」ケースを追加してみます。
spec/dollar_spec.rb
# frozen_string_literal: true
require 'dollar'
RSpec.describe 'Dollar' do
# ... (掛け算のテストは省略) ...
it 'equality' do
# 1つ目のテスト:$5 と $5 は等しい
expect(Dollar.new(5)).to eq Dollar.new(5)
# 2つ目のテスト:$5 と $6 は等しくない
expect(Dollar.new(5)).not_to eq Dollar.new(6)
end
end
この時点でテストを実行すると、現在Dollarクラスの==メソッドは常にtrueを返す実装になっているため失敗します。
追加したテストケースを通すために、等価性比較処理を一般化する必要が生じます。
以下のように、Dollarクラスのインスタンス自身のamountと、引数で渡されたインスタンスのamountとの等価性を比較すればよさそうです。
lib/dollar.rb
# frozen_string_literal: true
class Dollar
# ... (省略) ...
# 等価性の定義
def ==(other)
amount == other.amount
end
end
これでテストがすべて通るようになりました!
三角測量について
測量において、ある地点の正確な位置を特定するために2つの異なる地点からの観測データを用いることになぞらえています。
TDDにおける三角測量も同様です。
- テストA:$5 == $5 が通る実装の範囲
- テストB:$5 != $6 が通る実装の範囲
この2つのテストを同時にパスさせようとすると、実装は 「amount同士を比較する」という唯一の正解(一般解) に絞り込まれます。
今回の処理ぐらいの複雑さであれば、「明白な実装」でシンプルに実装を進めることは可能かと思います。
が実装方針がぱっと思いつかない際は具体的なテストケースを追加して徐々に具体的なコードに落とし込んでいく今回の「三角測量」の手法は有効になるかと思います。
コラム:RubyとJavaのprivateの違い
ここで Ruby 特有のアクセス制御について注意が必要です。
Java の private が「クラス」単位のアクセス制御であるのに対し、Ruby の private は「レシーバ」に対する制約(レシーバを指定したメソッド呼び出しの禁止)として機能します。そのため、Java の感覚で private にすると other.amount が呼べなくなるという落とし穴があります。
細かい説明については別記事で投稿しようと思います。
参考文献としては以下の記事をご参照ください
おわりに
今回は、TDDのテクニックの一つである「三角測量」を用いて、Dollar クラスに等価性(Equality)を実装しました。 あえて「仮実装(Fake It)」を挟み、テストケースを増やすことで、コードを一般化せざるを得ない状況を作り出すプロセスを体験できたかと思います。
これで、Dollar は数値と同じように「値」として比較できるオブジェクトになりました。 ToDoリストを更新して終了します。
【現在のToDoリスト】
- $5 + 10 CHF = $10 (レートが2:1の場合)
- $5 * 2 = $10
- amountをprivateにする
- Dollarの副作用どうする?
- Moneyの丸め処理どうする?
- == (\$5 同士の比較)
- hash メソッドの定義
次回は、このコラムで触れた違いを意識しながらToDoリストにある「amountをprivateにする」に着手します。