5
3

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 5

テスト駆動開発でお試しする Pharo Smalltalk・第2回 等価性チェックと乗算の実装

Last updated at Posted at 2017-12-05

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

前回 は 5 USD というメッセージ式が Dollar のインスタンスを返すところまでを実装しました。

今回はこの Dallar のインスタンスをもう少しまともなものにしてゆきます。


#####参考: 多国通貨実装のおおまかな流れ

  1. Dollar を定義 ← 今ここ
  2. Dollar をコピーして Franc を定義
  3. Dollar、Franc の重複を新しく作ったスーパークラスの Money にプルアップ
  4. Sum(Expression)、Bank を定義して reduce を実現

Dollar の等価性

Dollar にインスタンス変数を追加して状態を持たせ、インスタンス間で等価(および、不等価)をチェックできるようにしましょう。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す
  • 5 USD = 5 USD
  • 5 USD ~= 6 USD

まずは等価性のテストを書きます。

MoneyTest >> testEquality
	self assert: 5 USD = 5 USD

MoneyTest の ○印をクリックしてテストを実行すると今回はレッド(エラー)にはならずイエロー(失敗)になります。

780.png

等価性チェックを行なう = メソッドは(ProtoObject を除く)全オブジェクトのスーパークラスである Object に定義済みで、デフォルトの振る舞いでは同一オブジェクトかどうかを返します。このため、先の testEquality はエラーにはなりませんでした。

Object >> = anObject 
	"Answer whether the receiver and the argument represent the same 
	object. If = is redefined in any subclass, consider also redefining the 
	message hash."

	^self == anObject 

しかし、最初の 5 USD と二番目の 5 USD は個々に生成された別のオブジェクトであるため同一にはならずテストには失敗したわけです。

仮実装(最短のグリーン化)は省いて、ステップを大きく進めましょう。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す
  • 5 USD = 5 USD
  • 5 USD ~= 6 USD
  • インスタンス変数 amount を定義
  • amount のアクセッサーメソッド(amount 、amount: ) を追加

まず、不等価も念のためテストに追加しておきます。

MoneyTest >> testEquality
	self assert: 5 USD = 5 USD.
	self assert: 5 USD ~= 6 USD

ちなみに ~= は不等価チェックです。デフォルト実装は次のコードになっていて、テンプレートメソッド・パターン(デパp347)により、= のみの再定義で機能します。

Object >> ~= anObject 
	"Answer whether the receiver and the argument do not represent the 
	same object."

	^self = anObject == false

インスタンス変数は、クラス定義の instanceVariableNames: '' の引数っぽくなっている文字列に(複数あるときはスペースで区切って)指定すればいいのですが、今回は機能の紹介を兼ねて Pharo のリファクタリング機能を使ってみましょう。

Dollar クラスを右クリック → Refactoring → Inst Var Refactoring →  Add を選びます。

781.png

Provide the following information で追加したいインスタンス名を求められるので、inst を消して amount に変え OK します。

Provide the following information.png

すると(今回は amount の追加だけなので項目はひとつしか出ませんが)リファクタリングに伴う作業の一覧が表示され、本当に実行してよいか確認を促されるのでそのまま OK します。

Changes Browser.png

Dollar を二度クリックして再度選択する必要がありますが、先述の instanceVariableNames: の引数が '' から 'amount' に変わっているのが確認できるはずです。

783.png

Smalltalk のインスタンス変数は Java などのそれとは違い、インスタンスメソッド外から直接アクセスする手段がありません。インスタンス外から値を読み取ったり設定する必要があるときは、通常は、あらかじめアクセッサーメソッドを定義しておき、それらを介する必要があります(アクセッサーメソッドなんかなくとも Smalltalk の強力なリフレクション機能を使って済ませる手もありますが、それはまた別の話)。

Pharo では、インスタンス変数の追加したときと同じ Refactoring メニューにインスタンス変数のアクセッサーを自動生成する機能もあるのでこれを使いましょう。

Dollar クラスを右クリック → Refactoring → Inst Var Refactoring → Accessors を選びます。

787.png

アクセッサーを自動生成したいインスタンス変数を指示するように促されるので amount をクリックして選択します。

788.png

続けて、ゲッター(インスタンス変数と同名のメソッド)、セッター(インスタンス変数に : を付けた引数をひとつとるメソッド)、どちらのメソッドを生成するかを尋ねてくるので、そのまま OK します。

accessing プロトコルが新たに設けられ、そこに amount 、amount: が定義されます。メソッドの中身の確認ついでに、amount: の仮引数が anObject なのがあんまりなので、aNumber とでもしておきましょう。

Dollar >> amount: aNumber
	amount := aNumber

791.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す
  • 5 USD = 5 USD
  • 5 USD ~= 6 USD
  • インスタンス変数 amount を定義
  • amount のアクセッサーメソッド(amount 、amount: ) を追加

準備は整ったので改めて Dollar オブジェクトの等価性チェック Dollar>> = を定義しまます。Dollar クラスのプロトコルリスト枠にある -- all -- をクリックして、下のコードペインに(Dallar クラスの定義式ではなく)メソッド定義用テンプレートが現れたのを確認して次のように = メソッドを定義します。

