4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SmalltalkAdvent Calendar 2017

Day 21

テスト駆動開発でお試しする Pharo Smalltalk・最終回 MoneyやMoneyBagがらみの乗加算をダブルディスパッチに変える

Last updated at Posted at 2017-12-21

新訳版『テスト駆動開発』の第Ⅰ部の多国通貨をネタに Smalltalk を学ぶシリーズの第9回です。同書と似た仕様を目指しますが、好みを優先する等の都合で写経にはなっていませんのでどうぞあしからず。(12/24追記:なるべく忠実な写経は別に書きました→こちら

前回 は、きっとこんな感じなのではないかな…と勝手な想像を巡らしながら実装を試した“MoneyBag”を、気持ち Bag っぽく振舞うようにちょとした内部表現の改造を試みました。

最終回となる今回は、Money や MoneyBag を adapting プロトコルに対応させることで、それらの組み合わせによる乗加算等の実装を大胆に変えてみます。

ダブルディスパッッチのおさらい

説明はいらないかと思いますが、ダブルディスパッチ(Double Dispatch, ベパp.62)について。

Smalltalk において(Smalltalk に限らず、第一オペランドの型に処理を持たせるタイプの言語は皆そうですが…) 3 + 4 などの二項演算を模した二項メッセージ(3 に + 4 を送る)では、メッセージの引数(3 + 4 の場合は 4 )にどんなオブジェクトが来るかで処理を変える必要が生じる場面がよくあります。

オペランドの組み合わせによって演算子を(あるいは引数の型でメソッドを)オーバーロードできる静的型付きの言語であれば問題はないのですが(引数の型の組み合わせ爆発はさておき)、動的型言語の場合は何らかの方法で引数の型によって処理を分岐させなければなりません。

引数の型を見て、それをもとに素直に if で分岐させるのも手ですが、レシーバーによって多態(ある種の分岐)させることができる(メッセージングの)オブジェクト指向の特性を活かした別の方法がダブルディスパッチです。

引数となるオブジェクトに対して、self(そのコンテキストでのレシーバー)を引数として添えて改めて別のメッセージを送ることでコールされるメソッドで処理を行えば、当初の式におけるレシーバーと引数の両方の型を確定した状態で処理を記述することができるので何かと便利です。

たとえば、#(1 2 3) * 4 がどのように処理されるか実際に見てみましょう。

Pharo が実行中なら、デスクトップをクリックして出るメニューから Playground を選択してウインドウを表示させます。これはかつて Workspace と呼ばれた(あるいは Squeak等、他の Smalltalk 処理系では今もそう呼ぶ)書き捨てのメモ帳機能で、Smalltalk の式を気ままに書いて実行するのによく利用されます。またケント・ベックらが後に xUnit と呼ばれるようになるテストフレームワークを思いつくのにも役に立った、古来からある Smalltalk 環境の機能でもあります。

この Playground に

#(1 2 3) * 4

とタイプして入力し(この場合は不要ですが)ドラッグして選択して、右クリックメニューから Debug it を選択します。

1109.png

するとデバッガーが開くので、ウインドウ右上の Into ボタンをクリックしてみてください。

1110.png

#(1 2 3) への * 4 というメッセージ送信は、まず、#(1 2 3) の属する Array のスーパークラスにある Collection>> * というメソッドをコールします。そこに書いてあるのは1行だけの、

Collection >> * arg
	^ arg adaptToCollection: self andSend: #*

という式からなるメソッドです。この式は引数である 4 に改めて adaptToCollection: self andSend: #* というメッセージを送る(そしてその返値をリターン ^ する)ことを意味します。

引き続き、Into ボタンをクリックし、先のメッセージ送信によりコールされる Number>>adaptToCollection:andSend: メソッドのコンテキストに切り替えると、そこには実際の処理内容が書かれています。

Number >> adaptToCollection: rcvr andSend: selector
	"If I am involved in arithmetic with a Collection, return a Collection of
	the results of each element combined with me in that expression."

	^ rcvr collect: [:element | element perform: selector with: self]

前のコンテキストの self(今のコンテキストでは rcvr)である #(1 2 3) に、collect: [:element | element perform: selector with: self] というメッセージを送っています。この場合 self は 4、selector は #* なので、

#(1 2 3) collect: [:element | element * 4]

となり、各要素に * 4 を送り直して collect: するという処理だと分かります。

では、#(1 2 3) * #(4 5 6) ならどうでしょう。同じように、Debug it して調べてみてください。

今度は、Number>>adaptToCollection:andSend: ではなく、Collection>>adaptToCollection:andSend: がコールされ、適切な処理がなされます。

1111.png

この動きを真似て、今のところ if 分岐で実現されている Money の乗加算をダブルディスパッチを使った処理に置き換えてみましょう。

Money>> * に adaptToMoney:andSend: をコールさせる

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築

現状の Money>> * はこのようになっています。

Money >> * multiplier
	multiplier isNumber
		ifTrue: [ ^ amount * multiplier perform: currency ].
	self assert: multiplier fromCurrency == currency.
	^ amount * multiplier xRate perform: multiplier toCurrency

multiplier が Number かそうでないか(すなわち MoneyXRate か)で処理を分けています。これを次の書き換えてコンパイル(Accept)します。

Money >> * multiplier
	^ multiplier adaptToMoney: self andSend: #*

MoneyTest と MoneyBagTest の ○印 をクリックしてそれぞれの全テストを走らせると、もちろん結果はレッド(エラー)です。MoneyTest では 乗算関係のテストが3つ赤くなっているのが分かります。

1112.png

とりあえず testMultiplication の ○印 をクリックして走らせてノーティファイアーを出し、エラーの内容を確かめます。

1113.png

つまるところ、5 USD * 2 の引数である 2(のクラスである SmallInteger)に adaptToMoney:andSend: が定義されていない…というエラーのようです。

であれば、ここではもうお馴染みになった Create でさくっと定義してあげましょう。定義先は Smallinteger のスーパークラスのひとつである Number 、

1114.png

プロトコルは(本来は adapting なのですが、パッケージに含めたいので…)*TDD-Money にします。

1115.png

テストをエラー(レッド)から失敗(イエロー)に変えたいので、aMoney をそのまま返すメソッドにします。二番目の仮引数名は selector に変更しておきます。

Number >> adaptToMoney: aMoney andSend: selector
	^ aMoney

1117.png

ちなみにセレクター(selector)というのは、Smalltalk においてメッセージの構成要素のひとつ(もうひとつは引数)で、メッセージセレクターとも呼ばれるものです。こう書くとなんか難しそうですが、通常の言語でいうところのたんなる「メソッド名」のことです。実体はシンボル(同一性が保証されたイミュータブルな文字列オブジェクト。リテラル式は、トークンまたは文字列リテラルの前に # を付す→ #abc )で表現されます。

MoneyTest の全テストを走らせると、あいかわらず赤のままのテストはありますが、testMultiplication は無事イエローに変わります。

1116.png

仮実装としては、こんなふうにすれば testMultiplication は通るはずです。

Number >> adaptToMoney: aMoney andSend: selector
	^ (aMoney amount * 2) perform: aMoney currency

1118.png

* 2 の部分を self を伴った selector のコールの式に書き換えて重複を排除します。

Number >> adaptToMoney: aMoney andSend: selector
	selector == #* ifTrue: [ ^ (aMoney amount * self) perform: aMoney currency ].
	self error: 'unexpected operation'

1119.png

赤のまま残っている testExchangeMoney と testExchangeMoneyDifferentCurrency はなぜエラーなのでしょうか? ○印 をクリックして確認します。

1120.png

どうやらこちらは MoneyXRate に adaptToMoney:andSend: が無いので怒っているようです。Create してあげます。プロトコルは MoneyXRate自体がすでにパッケージに含まれているので本来の adapting で大丈夫です。

仮実装のくだりは省いて、いきなり本実装を書いてしまいましょう。

Money>> * の前の実装のときと同様に、aMoney currency と self fromCurrency が一致していない場合は通貨変換が成立しないことはもちろんですが、それに加えて乗算(*)以外は受け付けないことも新たに条件として記述する必要があり、あまりシンプルになった感がないのがちょっと残念です。

MoneyXRate >> adaptToMoney: aMoney andSend: selector
	(selector == #* and: [ aMoney currency == self fromCurrency ])
		ifTrue: [ ^ aMoney amount * xRate perform: self toCurrency ].
	self error: 'unexpected operation'

1121.png

デバッガーを閉じて全テストを走らせるとレッドは解消しているはずです。

1122.png

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築

Money>> + に adaptToMoney:andSend: をコールさせる

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築
  • Money>> + を adaptToMoney:andSend: を使って再構築

現状の Money>> + はこうなっています。

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

addend が Money かそれ以外か(MoneyBag か)で処理を分けていますね。これをシンプルに adaptToMoney:andSend: のコールに置き換えます。

Money >> + addend
	^ addend adaptToMoney: self andSend: #+

MoneyTest と MoneyBagTest の全テストを走らせると、MoneyTest の testExchangeMoneyDifferentCurrency と testSimpleAddtion、 MoneyBagTest の全テストがレッドになります。

1128.png

この機に MoneyTest の testExchangeMoneyDifferentCurrency と testSimpleAddtion は MoneyBagTest に移動してしまいましょう。

1124.png

MoneyTest>>testSimpleAddtion 改め、MoneyBagTest>>testSimpleAddtion のみを走らせると Money に adaptToMoney:andSend: が無いことがエラーの原因だと分かるので Create します。プロトコルは adapting です。

1143.png

テストを失敗させるには aMoney をそのまま返せばよいでしょう。

Money >> adaptToMoney: aMoney andSend: selector
	^ aMoney

1144.png

デバッガーを閉じて MoneyBagTest の全テストを走らせます。

1145.png

通ってしまっているテストもありますが、これはチェックにも Money>> + を使ってしまっているからですね。今は手をつけずにおきます。

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

Money>>adaptToMoney:andSend: には、変更前の Money>> + メソッドの isMoney チェック以降と同じ内容を書けばよいのですが(上の再掲を参照)、ここは addend をレシーバーにして呼ばれたコンテキストなので、Money>> + における self と addend の関係が逆転して aMoney と self に置き換わっているのがやっかいですね。

Money >> adaptToMoney: aMoney andSend: selector
	selector == #+ ifTrue: [
		^ currency == aMoney currency
			ifTrue: [ aMoney amount + amount perform: currency ]
			ifFalse: [ MoneyBag withAll: {aMoney. self} ] ].
	self error: 'unexpected operation'

面倒であれば、+ 可換性に頼って、もとのコードの addend をそのまま aMoney に置き換えるだけても大丈夫でしょう。

テストは全部通る…かと思いきや、

1147.png

testMoneyPlusMoneyBag がレッドのままです。このテストのみ走らせると、MoneyBag にも adaptToMoney:andSend: が必要だとわかります。Create しましょう。

MoneyBag >> adaptToMoney: aMoney andSend: selector
	selector == #+ ifTrue: [ ^ self + aMoney ].
	self error: 'unexpected operation'

1149.png

これで全テストが通ります。

TODO リスト
  • Money>> * を adapting プロトコルに書き換える
  • Money>> + を adapting プロトコルに書き換える

Money>> / に adaptToMoney:andSend: をコールさせる

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築
  • Money>> + を adaptToMoney:andSend: を使って再構築
  • Money>> / を adaptToMoney:andSend: を使って再構築

調子に乗って Money>> / もやってしまいましょう。現状の Money>> / はこうなっています。

Money >> / 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 >> / divisor
	^ divisor adaptToMoney: self andSend: #/

テストはもちろん真っ赤です。

1151.png

簡単そうな testDividedByNum からいきます。

1152.png

これは予防的に設置した error ですね。Debug ボタンを押してデバッガーを起動し、Number>>adaptToMoney:andSend: を除算も許すように書き換えます。

Number >> adaptToMoney: aMoney andSend: selector
	(#(* /) includes: selector)
		ifTrue: [ ^ (aMoney amount perform: selector with: self) perform: aMoney currency ].
	self error: 'unexpected operation'

1153.png

次は testDivideDifferentCurrencyReturnsMoneyXRate です。

1154.png

こちらは Money>>adaptToMoney:andSend: に設置したエラーです。 / に対応させます。

Money >> adaptToMoney: aMoney andSend: selector
	selector == #+ ifTrue: [ ^ aMoney currency == currency
		ifTrue: [ aMoney amount + amount perform: currency ]
		ifFalse: [ MoneyBag withAll: {self. aMoney} ] ].
	selector == #/ ifTrue: [ ^ aMoney currency == currency
		ifTrue: [ aMoney amount / amount ]
		ifFalse: [ MoneyXRate new
			setFromToCurrency: currency -> aMoney currency
				xRate: aMoney amount / amount
			; yourself ] ].
	self error: 'unexpected operation'

1155.png

なんか if 分岐が増えたような感じがしますが、気にしない。気にしない。

いまさらですが、MoneyXRate のインスタンス生成メソッドを定義し忘れていました。

Money >> adaptToMoney: aMoney andSend: selector
	selector == #+ ifTrue: [ ^ aMoney currency == currency
		ifTrue: [ aMoney amount + amount perform: currency ]
		ifFalse: [ MoneyBag withAll: {self. aMoney} ] ].
	selector == #/ ifTrue: [ ^ aMoney currency == currency
		ifTrue: [ aMoney amount / amount ]
		ifFalse: [ MoneyXRate is: self equals: aMoney ] ].
	self error: 'unexpected operation'

