新訳版『テスト駆動開発』の第Ⅰ部の多国通貨をネタに 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 を選択します。
するとデバッガーが開くので、ウインドウ右上の Into ボタンをクリックしてみてください。
#(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: がコールされ、適切な処理がなされます。
この動きを真似て、今のところ 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つ赤くなっているのが分かります。
とりあえず testMultiplication の ○印 をクリックして走らせてノーティファイアーを出し、エラーの内容を確かめます。
つまるところ、5 USD * 2 の引数である 2(のクラスである SmallInteger)に adaptToMoney:andSend: が定義されていない…というエラーのようです。
であれば、ここではもうお馴染みになった Create でさくっと定義してあげましょう。定義先は Smallinteger のスーパークラスのひとつである Number 、
プロトコルは(本来は adapting なのですが、パッケージに含めたいので…)*TDD-Money にします。
テストをエラー(レッド)から失敗(イエロー)に変えたいので、aMoney をそのまま返すメソッドにします。二番目の仮引数名は selector に変更しておきます。
Number >> adaptToMoney: aMoney andSend: selector
^ aMoney
ちなみにセレクター(selector)というのは、Smalltalk においてメッセージの構成要素のひとつ(もうひとつは引数)で、メッセージセレクターとも呼ばれるものです。こう書くとなんか難しそうですが、通常の言語でいうところのたんなる「メソッド名」のことです。実体はシンボル(同一性が保証されたイミュータブルな文字列オブジェクト。リテラル式は、トークンまたは文字列リテラルの前に # を付す→ #abc )で表現されます。
MoneyTest の全テストを走らせると、あいかわらず赤のままのテストはありますが、testMultiplication は無事イエローに変わります。
仮実装としては、こんなふうにすれば testMultiplication は通るはずです。
Number >> adaptToMoney: aMoney andSend: selector
^ (aMoney amount * 2) perform: aMoney currency
* 2
の部分を self を伴った selector のコールの式に書き換えて重複を排除します。
Number >> adaptToMoney: aMoney andSend: selector
selector == #* ifTrue: [ ^ (aMoney amount * self) perform: aMoney currency ].
self error: 'unexpected operation'
赤のまま残っている testExchangeMoney と testExchangeMoneyDifferentCurrency はなぜエラーなのでしょうか? ○印 をクリックして確認します。
どうやらこちらは 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'
デバッガーを閉じて全テストを走らせるとレッドは解消しているはずです。
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 の全テストがレッドになります。
この機に MoneyTest の testExchangeMoneyDifferentCurrency と testSimpleAddtion は MoneyBagTest に移動してしまいましょう。
MoneyTest>>testSimpleAddtion 改め、MoneyBagTest>>testSimpleAddtion のみを走らせると Money に adaptToMoney:andSend: が無いことがエラーの原因だと分かるので Create します。プロトコルは adapting です。
テストを失敗させるには aMoney をそのまま返せばよいでしょう。
Money >> adaptToMoney: aMoney andSend: selector
^ aMoney
デバッガーを閉じて MoneyBagTest の全テストを走らせます。
通ってしまっているテストもありますが、これはチェックにも 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 に置き換えるだけても大丈夫でしょう。
テストは全部通る…かと思いきや、
testMoneyPlusMoneyBag がレッドのままです。このテストのみ走らせると、MoneyBag にも adaptToMoney:andSend: が必要だとわかります。Create しましょう。
MoneyBag >> adaptToMoney: aMoney andSend: selector
selector == #+ ifTrue: [ ^ self + aMoney ].
self error: 'unexpected operation'
これで全テストが通ります。
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: #/
テストはもちろん真っ赤です。
簡単そうな testDividedByNum からいきます。
これは予防的に設置した 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'
次は testDivideDifferentCurrencyReturnsMoneyXRate です。
こちらは 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'
なんか 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'
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
これで全テストが通ります。
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
ノーティファイアーに従い、Money に adaptToNumber:andSend: を追加します。プロトコルは adapting です。
Money >> adaptToNumber: aNumber andSend: selector
仮実装には 10 USD を返させればよいでしょう。
Money >> adaptToNumber: aNumber andSend: selector
^ 10 USD
重複の排除は次のようにします。
Money >> adaptToNumber: aNumber andSend: selector
selector == #* ifTrue: [ ^ aNumber * amount perform: currency ].
self error: 'unexpected operation'
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
仮実装等のくだりは省略します。
MoneyBag >> adaptToNumber: aNumber andSend: selector
selector == #* ifTrue: [ ^ self class withAll: aNumber * self elements ].
self error: 'unexpected operation'
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! !