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

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

- [テスト駆動開発でお試しする Pharo Smalltalk・目次](https://qiita.com/sumim/items/3a36c03ab41519077b37)

[前回](https://qiita.com/sumim/items/264d7cb7446850e13372) は既存の Dollar から Franc を複製し、Franc として振る舞いを整えました。

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

<br />
#####*参考： 多国通貨実装のおおまかな流れ*
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 の場合）
- [x] 5 USD * 2 = 10 USD
- [x] 5 CHF * 2 = 10 CHF
- [ ] **Money を作る**
- [ ] **Dollar と Franc を Money のサブクラスに変更**
- [ ] **Dollar と Franc の amount とアクセッサーを Money に移動**

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

![843.png](https://qiita-image-store.s3.amazonaws.com/0/49275/b41cc76c-6843-316f-7178-7be169e9f4f4.png)

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

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

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

![844.png](https://qiita-image-store.s3.amazonaws.com/0/49275/7dd6034c-fa96-eeec-29f2-199cda6892f2.png)

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

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

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

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

![845.png](https://qiita-image-store.s3.amazonaws.com/0/49275/3d97c27b-af44-b90d-0c04-dd640557a624.png)

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

![846.png](https://qiita-image-store.s3.amazonaws.com/0/49275/453570f5-a94f-d41f-b9ea-d9ced53fe802.png)

Franc も同様にします。

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

![847.png](https://qiita-image-store.s3.amazonaws.com/0/49275/5575c716-d812-3264-4bae-5615783e2b6b.png)

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

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

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

![848.png](https://qiita-image-store.s3.amazonaws.com/0/49275/0861249c-b321-1d90-c391-843552bd456a.png)

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

![849.png](https://qiita-image-store.s3.amazonaws.com/0/49275/6852550f-69df-6af9-3473-8e64922f538d.png)

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

![850.png](https://qiita-image-store.s3.amazonaws.com/0/49275/b907ddb8-d221-519b-f8f7-d8af5bc4cd5c.png)

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

```Smalltalk
Money subclass: #Dollar
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'
```

```Smalltalk
Money subclass: #Franc
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'TDD-Money'
```

Money に追加されます。

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

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

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

![853.png](https://qiita-image-store.s3.amazonaws.com/0/49275/aefc65d6-ad90-35b0-6776-9f1d97fffb4b.png)


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

![854.png](https://qiita-image-store.s3.amazonaws.com/0/49275/c12966ab-59b1-1906-8e86-903a1529d21a.png)

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

![855.png](https://qiita-image-store.s3.amazonaws.com/0/49275/a413a825-f402-5c5e-8fe9-f010d7fc0c6c.png)

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

```Smalltalk
Money >> amount
	^ amount
```

![856.png](https://qiita-image-store.s3.amazonaws.com/0/49275/5e4b7156-279f-86cc-85dc-49d41d1fc1a5.png)


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

```Smalltalk
Money >> amount: aNumber
	amount := aNumber
```

![857.png](https://qiita-image-store.s3.amazonaws.com/0/49275/aff06d72-023d-a217-162d-b86dcafcc39f.png)


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

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

![852.png](https://qiita-image-store.s3.amazonaws.com/0/49275/2549225d-f8ff-8ae1-2340-d62a196176cd.png)

#currency を導入する

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

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

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

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

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

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

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

![865.png](https://qiita-image-store.s3.amazonaws.com/0/49275/03c0975c-e90f-462c-e9ee-8900f81d72b6.png)

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

![864.png](https://qiita-image-store.s3.amazonaws.com/0/49275/02730a88-b530-f37f-d076-4110be5bc8cb.png)

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

![860.png](https://qiita-image-store.s3.amazonaws.com/0/49275/c2e8ad7e-644e-a309-4d1d-3e2a71ace59e.png)

定義先は Money です。

![861.png](https://qiita-image-store.s3.amazonaws.com/0/49275/107d7b3f-59d6-711b-c003-c0f1465887a2.png)

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

![862.png](https://qiita-image-store.s3.amazonaws.com/0/49275/d3b5537d-b5d6-c479-9b36-e4c4295b2007.png)

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

![863.png](https://qiita-image-store.s3.amazonaws.com/0/49275/453fbcb0-8f0d-182d-5565-68ae5fce5573.png)

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

![866.png](https://qiita-image-store.s3.amazonaws.com/0/49275/616df183-cf26-3cf6-98c7-2e3f9be774f7.png)

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

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

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

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

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

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

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

```Smalltalk
Money >> currency
	self subclassResponsibility 
```

![subclassResponsibility.png](https://qiita-image-store.s3.amazonaws.com/0/49275/38a59978-3fea-2f6d-3959-ebe7c06008d5.png)

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

![Vocabulary of Dollar.png](https://qiita-image-store.s3.amazonaws.com/0/49275/472cfd76-e176-4fd2-50a6-090367d88699.png)

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

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

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

![869.png](https://qiita-image-store.s3.amazonaws.com/0/49275/96de97a4-53d3-9ba6-cd2f-ce06c46ac7db.png)

さて、テストは？

![871.png](https://qiita-image-store.s3.amazonaws.com/0/49275/daeb1eb9-aec6-6ff6-24a4-768cb5847de4.png)

もちろんグリーンです。

###### **TODO リスト**
- [ ] 5 USD + 10 CHF = 10 USD （レートが 2 : 1 の場合）
- [x] 5 USD * 2 = 10 USD
- [x] 5 CHF * 2 = 10 CHF
- [x] Money を作る**
- [x] Dollar と Franc を Money のサブクラスに変更
- [x] Dollar と Franc の amount を Money に移動
- [x] **通貨種（currency）という属性の導入**
- [x] **Money>>currency を self subclassResponsibility に**
- [x] **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 に比べるとかなり落ちます^^;）。

[[前回からの差分はこちら](https://github.com/sumim/tdd-money/commit/7693ba314817182772f8ab676b507d2b1b1c01a1)]

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