5
1

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 9

テスト駆動開発でお試しする Pharo Smalltalk・第4回 スーパークラス Money を作る

Last updated at Posted at 2017-12-09

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

前回 は既存の Dollar から Franc を複製し、Franc として振る舞いを整えました。

今回は Dollar と Franc に重複した機能を排除するために、両クラスに共通のスーパークラスである Money を作ります。


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

#Money を作って Dollar と Franc のスーパークラスにする

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount とアクセッサーを Money に移動

クラス枠内で右クリック → Add Class... を選択。

843.png

続けて提示されるクラス定義テンプレートを編集後 OK し、Money クラスを作成します。

Object subclass: #Money
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'

Add Class... を使わなくても、クラス枠内の余白をクリックするなどして下のコード枠にクラス定義テンプレートを呼び出してそれを編集 → Accept しても同じです。

844.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount とアクセッサーを Money に移動

無事 Money を作ることができたら、Dollar、Franc のスーパークラスを Object から Money にすげ替えます。

まず、Dollar をクリックして選択しクラス定義式を呼び出しましょう。最初の Object を Money に書き換えて Accept するだけ作業は終わりです。

Money subclass: #Dollar
	instanceVariableNames: 'amount'
	classVariableNames: ''
	package: 'TDD-Money'

845.png

Accept と同時に、クラスリスト枠の Money 以下に Dollar が移動するのが分かります。

846.png

Franc も同様にします。

Money subclass: #Franc
	instanceVariableNames: 'amount'
	classVariableNames: ''
	package: 'TDD-Money'

847.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount とアクセッサーを Money に移動

続いて、Dollar と Franc に重複して存在するインスタンス変数 amount を Money に移動(プルアップ)します。

Dollar もしくは Franc を右クリック → Refactoring → Inst Var Refactoring → Pull Up を選択すると

848.png

プルアップするインスタンス変数を尋ねられるので(と言っても一つしかありませんが)amount をクリックして選択します。

849.png

リファクタリング作業の確認を促されるので問題なければ OK をクリック。

850.png

Dollar、Franc の instanceVariableNames: の引数から amount が消え、

Money subclass: #Dollar
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'
Money subclass: #Franc
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'

Money に追加されます。

Object subclass: #Money
	instanceVariableNames: 'amount'
	classVariableNames: ''
	package: 'TDD-Money'

アクセッサー(amount 、amount: )のプルアップ(コマンドは Push up)もほぼ同様の操作です。

まず、Dollar か Franc の accessing → amount を右クリック → Refactoring → Push Up を選択します。

853.png

同じ Money のサブクラス(もし Dollar で操作しているなら Franc)にある重複ゲッターも同時に削除するか尋ねられるので Yes とします。

854.png

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

855.png

これで Dollar>>amount 、Franc>>amount が削除され、同内容で Money>>amount が作成されます。

Money >> amount
	^ amount

856.png

コロン付き(引数付き)のメソッドでセッターの amount: についても同様の操作を行ないましょう。

Money >> amount: aNumber
	amount := aNumber

857.png

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount とアクセッサーを Money に移動

テストは相変わらず通るはずです。

852.png

#currency を導入する

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る**
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount を Money に移動
  • 通貨種(currency)という属性の導入

そろそろテストを書かないと、定規で打ち据えられ続けた手が腫れ上がりそうです。

通貨の概念を導入します。

testCurrency
	self assert: 5 USD currency equals: #USD.
	self assert: 5 CHF currency equals: #CHF

ここで #USD と #CHF は、シンボルのリテラル式です。シンボルは Smalltalk では一意性が保証された(必然的にイミュータブルな)文字列として振る舞います。

通常はコンパイラがトークンとして解釈する文字列を使うことが多いので先頭に # を付けるだけですが、#'a b c' のように、文字列リテラルの前に # を付ける記法を用いることで、スペースなどのセパレーターや記号を含めたシンボルを生成(生成済みであれば参照)することも可能です。

もちろんテストの結果はレッド(エラー)です。

865.png

Money>>currency が未定義なのが原因なのは明らかなので、testCurrency の ○印 でノーティファイアーを呼び出して、

864.png

さっさと Create してしまいましょう。

860.png

定義先は Money です。

861.png

プロトコルは accessing がよいでしょう。

862.png

デバッガーが起動し、currency のコンテキストが表示されたらメソッドの中身(メソッド名の currency 以外)を空にしてコンパイル (Accept)し、デバッガーは閉じてしまいます。

863.png

これでテストは無事、失敗します。

866.png

さて、このイエローをできるだけ手短にグリーンにするにはどうしたらよいでしょうか。ここではこんなふうにしてみます。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る**
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount を Money に移動
  • 通貨種(currency)という属性の導入
  • Money>>currency を self subclassResponsibility に
  • Money のサブクラスに currency を実装

