LoginSignup
2
1

More than 5 years have passed since last update.

テスト駆動開発でお試しする Pharo Smalltalk・第5回 Money の具象クラス化と Dollar、Franc の廃止

Last updated at Posted at 2017-12-10

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

今回は、さらにその作業を進め、Dollar と Franc の役割を Money に持たせ、不要になった Dollar と Franc は消してしまいます。


参考: 多国通貨実装のおおまかな流れ
  1. Dollar を定義
  2. Dollar をコピーして Franc を定義
  3. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ ← 今ここ
  4. Sum(Expression)、Bank を定義して reduce を実現

Dollar と Franc の重複を排除する

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • = と hash を Money にプルアップ(同一コードなのでそのまま)
  • * と printOn: を Money にプルアップ(コードを一致させてから)

= と hash のプルアップ(コマンドは Push up)は、前回のアクセッサー(amount 、amount: )のときと同様です。右クリック → Refactoring → Push up → Do you want to remove duplicate subclass methods? に Yes → リファクタリング作業の確認で問題なければ Ok 。

873.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • = と hash を Money にプルアップ(同一コードなのでそのまま)
  • * と printOn: を Money にプルアップ(コードを一致させてから)

* と printOn: の方は、Dollar には USD、Franc には CHF がメッセージ送信であったり、文字列であったりでそれぞれ使われているので、これをなんとかして排除しないと一緒にプルアップできません。

通貨種(curreny)の導入で、USD も CHF も currency というメッセージで問い合わせが可能ですので、これを使って * と printOn: を Dollar でも Franc でも同じコードになるように書き換えましょう。

Dollar >> * multiplier
    ^ (amount * multiplier) perform: self currency
Franc >> * multiplier
    ^ (amount * multiplier) perform: self currency
Dollar >> printOn: aStream
    aStream print: amount; space; nextPutAll: self currency
Franc >> printOn: aStream
    aStream print: amount; space; nextPutAll: self currency

書き換えたら、念のためテストが通るか確認し、

877.png

あとはこれまで通り、右クリック → Refactoring → Push up → Do you want to remove duplicate subclass methods? に Yes → リファクタリング作業の確認で問題なければ Ok です。

878.png

879.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • = と hash を Money にプルアップ(同一コードなのでそのまま)
  • * と printOn: を Money にプルアップ(コードを一致させてから)

Dollar、Franc には currency しか残っていないので、それぞれのプロトコルリスト枠で 右クリック → Remove empty protocols して不要なプロトコルを削除しておきます。

880.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す

Dollar と Franc はもはや、currency で #USD を返すか、#CHF を返すかの違いしかなくなりました。違いが返す値だけで、ロジックでないのであれば、もはやクラスである必要はなくなります。

まず、5 USD や 5 CHF が Dollar や Franc ではなく Money を返すようにしましょう。最初の仕事はテスト(testFiveUSDReturnsADollar 、testFiveCHFReturnsAFranc )の書き換えです。

とりあえず、メソッド名を相応しいものに変えたいのですが、Smalltalk ではメソッド名を変えてコンパイルすると元のメソッドは手つかずのまま、編集後の名前での新しいメソッドの生成(複製)になってしまうので要注意です。古い名前のメソッドの削除(右クリック → Remove... )を忘れずに行なう必要があります。

ここでは、リファクタリング・ブラウザの機能を使ってメソッドの名前の変更をします。クラスリスト枠で 右クリック → Rename method (all) を選択。

882.png

メソッド名の新しい名前を尋ねられたら、testFiveUSDReturnsADollar なら testFiveUSDReturnsAMoney、testFiveCHFReturnsAFranc なら testFiveCHFReturnsAMoney と書き換え OK します。

883.png

いつもどおりリファクタリングの作業内容の確認を促されるので、問題なければ Ok 。

884.png

前述の操作(新しい名前でメソッドを複製し、古い名前の元のメソッドを削除)をしてくれているだけだとわかります。

テストの内容も忘れずに変更しておきましょう。

MoneyTest >> testFiveUSDReturnsAMoney
    self assert: 5 USD class equals: Money
MoneyTest >> testFiveCHFReturnsAFranc
    self assert: 5 CHF class equals: Money

テストが失敗することも確認します。

885.png

このテストを通すには、いろいろとかたづけないといけないことがありそうです。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す
  • 抽象メソッドの Money>>currency を具象化(その道すがらインスタンス変数 currency 追加)

Money を具象クラスとして利用するなら、抽象メソッドを排除する必要があります。前回 subclassResponsibility を使って作った Money>>currency がそれに当たります。

通貨種情報は、インスタンスごとに適切な値を返なければならないので、同名のインスタンス変数を追加し、それを返すメソッドにします。