1156.png

Proceed するとエラーになるので instance creation プロトコルに MoneyXRate>>is:equals: を Create しておきます。

MoneyXRate class >> is: fromMoney equals: toMoney
	^ self new
		setFromToCurrency: fromMoney currency -> toMoney currency
			xRate: toMoney amount / fromMoney amount;
		yourself

これで全テストが通ります。

1157.png

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築
  • Money>> + を adaptToMoney:andSend: を使って再構築
  • Money>> / を adaptToMoney:andSend: を使って再構築

2 * 5 USD = 10 USD

ここまできたら、Number * Money や Number * MoneyBag にも対応してしまいましょう。

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築
  • Money>> + を adaptToMoney:andSend: を使って再構築
  • Money>> / を adaptToMoney:andSend: を使って再構築
  • 2 * 5 USD = 10 USD
  • 2 * (5 USD + 10 CHF) = (10 USD + 20 CHF)

ふと気がつけば、ひどく腫れ上がった手でテストを書きます。

MoneyTest >> testNumMultiply
	| bank result |
	self assert: 2 * 5 USD equals: 10 USD

1158.png

ノーティファイアーに従い、Money に adaptToNumber:andSend: を追加します。プロトコルは adapting です。

Money >> adaptToNumber: aNumber andSend: selector