Dollar >> = other
	^ other class == self class and: [ other amount = amount ]

プロトコルは as yet classified に分類されるので、同枠内で右クリック → Categorize all uncategorized を選んで、comparing に自動割り当てしてもらいます。

793.png

テストは相変わらず失敗するはずですが、レッドになってしまっていないことは確認しておきます。

796.png

最後に、Number>>USD が amount をセットした適切な Dallar を返すように修正します。

Number >> USD
	^ Dollar new amount: self; yourself

797.png

「 ; yourself 」というのは、直前の式の結果を無視してレシーバーを返す「カスケード式」と呼ばれる“おまじない”(シンタックスシュガー)です。次のように書くのと意味は同じです。

Number >> USD
	| aDollar |
	aDollar := Dollar new.
	aDollar amount: self.
	^ aDollar

これでテストは通るはずです。

798.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す
  • 5 USD = 5 USD
  • 5 USD ~= 6 USD
  • インスタンス変数 amount を定義
  • amount のアクセッサーメソッド(amount 、amount: ) を追加

Dollar の乗算

ずいぶん大股歩きをしてしまいましたが、最後にさくっと乗算のサポートをしておしまいにしましょう。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す
  • 5 USD = 5 USD
  • 5 USD ~= 6 USD
  • インスタンス変数 amount を定義
  • amount のアクセッサーメソッド(amount 、amount: ) を追加

テストを書きます。

MoneyTest >> testMultiplication
	self assert: 5 USD * 2 = 10 USD

799.png

コンパイルは通るのですが、下で Pharo が何かサジェスチョン「Use assert:equals: instead of assert: and = 」をしてくれていますのでレンチアイコンをクリックして従ってみましょう。

Apply the proposed changes.png

いちおう、どんなふうに変えるか(赤部分を削って緑部分を追加)を教えてくれるみたいですね。問題なさそうなので OK します。

テストを実行します。

800.png

Dollar>> * は定義されていませんから、もちろんレッドです。

イエローも仮実装もすっとばして一気にグリーンにします。Dollar>> = のときと同じように、Dollar クラスのプロトコルリスト枠にある -- all -- をクリックして次のコードをコンパイルしてメソッドを生成しましょう。

Dollar >> * multiplier
	^ (amount * multiplier) USD

コンパイルが終わったら、プロトコルリスト欄で 右クリック → Categorize all uncategorized もお忘れなく。( * は arithmtic プロトコルに分類されます。)

804.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 USD が Dollar を返す
  • 5 USD = 5 USD
  • 5 USD ~= 6 USD
  • インスタンス変数 amount を定義
  • amount のアクセッサーメソッド(amount 、amount: ) を追加
  • hash の再定義
  • printOn: の再定義

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

  • 等価チェックは = 、同一チェックは ==
  • = のデフォルト動作(未定義時動作)は ==
  • = を再定義したら hash も再定義(忘れていたのでそのうちやる)
  • ; でメッセージを区切って連ねる「カスケード」という記法を使うと、テンポラリ変数を介してメッセージを畳みかけて送る処理と同等の記述ができる
  • 式; yourself で、式のレシーバーを無条件に(式が何を返そうがそれを無視して)返すことができる
  • メソッド名がよく利用されるものなら、Categorize all uncategorized で適切なプロトコルを推測して分類してくれる(でも全部は任せられないので、普段から調べて慣習を理解したり、自分で思いついたりできるようにしよう)
  • 外部からはアクセッサーをコールするメッセージ式を介してしかインスタンス変数にはアクセスできない
  • 引数をとるメソッドの名前はコロンを含み、コロンまで含めてひとつのメソッド名(例:amount: 、assert: 、printOn: 、assert:equals: 等々)でコロンなしの同名メソッドとは別物(例: amount と amount: 等)。さらに言うと同名のインスタンス変数とメソッド(アクセッサーメソッド)も別物
  • インスタンスの文字列表現のデフォルトは 不定冠詞 + クラス名(an Object や a Dollar)なので、状態を持つようになったら printOn: を再定義して適切な表現に変える(忘れていたのでそのうちやる)

#この時点のコード
ファイルアウト形式のソースです。(この形式で 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/3/2017 17:27'!
USD
	^ Dollar new amount: self; yourself! !


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


!Dollar methodsFor: 'accessing' stamp: 'sumim 12/3/2017 17:06'!
amount: aNumber
	amount := aNumber! !


!Dollar methodsFor: 'accessing' stamp: 'sumim 12/3/2017 17:06'!
amount
	^ amount! !


!Dollar methodsFor: 'comparing' stamp: 'sumim 12/3/2017 17:20'!
= other
	^ other class == self class and: [ other amount = amount ]! !


!Dollar methodsFor: 'arithmetic' stamp: 'sumim 12/3/2017 21:40'!
* multiplier
	^ (amount * multiplier) USD! !


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


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/3/2017 11:18'!
testFiveUSDReturnsADollar
    self assert: (5 USD isKindOf: Dollar)! !


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


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/3/2017 17:45'!
testMultiplication
	self assert: 5 USD * 2 equals: 10 USD! !
5
3
2

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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?