Money >> currency
    ^ currency

886.png

currency という変数が無いので、テンポラリ変数に(とりあえず)するか、インスタンス変数を新たに用意するかを尋ねられるので後者を選択します。

887.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す
  • 抽象メソッドの Money>>currency を具象化(その道すがらインスタンス変数 currency 追加)

何か色々と忘れているような気がしますが(ぉぃぉぃ…)、少なくとも Money から抽象メソッドは排除できたので、Number>>USD、CHF を書き換えます。

Number >> USD
    ^ Money new amount: self; yourself
Number >> CHF
    ^ Money new amount: self; yourself

テストを走らせましょう。

911.png

赤々とエラー(レッド)が点ります。

MoneyTest のメソッドリスト枠で詳細を確認すると、testFiveUSDReturnsAMoney と testFiveCHFReturnsAMoney は成功しましたが、入れ替わりで testtestCurrency と testEquality は失敗(イエロー)、testMultiplication と testPrintString はエラー(レッド)だと分かります。

testEquality を除き他の失敗やエラーは全て同根で、Money>>currency が nil を返していることが原因です。インスタンス生成時に必要な情報を与えていないのですから当然ですね。

currency のセッターを定義してもよいのですが、amount と同じくこれらを外部から書き換えることは好ましくありません。そこで、インスタンス生成時のみに使用する private な setAmount:currency: メソッドを定義します。

“private な”などと言っても、Smalltalk の言語仕様的にはメソッドは全ていわゆる“public”と決まっているので、他言語で言うところの private にできるわけではありません。

では何かというと、private プロトコルに分類してユーザー(プログラマ)に対して、メソッド外からのの仕様を推奨しない旨を伝えるということを指します。

参考:コンストラクターパラメーターメソッド・パターン(ベパp27) 初期化に必要なインスタンス変数値を一度に設定できるメソッドを作りましょう。setで始めて設定したい変数名が分かる名前にしましょう。private プロトコルに置きましょう。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す
  • 抽象メソッドの Money>>currency を具象化(その道すがらインスタンス変数 currency 追加)
  • private(?) な setAmount:currency: を作成(amount のセッター amount: は削除)

まず、プロトコルリスト枠の右クリック → Add protocol... を選び、

889.png

「private」をタイプして入力して OK し、private プロトコルを追加します。

890.png

private プロトコルが追加され、下にメソッド定義のテンプレートテキストが表示されたら次のコードに置き換えてコンパイルします。

Money >> setAmount: aNumber currency: aSymbol
    amount := aNumber.
    currency := aSymbol

891.png

なお、このように初期化時のみに使用する private なセッターには、Smalltalk では通常の public なセッターには使わない set をあえて頭に付ける慣習があります。

改めて Number>>USD、CHF をこの setAmount:currency: を使って書き換えます。

Number >> USD
    ^ Money new setAmount: self currency: #USD; yourself
Number >> CHF
    ^ Money new setAmount: self currency: #CHF; yourself

892.png

テストを走らせるとエラーは消え、testEquality 以外はすべて成功します。

912.png

testEquality の ○印 をクリックして走らせると、ノーティファイアが現れます。

913.png

Debug ボタンを押してデバッガーを起動すると 5 USD ~= 5 CHF が失敗していることがわかります。

TestFailure; Assertion failed.png

Money>> = を確認すると、プルアップしたにもかかわらず相変わらずクラスを比較しているので、currency の比較に変えます。

Money >> = other
    ^ other currency == currency and: [ other amount = amount ]

914.png

これでテストは通ります。

ただ、インスタンス変数 currency へのアクセスが直接アクセスと Money>>currency を介した間接アクセスが混在してしまっているのがよくありません。

参考:同じ変数については 直接アクセス・パターン (ベパp99, 実パp58)か 間接アクセス・パターン(ベパp101, 実パp59)のどちらかひとつに決めましょう(混在させない)。継承して拡張するつもりがないなら間接アクセス・パターンはコードを読みにくくするだけなのでやめましょう。

Money>>currency を使っているメソッド(senders)を調べて直接アクセスに変えてしまいましょう。

Money>>currency を右クリック → Senders of ... を選択すると、現在 Smalltalk 環境内で currency をコールしているメソッドは 5 つあることが分かります。

Senders of currency [5].png

それらのうち、Money クラスのメソッドで、self currency とコールしているメソッド(Money>> * と printOn:)を見つけたら、self を削除し currency のみに変えてコンパイル(Accept)します。

916.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す
  • 抽象メソッドの Money>>currency を具象化(その道すがらインスタンス変数 currency 追加)
  • private(?) な setAmount:currency: を作成(amount のセッター amount: は削除)