1160.png

仮実装には 10 USD を返させればよいでしょう。

Money >> adaptToNumber: aNumber andSend: selector
	^ 10 USD

1161.png

重複の排除は次のようにします。

Money >> adaptToNumber: aNumber andSend: selector
	selector == #* ifTrue: [ ^ aNumber * amount perform: currency ].
	self error: 'unexpected operation'

1162.png

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築
  • Money>> + を adaptToMoney:andSend: を使って再構築
  • Money>> / を adaptToMoney:andSend: を使って再構築
  • 2 * 5 USD = 10 USD
  • 2 * (5 USD + 10 CHF) = 20 USD

残りは Num * MoneyBag です。

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

1163.png

仮実装等のくだりは省略します。

MoneyBag >> adaptToNumber: aNumber andSend: selector
	selector == #* ifTrue: [ ^ self class withAll: aNumber * self elements ].
	self error: 'unexpected operation'

1164.png

TODO リスト
  • Money>> * を adaptToMoney:andSend: を使って再構築
  • Money>> + を adaptToMoney:andSend: を使って再構築
  • Money>> / を adaptToMoney:andSend: を使って再構築
  • 2 * 5 USD = 10 USD
  • 2 * (5 USD + 10 CHF) = 20 USD

#ここまでのまとめと補足

  • Squeak や Pharo の adapting プロトコルは、各種オブジェクト間四則演算等で用いられているダブルディスパッチ機構。
  • adaptToHOGE:andSend: でダブルディスパッチをすれば、レシーバーと引数の型の組み合わせを特定して処理を書ける。が、演算の意味が多いとセレクターで分岐しないといけないのでうれしさは半分。
  • adaptToNumber:andSend: を実装すれば Number をレシーバーにした四則演算等に自作クラスを対応させることができる
  • 素直に Composit パターンにすればよかった…
  • Pharo をこんなに使ったのは初めてですが、プラグマティックな側面での進化を感じました。でも again が無いので、自分的にワープロ代わりに使うのはこれからも Squeak 。
  • 穴埋め企画としてはよく頑張ったと思う。>自分w

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

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

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

