テスト駆動開発でお試しする Pharo Smalltalk・第8回 MoneyBagを少しだけBagらしくする

[新訳版『テスト駆動開発』](https://estore.ohmsha.co.jp/titles/978427421788P)の第Ⅰ部の多国通貨をネタに Smalltalk を学ぶシリーズの第8回です。同書と似た仕様を目指しますが、好みを優先する等の都合で写経にはなっていませんのでどうぞあしからず。（12/24追記：なるべく忠実な写経は別に書きました→[こちら](https://qiita.com/sumim/items/791984a4702bce91dacd) ）

- [テスト駆動開発でお試しする Pharo Smalltalk・目次](https://qiita.com/sumim/items/3a36c03ab41519077b37)

[前回](https://qiita.com/sumim/items/16b84a2f875a5aa10555) は、同書第17章でちょっとだけ触れられている“MoneyBag”を、きっとこんな感じなのではないかな…と勝手な想像を巡らしながら実装してみました。

今回は MoneyBag に手を加えて、気持ち Bag らしく振舞うよう細工を施します。


<br />
#####*参考： 多国通貨実装のおおまかな流れ*
1. Dollar を定義
1. Dollar をコピーして Franc を定義
1. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ
1. **Sum（Expression）、Bank を定義して reduce を実現** ← 今ここ

*同書を読んでいない方向けに補足：Sum（当シリーズでは MoneyBag）は換算前の異種通貨同士の加算（式, expression）の保持を、Bank は異種通貨間の換算レートの管理、および、Sum の換算を伴う簡約（reduce）それぞれ担当します。*

# Bag について

Smalltalk で Bag というのは多重集合（multiset）を扱うための組み込みのコレクション・クラスで、要素を種類ごとに分類してその数で管理します。

```Smalltalk
#(1 2 2 3) asBag = #(2 3 2 1) asBag "=> true "
```

*クラスブラウザのコード枠の空欄や、Playground などに式を入力（あるいはコピペ）してから右クリック → Print it すると式を実行して結果を見ることができます。*

![1030.png](https://qiita-image-store.s3.amazonaws.com/0/49275/a08d9938-5e3f-463a-1691-740db754bc9d.png)


MoneyBag の場合、通貨の種類とその小計で管理することになるでしょう。

```Smalltalk
(5 USD + 10 CHF) = (10 CHF + 5 USD) "=> true"
```

つまり、MoneyBag class>>withAll: で受け取った配列内の順序が違っていても

```Smalltalk
(MoneyBag withAll: {5 USD. 10 CHF}) = ((MoneyBag withAll: {10 CHF. 5 USD})) "=> true"
```

というように、両者は等価な MoneyBag を返すことを意味まします。

Bag に似せると言っても、MoneyBag は Money 同様イミュータブルなバリューオブジェクトなので adding プロトコルなどはサポートしない最低限の実装に止めます。

MoneyBag に対する Money の加算にはこんな感じの（しかし新たに作られた別の）MoneyBag を返す振舞いを期待します。

```Smalltalk
(3 USD + 10 CHF) + 2 USD "=> 5 USD + 10 CHF "
```


# MoneyBag の printing、comparing プロトコルを整える

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [ ] **MoneyBag に printOn: を再定義**
- [ ] MoneyBag に = と hash を再定義

…とその前に、テストも増えて来て MoneyTest のメソッドリスト枠も見にくくなってしまった（そもそも MoneyTest に盛り込みすぎな）ので、MoneyBag 関連のテストメソッドは独立したテストクラスである MoneyBagTest を新たに作成してそこにまとめてしまいましょう。

クラスリスト枠内の選択を解除し、コード枠に現れるクラス定義式を次のように編集して Accept します。

```Smalltalk
TestCase subclass: #MoneyBagTest
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'
```

MoneyBag を右クリックして Jump to test class を選択しても、自動的に MoneyBagTest クラスを作成してくれます。

![1031.png](https://qiita-image-store.s3.amazonaws.com/0/49275/3ff231ab-2983-f73b-70df-d99e7ad65d3d.png)

しかし自動生成した場合、Tests タグというものが付いて同名の別フォルダに分類されしまうため、ブラウザでの切り替えが面倒になります。

![1032.png](https://qiita-image-store.s3.amazonaws.com/0/49275/7b05f824-961d-e761-b3a3-d495eed1f044.png)

もとよりこういうときのためにクラスブラウザは複数開くことができるのですが、ここでは簡単のため MoneyTest と同様に、パッケージ内の他のクラスと一覧できるように Tests タグは付けずにおきましょう。

直近に示したクラス定義式のように package: の引数部分を 'TDD-Money-Tests' から 'TDD-Money' に変更して改めて Accept すると、Tests フォルダが消えて、MoneyBagTest が TDD-Money パッケージ内の他のクラスと同じ場所に表示されます。

Testsタグ（フォルダ）内の MoneyBagTest をドラッグして TDD-Money フォルダにドロップインしても同じことができます。

![1035.png](https://qiita-image-store.s3.amazonaws.com/0/49275/f524479a-80d3-70ed-355d-d716d0134f55.png)

![1038.png](https://qiita-image-store.s3.amazonaws.com/0/49275/d79a609c-3cbf-0717-c7b0-3dd8fa1caf25.png)

同様の方法でメソッドも別クラスに簡単に移動できます。MoneyTest のメソッドである testPlusDifferentCurrencyReturnsMoneyBag もこの機に MoneyBagTest に移動してしまいましょう。

![1070.png](https://qiita-image-store.s3.amazonaws.com/0/49275/6ddd1f35-2724-1c77-e08a-3e474aec8d75.png)

![1074.png](https://qiita-image-store.s3.amazonaws.com/0/49275/4675f0f3-e73a-f608-9f0d-6864feb259d7.png)

では MoneyBagTest にテストメソッドを追加してゆきましょう。プロトコルは tests です。

```Smalltalk
MoneyBagTest >> testPrintOn
	self assert: (5 USD + 10 CHF) printString equals: '(5 USD + 10 CHF)'
```

MoneyBagTest の ○印 をクリックして全テストを走らせるとイエローバーが出ます。

![1075.png](https://qiita-image-store.s3.amazonaws.com/0/49275/0cf57683-f0f1-fd35-db53-7a7c2d5a0a07.png)

printing プロトコルを MoneyBag に新たに作成し、仮実装のくだりは省略して重複を排除した MoneyBag>>printOn: を次ように書いてコンパイル（Accept）します。

```Smalltalk
MoneyBag >> printOn: aStream
	aStream nextPut: $(.
	elements
		do: [ :aMoney | aStream print: aMoney ]
		separatedBy: [ aStream nextPutAll: ' + ' ].
	aStream nextPut: $)
```

![1076.png](https://qiita-image-store.s3.amazonaws.com/0/49275/d7195f28-927f-1de8-8291-a573193be0b9.png)

とりあえずグリーンになりますが、MoneyBag を Bag っぽく振舞うようになるとテストが通らなくなるかもしれない（要素が順不同になる）ので、このテストは後で消してしまってもよいと思います。

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] **MoneyBag に printOn: を再定義**
- [ ] **MoneyBag に = と hash を再定義**

次に等価性チェックを実装（再定義）します。

```Smalltalk
MoneyBagTest >> testEquality
	self assert: 5 USD + 10 CHF equals: 5 USD + 10 CHF
```

![1077.png](https://qiita-image-store.s3.amazonaws.com/0/49275/a3e6a5cd-4542-efad-079e-668aed48343a.png)

結果はイエローです。

MoneyBag に = とついでに hash も(再)定義します。プロトコルは comparing です。

```Smalltalk
MoneyBag >> = other
	^ other class == self class and: [ other elements = elements ]
```
```Smalltalk
MoneyBag >> hash
	^ self class hash bitXor: elements hash
```

![1078.png](https://qiita-image-store.s3.amazonaws.com/0/49275/c3b132b6-ba73-de88-5766-c2e6bb8c05ab.png)

グリーンになります。

MoneyBag を Bag っぽく振舞わせるために、テストを足しましょう。

```Smalltalk
MoneyBagTest >> testEquality
	self assert: 5 USD + 10 CHF equals: 5 USD + 10 CHF.
	self assert: 5 USD + 10 CHF equals: 10 CHF + 5 USD
```

MoneyBag の加算の要素を収める elements は、生成時に withAll: で受け取った配列をそのまま使うよう実装されているのでこれはもちろん失敗します。

![1080.png](https://qiita-image-store.s3.amazonaws.com/0/49275/e270191d-f873-17a8-9177-bb04a7ac9e06.png)

参考まで現時点の MoneyBag class>>withAll: とそれがコールしている MoneyBag>>setElements: の実装は次のとおりです。

```Smalltalk
MoneyBag class >> withAll: aCollectionOfMoney
	^ self new setElements: aCollectionOfMoney; yourself
```
```Smalltalk
MoneyBag >> setElements: aCollectionOfMoney
	elements := aCollectionOfMoney
```

まず失敗しているテストを通すため、MoneyMag>> = を仮実装にします。

```Smalltalk
MoneyMag >> = other
	^ true
```

![1081.png](https://qiita-image-store.s3.amazonaws.com/0/49275/bdb5c4f3-080a-8ccc-8c08-9f43eddfd188.png)

MoneyBag の要素は配列ではなく、辞書で管理しましょう。currency を key に、Money を value として取るようにします。

まず、それに相応しい名前に変更するため、MoneyBag 右クリック → Rename inst var で elements を elemsDict に変えます。

![1082.png](https://qiita-image-store.s3.amazonaws.com/0/49275/65000ed6-8aee-b460-793b-091a8a4c0981.png)

![1083.png](https://qiita-image-store.s3.amazonaws.com/0/49275/1403add9-ea10-0a02-52b3-90acaffef9a6.png)

![1084.png](https://qiita-image-store.s3.amazonaws.com/0/49275/ffa66204-f7d0-0063-c7ad-e0e349d2dc85.png)

…とここで、確認のために出てきたリファクタリング作業の項目の多さがこれからの困難を予感させますね。いったん Cancel してこの作業はやめておきます。

![1086.png](https://qiita-image-store.s3.amazonaws.com/0/49275/36e878ab-11e1-79f7-7327-9ad0bc91d2d2.png)

elements への直接アクセス（[ベパ](https://www.amazon.co.jp/dp/4894717549/)p.99、[実パ](https://www.amazon.co.jp//dp/4894712873/)p.58）が良くなかったパターンっぽいですね。辞書による管理に切り替える前に、間接アクセス（[ベパ](https://www.amazon.co.jp/dp/4894717549/)p.101、[実パ](https://www.amazon.co.jp//dp/4894712873/)p.59）に変更しておくのが良さそうです。

MoneyBag 右クリック → Analyze → Inst var references... を選択すると elements を参照しているメソッドの一覧が表示されます。

![1087.png](https://qiita-image-store.s3.amazonaws.com/0/49275/8086da91-09d3-b65a-4cdc-15e94830c60c.png)

![1088.png](https://qiita-image-store.s3.amazonaws.com/0/49275/2c0e5bfe-5858-2b3a-df46-d354c77cc07b.png)

![1089.png](https://qiita-image-store.s3.amazonaws.com/0/49275/5ceb05dd-9279-585e-dfd6-eb202d6d011b.png)

elements の間接アクセスの際にこれから用いる MoneyBag>>elements と elements に代入している MoneyBag>>setElements: 、配列でも辞書でも影響の無い MoneyBag>>hash を除く次の２つのメソッドが書き換えの対象になります。

それぞれのメソッドをクリックして定義を下のコード枠に呼び出し、コード中の elements への直接アクセスを self elements（間接アクセス）に書き換えてコンパイルし直します。

```Smalltalk
MoneyBag >> exchangeTo: toCurrency with: aBank
	^ self elements
		inject: (0 perform: toCurrency)
		into: [ :acc :aMoney | acc + (aMoney exchangeTo: toCurrency with: aBank) ]
```
```Smalltalk
MoneyBag >> printOn: aStream
	aStream nextPut: $(.
	self elements
		do: [ :aMoney | aStream print: aMoney ]
		separatedBy: [ aStream nextPutAll: ' + ' ].
	aStream nextPut: $)
```

書き換えと再コンパイルが済んだらこのウインドウは閉じてしまいましょう。

準備は整いましたので、改めてインスタンス変数 elements を、MoneyBag 右クリック → Rename inst var で elemsDict に変更します。

続けて、この elements 改め elemsDict の初期化のためのメソッドを定義します。

MoneyBag を右リック → Analyze → Generate initialize method で MoneyBag>>initialize というメソッドを自動生成して追加し、elemsDict への代入を nil から Dictionary new に変えてコンパイルします。

```Smalltalk
MoneyBag >> initialize

	super initialize.

	elemsDict := Dictionary new.
```

![1091.png](https://qiita-image-store.s3.amazonaws.com/0/49275/c7ec86bc-a849-744a-22bf-9b38c3013677.png)


あとは、MoneyBag>>elements に elemsDict をそのままではなく、値の配列である values を返させます。

こうしておくことで、self elements で配列が返ってくることを期待している先ほど書き換えた２つのメソッド（exchangeTo:with: 、printOn: ）も問題なく動作するはずです。

```Smalltalk
MoneyBag >> elements
	^ elemsDict values

```

最後に配列を受け取る setElements: は、受け取った配列を重複なく辞書（elemsDict）に登録するよう書き換えておしまいです。

```Smalltalk
MoneyBag >> setElements: aCollectionOfMoney
	aCollectionOfMoney do: [ :aMoney | 
			elemsDict
				at: aMoney currency
				put: (elemsDict
						at: aMoney currency
						ifPresent: [ :pMoney | pMoney + aMoney ]
						ifAbsent: [ aMoney ]) ]
```

ああ、ひとつ忘れていました。このタイミングで MoneyBag>> = の仮実装もきちんと元に戻しておかねば…。

Smalltalk には古来より、メソッド単位でのバージョン履歴機能が備わっています。MoneyBag>> = を右クリック → Versions でこれまでの書き換え履歴の一覧が表示されます。

![1092.png](https://qiita-image-store.s3.amazonaws.com/0/49275/419f7322-2eb4-7c15-6509-b10f703544e4.png)

![1105.png](https://qiita-image-store.s3.amazonaws.com/0/49275/e0639d37-8788-1931-1112-7565cbb27b01.png)


仮実装にするひとつ手前（２番目）の実装を選択して Revert します。バージョンブラウザは不要なので閉じてしまいます。

元に戻ってはいるのですが、elemsDict に名前を変える前の elements に、しかも直接アクセスしていた時代のコードに戻ってしまうので、改めて self elements（間接アクセス）に変えてから再度コンパイルします。

```Smalltalk
MoneyBag >> = other
	^ other class == self class and: [ other elements = self elements ]
```

![1093.png](https://qiita-image-store.s3.amazonaws.com/0/49275/453132d8-8fc4-35b4-be02-dfebc72c9473.png)

少々歩幅が広すぎましたが、全テストの実行結果はグリーンとなり作業完了です。

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] **MoneyBag に = と hash を再定義**


# MoneyBag + Money と Money + MoneyBag と MoneyBag + MoneyBag

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] MoneyBag に = と hash を再定義
- [ ] **&thinsp;(3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)**
- [ ] &thinsp;3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
- [ ] &thinsp;(3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)

次は、MoneyBag と Money の加算をサポートします。

```Smalltalk
MoneyBagTest >> testPlusMoney
	self assert: (3 USD + 10 CHF) + 2 USD equals: 5 USD + 10 CHF
```

結果はもちろんレッド（エラー）です。testPlusMoney のみ走らせて、足りない MoneyBag>> + をいつも通り Create しましょう。

![1094.png](https://qiita-image-store.s3.amazonaws.com/0/49275/ae0a74b7-ae57-3d3b-8869-702120248b79.png)

プロトコルは arithmetic です。

```Smalltalk
MoneyBag >> + addend
```

仮引数を addend に変更し、self shouldBeImplemented を消して何もない（self を返す）メソッドをコンパイル（Accept）したら、

![1095.png](https://qiita-image-store.s3.amazonaws.com/0/49275/81d26787-d082-63ec-6657-192adf0b254f.png)

デバッガーは閉じてしまいます。

テストは失敗します（イエロー）。テストが期待する結果をそのまま返す仮実装に変えてコンパイルし直します。

```Smalltalk
MoneyBag >> + addend
	^ 5 USD + 10 CHF
```

![1096.png](https://qiita-image-store.s3.amazonaws.com/0/49275/9788f0fd-a092-cd69-f09a-a87cc9c3adb7.png)

期待通り、グリーンが出ます。重複の排除は次のようにします。

```Smalltalk
MoneyBag >> + addend
	^ self class withAll: (self elements copyWith: addend)
```

![1097.png](https://qiita-image-store.s3.amazonaws.com/0/49275/721ac8e5-067f-e4f1-f975-1e1880346090.png)

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] MoneyBag に = と hash を再定義
- [x] **&thinsp;(3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)**
- [ ] **&thinsp;3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)**
- [ ] &thinsp;(3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)

引き続き、Money に MoneyBag を加算する場合を考えましょう。

```Smalltalk
MoneyBagTest >> testMoneyPlusMoneyBag
	self assert: 3 USD + (10 CHF + 2 USD) equals: 5 USD + 10 CHF
```

結果はレッドです。testMoneyPlusMoneyBag のみを走らせて出てきたノーティファイアーの Debug をクリックしてデバッガーを起動すると、Money>> + の今の実装は addend に MoneyBag を考えていないため、MoneyBag は currency に答えられずエラー（レッド）になっていることが分かります。

![1098.png](https://qiita-image-store.s3.amazonaws.com/0/49275/4ee7e330-3aad-3b52-7a36-d6a0b5e8afa6.png)

そのままデバッガーのコード枠で、addend が Money の時の処理と条件分けして仮実装を書きます。

```Smalltalk
Money >> + addend
	addend isMoney ifTrue: [
		addend currency ~~ currency
			ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
		^ amount + addend amount perform: currency ].
	^ 5 USD + 10 CHF
```

![1099.png](https://qiita-image-store.s3.amazonaws.com/0/49275/27a7bce2-6d30-d930-422d-46a532c8ce0a.png)

いったんデバッガーを閉じて、テストが通ることを確認したら、MoneyBag である addend に委譲する形に書き直して重複の排除とします。

```Smalltalk
Money >> + addend
	addend isMoney ifTrue: [
		addend currency ~~ currency
			ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
		^ amount + addend amount perform: currency ].
	^ addend + self
```

![1100.png](https://qiita-image-store.s3.amazonaws.com/0/49275/68b1624a-24f0-ef73-58c2-d25250e95f2a.png)

Money の加算について、結合則が成り立つようになったみたいで面白いですね。

```Smalltalk
((3 USD + 10 CHF) + 2 USD) = (3 USD + (10 CHF + 2 USD)) "=> true "
```

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] MoneyBag に = と hash を再定義
- [x] &thinsp;(3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
- [x] **&thinsp;3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)**
- [ ] **&thinsp;(3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)**


引き続き、MoneyBag 同士の加算もサポートします。

```Smallltalk
MoneyBagTest >> testPlusMoneyBag
	self assert: (3 USD + 4 CHF) + (2 USD + 6 CHF) equals: 5 USD + 10 CHF
```

スタックトレースを見ると、MoneyBag>>setElements: まで進んでから currency でエラーを出していますが、問題は上流の（一覧の下の方にある） MoneyBag>> + で起こっています。

![1107.png](https://qiita-image-store.s3.amazonaws.com/0/49275/5550fa8d-420c-40c4-024a-e1cd9058be5a.png)

そのままデバッガー内で、addend が Money か否かで条件分けをした仮実装を書きます。

```Smalltalk
MoneyBag >> + addend
	addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
	^ 5 USD + 10 CHF
```

![1101.png](https://qiita-image-store.s3.amazonaws.com/0/49275/a15b5c0e-8da8-b8c7-e143-695044e391b3.png)

addend の MoneyBag の elements を一つずつ足す作業にして重複を排除します。


```Smalltalk
MoneyBag >> + addend
	addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
	^ addend elements inject: self into: [ :acc :aMoney | acc + aMoney ]
```

![1102.png](https://qiita-image-store.s3.amazonaws.com/0/49275/f2c2c129-dbb7-ff81-2593-5562e81f6e3a.png)

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] MoneyBag に = と hash を再定義
- [x] &thinsp;(3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
- [x] &thinsp;3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
- [x] **&thinsp;(3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)**


# MoneyBag の乗算サポート

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] MoneyBag に = と hash を再定義
- [x] &thinsp;(3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
- [x] &thinsp;3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
- [x] &thinsp;(3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)
- [ ] **&thinsp;(5 USD + 10 CHF) * 2 = (10 USD + 20 CHF)**


```Smalltalk
MoneyBagTest >> testMoneyBagTimes
	self assert: (5 USD + 10 CHF) * 2 equals: 10 USD + 20 CHF
```

テストの結果はレッドなので、いつもどおり testMoneyBagTimes のみ走らせてノーティファイアーから MoneyBag>> * を Create します。プロトコルは arithmetic です。

```Smalltalk
MoneyBag >> * multiplier
```

仮引数を multiplier に変えて、self shouldBeImplemented を削除したらコンパイルし、デバッガーは閉じてしまいます。

MoneyBagTest の全テストを走らせると結果はイエローです。

仮実装は次のようにしましょう。

```Smalltalk
MoneyBag >> * multiplier
	^ self class withAll: {10 USD. 20 CHF}
```

![1103.png](https://qiita-image-store.s3.amazonaws.com/0/49275/f519712b-a431-1e43-c8e6-a13fe077791c.png)

Squeak や Pharo には、APL や R ライクな…などと言うとおこがましいので、それらから見れば実にささやかな配列演算が組み込みで用意されています。

```Smalltalk
#(1 2 3) * 4 "=> #(4 8 12) "
```

重複の排除には、これを使いましょう。

```Smalltalk
ManeyBag >> * multiplier
	^ self class withAll: self elements * multiplier
```

![1104.png](https://qiita-image-store.s3.amazonaws.com/0/49275/4054a5ba-2b18-2da1-c0ca-b35e65769317.png)

###### **TODO リスト**
- [x] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 3 USD + 4 USD = 7 USD
- [x] 5 USD + 10 CHF は MoneyBag を返す
- [x] MoneyBag に printOn: を再定義
- [x] MoneyBag に = と hash を再定義
- [x] &thinsp;(3 USD + 10 CHF) + 2 USD = (5 USD + 10 CHF)
- [x] &thinsp;3 USD + (10 CHF + 2 USD) = (5 USD + 10 CHF)
- [x] &thinsp;(3 USD + 4 CHF) + (2 USD + 6 CHF) = (5 USD + 10 CHF)
- [x] **&thinsp;(5 USD + 10 CHF) * 2 = (10 USD + 20 CHF)**



# ここまでのまとめと補足
- Bag は多重集合を扱うためのコレクション
- Analyze → Inst var references... でインスタンス変数を参照しているメソッド群を、Inst var assignments... でインスタンス変数に代入しているメソッドを呼び出せる。
- APLライクな配列演算は便利。ただし効率は良くないので注意。


#この時点のコード
ファイルアウト形式のソースです。（この形式で Smalltalk のソースを読むことはあまり推奨されません。為念）

Pharo では、いったんこのコードをテキストファイルに保存してから、そのファイルを Pharo 内から Tools → File Browser で開き、Filein ボタンをクリックすると読み込めます。

Squeak でも読み込みや実行が可能なコードにもなっています。Pharo 同様に File List で開くか、デスクトップクリック → Workspace で開いたワークスペースなどにコピペし、alt/cmd + shift + g などで file it in するとクラスブラウザから閲覧したり、デスクトップクリック → Test Runner でテストも試せます（ただし使い勝手は Pharo に比べるとかなり落ちます^^;）。

[[前回からの差分はこちら](https://github.com/sumim/tdd-money/commit/d2b5a8d87a5f13e7d78c4af4bbe8dcda87d931f2)]

```Smalltalk
!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/7/2017 18:16'!
USD
	^ Money amount: self currency: #USD! !


!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/7/2017 18:16'!
CHF
	^ Money amount: self currency: #CHF! !


!Object methodsFor: '*TDD-Money' stamp: 'sumim 12/14/2017 18:03'!
isMoney
	^ false! !


Object subclass: #Bank
	instanceVariableNames: 'xRateDict'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!Bank methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:39'!
addXRate: aMoneyXRate
	^ xRateDict at: aMoneyXRate fromToCurrency put: aMoneyXRate! !


!Bank methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:42'!
xRateOf: anAssociation
	anAssociation key == anAssociation value ifTrue: [ ^ 1 ]. 
	^ xRateDict at: anAssociation! !


!Bank methodsFor: 'exchanging' stamp: 'sumim 12/16/2017 09:05'!
exchange: aMoneyOrBag to: toCurrency 
	^ aMoneyOrBag exchangeTo: toCurrency with: self! !


!Bank methodsFor: 'initialization' stamp: 'sumim 12/16/2017 08:42'!
initialize

	super initialize.

	xRateDict := Dictionary new.! !


Object subclass: #Money
	instanceVariableNames: 'amount currency'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!Money methodsFor: 'accessing' stamp: 'sumim 12/7/2017 17:32'!
currency
	^ currency! !


!Money methodsFor: 'accessing' stamp: 'sumim 12/5/2017 17:28'!
amount
	^ amount! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/15/2017 15:06'!
* multiplier
	multiplier isNumber
		ifTrue: [ ^ amount * multiplier perform: currency ].
	self assert: multiplier fromCurrency == currency.
	^ amount * multiplier xRate perform: multiplier toCurrency! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/14/2017 18:05'!
/ divisor
	divisor isNumber
		ifTrue: [ ^ amount / divisor perform: currency ].
	divisor isMoney
		ifFalse: [ ^ self error: 'unsupported divisor' ].
	^ divisor currency == currency
		ifTrue: [ amount / divisor amount ]
		ifFalse: [ MoneyXRate new
				setFromToCurrency: divisor currency -> currency
				xRate: amount / divisor amount ]! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/19/2017 18:05'!
+ addend
	addend isMoney ifTrue: [
		addend currency ~~ currency
			ifTrue: [ ^ MoneyBag withAll: {self. addend} ].
		^ amount + addend amount perform: currency ].
	^ addend + self! !


!Money methodsFor: 'comparing' stamp: 'sumim 12/7/2017 17:50'!
= other
	^ other currency == currency and: [ other amount = amount ]! !


!Money methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
hash
	^ currency hash bitXor: amount hash! !


!Money methodsFor: 'exchanging' stamp: 'sumim 12/16/2017 09:08'!
exchangeTo: toCurrency with: aBank 
	^ self * (aBank xRateOf: currency -> toCurrency)! !


!Money methodsFor: 'printing' stamp: 'sumim 12/7/2017 18:08'!
printOn: aStream
	aStream print: amount; space; nextPutAll: currency! !


!Money methodsFor: 'private' stamp: 'sumim 12/7/2017 17:41'!
setAmount: aNumber currency: aSymbol
	amount := aNumber.
	currency := aSymbol! !


!Money methodsFor: 'testing' stamp: 'sumim 12/14/2017 18:04'!
isMoney
	^ true! !


!Money class methodsFor: 'instance creation' stamp: 'sumim 12/7/2017 18:15'!
amount: aNumber currency: aSymbol
	^ self new setAmount: aNumber currency: aSymbol; yourself! !


Object subclass: #MoneyBag
	instanceVariableNames: 'elemsDict'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!MoneyBag methodsFor: 'accessing' stamp: 'sumim 12/19/2017 17:55'!
elements
	^ elemsDict values! !


!MoneyBag methodsFor: 'arithmetic' stamp: 'sumim 12/19/2017 18:14'!
+ addend
	addend isMoney ifTrue: [ ^ self class withAll: (self elements copyWith: addend) ].
	^ addend elements inject: self into: [ :acc :aMoney | acc + aMoney ]! !


!MoneyBag methodsFor: 'arithmetic' stamp: 'sumim 12/19/2017 18:15'!
* multiplier
	^ self class withAll: self elements * multiplier! !


!MoneyBag methodsFor: 'comparing' stamp: 'sumim 12/19/2017 18:00'!
= other
	^ other class == self class and: [ other elements = self elements ]! !


!MoneyBag methodsFor: 'comparing' stamp: 'sumim 12/19/2017 17:55'!
hash
	^ self class hash bitXor: elemsDict hash! !


!MoneyBag methodsFor: 'exchanging' stamp: 'sumim 12/19/2017 17:53'!
exchangeTo: toCurrency with: aBank
	^ self elements
		inject: (0 perform: toCurrency)
		into: [ :acc :aMoney | acc + (aMoney exchangeTo: toCurrency with: aBank) ]! !


!MoneyBag methodsFor: 'printing' stamp: 'sumim 12/19/2017 17:53'!
printOn: aStream
	aStream nextPut: $(.
	self elements
		do: [ :aMoney | aStream print: aMoney ]
		separatedBy: [ aStream nextPutAll: ' + ' ].
	aStream nextPut: $)! !


!MoneyBag methodsFor: 'private' stamp: 'sumim 12/19/2017 17:57'!
setElements: aCollectionOfMoney
	aCollectionOfMoney do: [ :aMoney | 
			elemsDict
				at: aMoney currency
				put: (elemsDict
						at: aMoney currency
						ifPresent: [ :pMoney | pMoney + aMoney ]
						ifAbsent: [ aMoney ]) ]! !


!MoneyBag methodsFor: 'initialization' stamp: 'sumim 12/19/2017 17:55'!
initialize

	super initialize.

	elemsDict := Dictionary new.! !


!MoneyBag class methodsFor: 'instance creation' stamp: 'sumim 12/17/2017 16:47:06'!
withAll: aCollectionOfMoney
	^ self new setElements: aCollectionOfMoney; yourself! !


Object subclass: #MoneyXRate
	instanceVariableNames: 'fromToCurrency xRate'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:06'!
fromCurrency
	^ fromToCurrency key! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:07'!
toCurrency
	^ fromToCurrency value! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/14/2017 18:06'!
xRate
	^ xRate! !


!MoneyXRate methodsFor: 'accessing' stamp: 'sumim 12/16/2017 08:36'!
fromToCurrency
	^ fromToCurrency! !


!MoneyXRate methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
= other
	^ other class == self class
		and: [ other fromToCurrency = fromToCurrency and: [ other xRate = xRate ] ]! !


!MoneyXRate methodsFor: 'comparing' stamp: 'sumim 12/14/2017 18:08'!
hash
	^ fromToCurrency hash bitXor: xRate hash! !


!MoneyXRate methodsFor: 'printing' stamp: 'sumim 12/14/2017 18:08'!
printOn: aStream
	aStream
		print: xRate;
		space;
		nextPutAll: self toCurrency;
		nextPutAll: ' / ';
		print: 1;
		space;
		nextPutAll: self fromCurrency! !


!MoneyXRate methodsFor: 'private' stamp: 'sumim 12/14/2017 18:05'!
setFromToCurrency: anAssociation xRate: aNumber
	fromToCurrency := anAssociation.
	xRate := aNumber! !


TestCase subclass: #MoneyTest
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 08:28'!
testBankSettingGettingXRate
	| xRate bank |
	xRate := 1 USD / 2 CHF.
	bank := Bank new.
	bank addXRate: xRate.
	self assert: (bank xRateOf: xRate fromToCurrency) equals: xRate! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:28'!
testCurrency
	self assert: 5 USD currency equals: #USD.
	self assert: 5 CHF currency equals: #CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:04'!
testDivideDifferentCurrencyReturnsMoneyXRate
	| aMoneyXRate |
	aMoneyXRate := 1 USD / 2 CHF.
	self assert: aMoneyXRate class equals: MoneyXRate! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:00'!
testDividedByNum
	self assert: 2 USD / 2 equals: 1 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:02'!
testDividedBySameCurrency
	self assert: 2 USD / 1 USD equals: 2! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 12:55'!
testEquality
	self assert: 5 USD = 5 USD.
	self assert: 5 USD ~= 6 USD.
	self assert: 5 CHF = 5 CHF.
	self assert: 5 CHF ~= 6 CHF.
	self assert: 5 USD ~= 5 CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 08:44'!
testExchangeMoney
	| bank |
	bank := Bank new.
	bank addXRate: 1 USD / 2 CHF.
	self assert: (bank exchange: 10 CHF to: #USD) equals: 5 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 09:00'!
testExchangeMoneyDifferentCurrency
	| bank result |
	bank := Bank new.
	bank addXRate: 1 USD / 2 CHF.
	result := bank exchange: 5 USD + 10 CHF to: #USD.
	self assert: result equals: 10 USD! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:30'!
testFiveCHFReturnsAMoney
	self assert: 5 CHF class equals: Money! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:30'!
testFiveUSDReturnsAMoney
	self assert: 5 USD class equals: Money! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/14/2017 18:03'!
testIsMoney
	self deny: 1 isMoney.
	self assert: 1 USD isMoney! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 12:56'!
testMultiplication
	self assert: 5 USD * 2 equals: 10 USD.
	self assert: 5 CHF * 2 equals: 10 CHF! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/7/2017 17:28'!
testPrintString
	self assert: 5 USD printString equals: '5 USD'.
	self assert: 5 CHF printString equals: '5 CHF'! !


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/16/2017 08:47'!
testSimpleAddition
	self assert: 3 USD + 4 USD equals: 7 USD! !


TestCase subclass: #MoneyBagTest
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'TDD-Money'!


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 17:52'!
testEquality
	self assert: 5 USD + 10 CHF equals: 5 USD + 10 CHF.
	self assert: 5 USD + 10 CHF equals: 10 CHF + 5 USD! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:15'!
testMoneyBagTimes
	self assert: (5 USD + 10 CHF) * 2 equals: 10 USD + 20 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:03'!
testMoneyPlusMoneyBag
	self assert: 3 USD + (10 CHF + 2 USD) equals: 5 USD + 10 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 17:49'!
testPlusDifferentCurrencyReturnsMoneyBag
	| sum |
	sum := 5 USD + 10 CHF.
	self assert: sum class equals: MoneyBag! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:01'!
testPlusMoney
	self assert: (3 USD + 10 CHF) + 2 USD equals: 5 USD + 10 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 18:05'!
testPlusMoneyBag
	self assert: (3 USD + 4 CHF) + (2 USD + 6 CHF) equals: 5 USD + 10 CHF! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/19/2017 17:49'!
testPrintOn
	self assert: (5 USD + 10 CHF) printString equals: '(5 USD + 10 CHF)'! !
```