Smalltalk と言えば、アラン・ケイの「メッセージングのオブジェクト指向」をサポートする言語の筆頭として有名ですが、実は彼の手を離れた Smalltalk-80 以降の、つまり現在“Smalltalk”と呼ばれている言語は、もう一つの「抽象データ型のオブジェクト指向」についても限定的ながらサポートする機構を含んでいます。

その象徴的なもののひとつが、ここで使う subclassResponsibility という例外を挙げるメソッドです。

もちろん遅延結合性が“いのち”の Smalltalk ですから、静的型チェック機構を備えた抽象データ型のオブジェクト指向を主にサポートする言語のようにコンパイル時の型チェックのエラーなどというものを出すようなことはしません(できません…が、静的型チェック機構の試みはあります→「Strongtalk - オプショナルな静的型チェック機能を有したSmalltalk」)。

しかし、抽象クラスを含むスーパークラスに、この subclassResponsibility をコールする内容の抽象メソッドを定義することで、インターフェイスとその実装を意識したプログラミングをほんの少しだけサポートする気概は見せています。

ここでは、Money>>currency の内容を次のように書き換えておくことで、サブクラス(Dollar や Franc)での currency の実装の必要性をコードから、あるいは実行時のエラーによってユーザー(プログラマー)に伝えることができます。

Money >> currency
	self subclassResponsibility 

subclassResponsibility.png

もし処理系が、プロトコルブラウザと呼ばれる特殊なクラスブラウザを備えていれば、うっかり再定義するのを忘れたメソッドであることに気付くのにも役立ちます。(残念ながら Pharo にはこの機能はありません。図は Squeak のもの)。

Vocabulary of Dollar.png

ということで subclassResponsibility エラーで注意されないよう、Dollar>>currency、Franc>>currency を忘れずに実装しましょう。

Dollar >> currency
	^ #USD
Franc >> currency
	^ #CHF

currency メソッドのプロトコルは、すでに Money で accessing を指定してあるので、いつも通り Categorize all uncategorized で自動的に as yet unclassified を accessing に変更可能なはずです。

869.png

さて、テストは?

871.png

もちろんグリーンです。

TODO リスト
  • 5 USD + 10 CHF = 10 USD (レートが 2 : 1 の場合)
  • 5 USD * 2 = 10 USD
  • 5 CHF * 2 = 10 CHF
  • Money を作る**
  • Dollar と Franc を Money のサブクラスに変更
  • Dollar と Franc の amount を Money に移動
  • 通貨種(currency)という属性の導入
  • Money>>currency を self subclassResponsibility に
  • Money のサブクラスに currency を実装

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

  • インスタンス変数やメソッドのプルアップは、リファクタリング・ブラウザの機能を使うと手間が省ける
  • シンボルは一意性を保証された文字列
  • subclassResponsibility は抽象データ型のオブジェクト指向を限定的ながらもサポートするためのささやかな機能

#この時点のコード
ファイルアウト形式のソースです。(この形式で 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! !


!Number methodsFor: '*TDD-Money' stamp: 'sumim 12/5/2017 12:52'!
CHF
	^ Franc new amount: self; yourself! !


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


!Money methodsFor: 'accessing' stamp: 'sumim 12/5/2017 17:54'!
currency
	self subclassResponsibility

"5 USD currency"! !


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


!Money methodsFor: 'accessing' stamp: 'sumim 12/5/2017 17:34'!
amount: aNumber
	amount := aNumber! !


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


!Dollar methodsFor: 'accessing' stamp: 'sumim 12/5/2017 18:21'!
currency
	^ #USD! !


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


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


!Dollar methodsFor: 'comparing' stamp: 'sumim 12/5/2017 12:42'!
hash
	^ 0! !


!Dollar methodsFor: 'printing' stamp: 'sumim 12/5/2017 12:44'!
printOn: aStream
	aStream print: amount; space; nextPutAll: 'USD'

"5 USD"! !


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


!Franc methodsFor: 'accessing' stamp: 'sumim 12/5/2017 18:26'!
currency
	^ #CHF! !


!Franc methodsFor: 'arithmetic' stamp: 'sumim 12/5/2017 12:57'!
* multiplier
	^ (amount * multiplier) CHF! !


!Franc methodsFor: 'comparing' stamp: 'sumim 12/5/2017 12:48'!
= other
	^ other class == self class and: [ other amount = amount ]! !


!Franc methodsFor: 'comparing' stamp: 'sumim 12/5/2017 12:48'!
hash
	^ 0! !


!Franc methodsFor: 'printing' stamp: 'sumim 12/5/2017 12:54'!
printOn: aStream
	aStream print: amount; space; nextPutAll: 'CHF'

"5 CHF"! !


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


!MoneyTest methodsFor: 'tests' stamp: 'sumim 12/5/2017 17:21'!
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/5/2017 12:49'!
testFiveCHFReturnsAFranc
	self assert: (5 CHF isKindOf: Franc)! !


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


!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/5/2017 12:52'!
testPrintString
	self assert: 5 USD printString equals: '5 USD'.
	self assert: 5 CHF printString equals: '5 CHF'! !
5
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?