[前回からの差分はこちら]

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


!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! !


!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/20/2017 19:11'!
adaptToMoney: aMoney andSend: selector
	(#(* /) includes: selector)
		ifTrue: [ ^ (aMoney amount perform: selector with: self) perform: aMoney currency ].
	self error: 'unexpected operation'! !


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: 'adapting' stamp: 'sumim 12/20/2017 19:20'!
adaptToNumber: aNumber andSend: selector
	selector == #* ifTrue: [ ^ aNumber * amount perform: currency ].
	self error: 'unexpected operation'! !


!Money methodsFor: 'adapting' stamp: 'sumim 12/20/2017 19:14'!
adaptToMoney: aMoney andSend: selector
	selector == #+ ifTrue: [ ^ aMoney currency == currency
		ifTrue: [ aMoney amount + amount perform: currency ]
		ifFalse: [ MoneyBag withAll: {self. aMoney} ] ].
	selector == #/ ifTrue: [ ^ aMoney currency == currency
		ifTrue: [ aMoney amount / amount ]
		ifFalse: [ MoneyXRate is: self equals: aMoney ] ].
	self error: 'unexpected operation'! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/20/2017 18:22'!
* multiplier
	^ multiplier adaptToMoney: self andSend: #*! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/20/2017 18:46'!