ちょっとした整理

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す
  • 抽象メソッドの Money>>currency を具象化(その道すがらインスタンス変数 currency 追加)
  • private(?) な setAmount:currency: を作成
  • 不要になった amount: を削除
  • 不要になった Dollar と Franc を削除
  • インスタンス生成クラスメソッド amount:currency: で記述の簡素

セッターの Money>>amount: はもはや不要なので(そして存在することが好ましくないので)このタイミングで削除しておきましょう。

894.png

ここで Remove it で、たんに削除するのではなく、Remove, then browse senders を選んでおくと、amount: の senders を列挙してくれるので、Money 関係に影響がないことを同時に確認できます。

895.png

Dollar と Franc ももはや不要ですので同様に削除します。

Dollar を右クリック → Remove... すると、

900.png

リファクタリング作業の確認があるので Ok します。

901.png

Franc も同様にします。

902.png

903.png

Money のインスタンス生成時の、

Money new setAmount: 5 currency: #USD; yourself

という記述は長くて、そもそも private プロトコルの setAmount:currency: を外ではあまり使いたくないので、これをクラスメソッドの amount:currency: にまとめてしまいましょう。

Money class >> amount: aNumber currency: aSymbol
    ^ self new setAmount: aNumber currency: aSymbol; yourself

Java などの static メソッド(クラスメソッドとも呼ぶ)と違い、Smalltalk のクラスメソッドは、インスタンスとしてのクラスのインスタンスメソッドで、クラスのクラスであるメタクラスに定義します。

メタクラスは一部の例外を除き原則として無名なので、便宜的に「クラス名 class」と呼称します。たとえば、Money のメタクラスなら Money class です。ちなみにこれはメタクラスを参照するための Smalltalk 式も兼ねています(Money class を評価するとメタクラス自体が返り値として得られます)。

クラスブラウザでメタクラスの定義やそこに定義されたメソッドとそのプロトコルを見るには、クラスリスト枠の下にある「Class」ボタンをクリックして“クラスサイド”に切り替えます。

906.png

切り替わったかどうかがわかりにくいのですが、どうやら Class の前にある (C) のマークが出ているときが“インスタンスサイド”(通常のクラスのブラウズ)で (C) のマークが消えたときが“クラスサイド”(メタクラスのブラウズ)のようです。

クラスにメソッドを定義するのと同様に、プロトコルリスト枠の no message(既存メソッドがある場合は -- all --)をクリックしてメソッド定義テンプレートを下のコード枠に呼び出した状態でコードを入力しコンパイル(Accept)すればクラスメソッドが定義できます。

908.png

プロトコルが as yet unclassified になっていますが、自動振り分けは期待できないので、右クリック → Rename... で「instance creation」に変えておきます。

904.png

905.png

最後に再びクラスリスト枠の Class をクリックして、メタクラスではなく通常のクラスをブラウズできる“インスタンスサイド”に戻し、Number>>USD、CHF をもう一度、書き換えましょう。

Number >> USD
    ^ Money amount: self currency: #USD
Number >> CHF
    ^ Money amount: self currency: #CHF

909.png

テストは問題なく通ります。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD も 5 CHF も(Dollar や Franc ではなく)Money を返す
  • 抽象メソッドの Money>>currency を具象化(その道すがらインスタンス変数 currency 追加)
  • private(?) な setAmount:currency: を作成
  • 不要になった amount: を削除
  • 不要になった Dollar と Franc を削除
  • インスタンス生成クラスメソッド amount:currency: で記述の簡素化

ここまでのまとめと補足

  • インスタンス生成時に与えなければならない初期値は、set で始まるコンストラクターパラメーターメソッド(ベパp27)を作って、private にしておく
  • Smalltalk で言う“private”は、プロトコルを private にして注意を喚起するだけ( プラグマとか、メソッド名を pvt から始めるとかコンパイル時にチェックさせる試みもある)
  • インスタンスがコールするメソッドをクラスに定義するように、クラスメソッド(クラスがコールするメソッド)はメタクラス(クラスのクラス)に定義
  • メタクラスは原則無名なので「クラス名 class」と呼称(参照も同じ)
  • メタクラスは原則無名なので、クラスブラウザにはクラスを編集するインスタンスサイドと、メタクラスを編集するクラスサイドがある

この時点のコード

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

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

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

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

!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 subclass: #Money
    instanceVariableNames: 'amount currency'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'TDD-Money'!


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


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


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


!Money methodsFor: 'arithmetic' stamp: 'sumim 12/7/2017 18:07'!
* multiplier
    ^ (amount * multiplier) perform: currency! !


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


!Money methodsFor: 'comparing' stamp: 'sumim 12/7/2017 17:22'!
hash
    ^ 0! !


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


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


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


!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/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/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/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'! !
2
1
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
2
1