はじめに
前準備~第1章までの内容については以下の記事にまとめています!
第2章 明白な実装
作成するコードについて
第1章では「ドルの掛け算」を実装しましたが、現在のコードには大きな設計上の問題が残っています。
それは、Dollar オブジェクトに対して times を実行すると、オブジェクト自身の値(インスタンス変数)を書き換えてしまうという点です。
これを現実の「5ドル紙幣」に例えると、2倍の計算をしたら手元の5ドル札が10ドル札に魔法のように書き換わってしまうようなもので、プログラミングにおいては予期せぬ挙動(副作用)の原因となります。
【現在のToDoリスト】
- \$5 + 10 CHF = $10 (レートが2:1の場合)
- \$5 * 2 = \$10
- amountをprivateにする
- Dollarの副作用どうする? ← 今からここに着手
- Moneyの丸め処理どうする?
1. 失敗するテストを書く(Red)
上記のように、計算を行っても元オブジェクトが変更されないことを確かめるテストを書きます。
# frozen_string_literal: true
# spec/dollar_spec.rb
require 'dollar'
RSpec.describe 'Dollar' do
it 'multiplication' do
five = Dollar.new(5)
# 1回目の掛け算:2 * 5 = 10 を期待
five.times(2)
expect(five.amount).to eq 10
# 2回目の掛け算:元の five は 5 のままであり、 5 * 3 = 15 を期待
five.times(3)
expect(five.amount).to eq 15
end
end
このテストを実行すると、2回目の expect で結果が 30(10 * 3)になってしまい、失敗します。
実行結果
$ bundle exec rspec
Dollar
multiplication (FAILED - 1)
Failures:
1) Dollar multiplication
Failure/Error: expect(five.amount).to eq 15
expected: 15
got: 30
(compared using ==)
# ./spec/dollar_spec.rb:16:in 'block (2 levels) in <top (required)>'
Finished in 0.00769 seconds (files took 0.04801 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/dollar_spec.rb:7 # Dollar multiplication
上記のような意図のテストを通すためには、timesメソッドで新しいDollarのインスタンスが返るようになればよさそうです。
$5 * 2 の結果は \$10のDollarオブジェクトが返ればよさそうです。テストコードを以下のように修正します。
# frozen_string_literal: true
require 'dollar'
RSpec.describe 'Dollar' do
it 'multiplication' do
five = Dollar.new(5)
# timesメソッドが新しいオブジェクトを返すと定義する
product = five.times(2)
expect(product.amount).to eq 10
# 元の five オブジェクトは 5ドルのままであることを期待する
product = five.times(3)
expect(product.amount).to eq 15
end
end
いずれにせよ、現時点ではテストは失敗します。
2. 実装でテストを通す(Green)
第1章では、とにかく最短でテストを通すために amount = 10 と記述する「仮実装」を行いました。
しかし、今回の「計算結果として新しいオブジェクトを返す」という修正は、実装方法が非常に明白です。このように、実装がすぐに思い浮かぶ場合に最初から正解を書く手法を、本書では 「明白な実装(Obvious Implementation)」 と呼んでいます。
| 戦略 | 概要 |
|---|---|
| 仮実装 | コードにべた書きで値を書き、徐々に変数化 |
| 明白な実装 | すぐに頭の中の実装をコードに落とし込む |
著者であるKent Beck氏は、上記の2つのモードを揺れ動きながら実装を進めていると述べています。
書くべき内容がはっきりしているときは 明白な実装 から 明白な実装 へ、予期せずテストが失敗したら 仮実装 モードで細かく進めていく方針をとる、とのことです。
テストを通すため、Dollarクラスのtimesメソッドの内容を修正します。
# frozen_string_literal: true
class Dollar
attr_reader :amount
def initialize(amount)
@amount = amount
end
def times(multiplier)
Dollar.new(@amount * multiplier) # ←☆ Dollarインスタンスを生成して返す
end
end
これでテストは通るようになり、副作用の問題を解決できました!
このように、一度作成されたら値が変わらないオブジェクトを Value Object(値オブジェクト) と呼びます。
しかし、「値オブジェクト」になったことで、今度は「5ドルのオブジェクト同士が等しいか」を判断する必要が出てきました。次回はそこに焦点を当てます。
【現在のToDoリスト】
- \$5 + 10 CHF = $10 (レートが2:1の場合)
- \$5 * 2 = \$10
- amountをprivateにする
- Dollarの副作用どうする?
- Moneyの丸め処理どうする?