+ addend
	^ addend adaptToMoney: self andSend: #+! !


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/20/2017 19:08'!
/ divisor
	^ divisor adaptToMoney: self andSend: #/! !


!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: 'testing' stamp: 'sumim 12/14/2017 18:04'!
isMoney
	^ true! !


!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 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: 'adapting' stamp: 'sumim 12/20/2017 19:22'!
adaptToNumber: aNumber andSend: selector
	selector == #* ifTrue: [ ^ self class withAll: aNumber * self elements ].
	self error: 'unexpected operation'! !


!MoneyBag methodsFor: 'adapting' stamp: 'sumim 12/20/2017 19:07'!
adaptToMoney: aMoney andSend: selector
	selector == #+ ifTrue: [ ^ self + aMoney ].
	self error: 'unexpected operation'! !


!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: 'initialization' stamp: 'sumim 12/19/2017 17:55'!
initialize

	super initialize.

	elemsDict := Dictionary new.! !


!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: '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 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: 'private' stamp: 'sumim 12/14/2017 18:05'!
setFromToCurrency: anAssociation xRate: aNumber
	fromToCurrency := anAssociation.
	xRate := aNumber! !


!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: '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: '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: 'adapting' stamp: 'sumim 12/20/2017 18:44'!
adaptToMoney: aMoney andSend: selector
	(selector == #* and: [ aMoney currency == self fromCurrency ])
		ifTrue: [ ^ aMoney amount * xRate perform: self toCurrency ].
	self error: 'unexpected operation'! !


!MoneyXRate class methodsFor: 'instance creation' stamp: 'sumim 12/20/2017 19:16'!
is: fromMoney equals: toMoney
	^ self new
		setFromToCurrency: fromMoney currency -> toMoney currency
			xRate: toMoney amount / fromMoney amount;
		yourself! !


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/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/20/2017 19:17'!
testNumMultiply
	| bank result |
	self assert: 2 * 5 USD equals: 10 USD! !


!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'! !


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/20/2017 18:54'!
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! !


!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/20/2017 19:20'!
testNumMultiply
	self assert: 2 * (5 USD + 10 CHF) equals: 10 USD + 20 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)'! !


!MoneyBagTest methodsFor: 'tests' stamp: 'sumim 12/20/2017 18:54'!
testSimpleAddition
	self assert: 3 USD + 4 USD equals: 7 USD! !
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?