Uzabase Tech Blog

SPEEDA, NewsPicks, FORCASなどを開発するユーザベースの技術チームブログです。

Smalltalkで『テスト駆動開発』の「第I部 多国通貨」をハンズオンしたら快適で楽しかった

今日は。 SPEEDAの開発をやっている濱口です。

SPEEDA開発チームではテスト駆動開発(TDD)、ペアプログラミングを徹底しています。
だからなのか、『テスト駆動開発』はすごく楽しく読めました。
今回ハンズオンを行った「第I部 多国通貨」でも、ペアプロをしながら著者が語りかけてくるような感じで、 読者側も、著者の意図をひとつずつ理解しながら読み進めていけるようになっています。

有意義なハンズオンを、より有意義にしたい

ただ、そうは言っても、 読むだけよりも手を動かしたほうがよいと思いますし、 さらに、書籍で使用されているJava言語で書き写すだけよりも他のプログラミング言語でコーディングを行うことで、 ハンズオンがよりアクティブ・ラーニング化されると考えました。
プログラミング言語の翻訳プロセスを挟むことで、より能動的なハンズオンになるはずです。

古くてあたらしい言語(環境)、Smalltalkにふれたい

今回、私はプログラミング言語にSmalltalkを選択しました。
まったく違うパラダイムの言語でやってみても面白いかもしれませんが、 今回はより純粋なTDD、およびオブジェクト指向プログラミング(OOP)体験を求めてそうしました。
著者も最初はSmalltalkでTDDを行っていたはずですし、 Smalltalkは開発環境も内包していることから、IDEの源流みたいなものにも触れられると考えたからです。

環境はPharoを使いました。(イメージのテンプレートはPharo 7.0)
IcebergというGitリポジトリと連携するツールも付属しますので このハンズオンを行い、ソースコードを管理する限りでは、Pharo単体で完結しました。

f:id:yhamaro:20191209054113p:plain
Pharoの開発環境。ここですべてが完結します。

Smalltalkの文法は非常にシンプルなので、『Pharo by Example』の「Part I Getting Started」にひと通り目を通せば書き始められました。
ただ、Pharo自身を含むすべてのソースコードと自分の書いたコードは全く同じ扱いになるため、「自分の設計・実装が、そのまま既存のクラスライブラリの拡張になる」という意識づけが自然になされます。
なので、既存のクラスライブラリに分け入って、自分の設計を環境になじませる必要性を常に感じることになります。
開発環境の詳細な解説は上述のドキュメントに譲りますが、System Browser、Finderなどの各種ツールが上記のような探索的開発をサポートしてくれます。
今回は、メッセージの命名やプロトコルの整理などの作法を中心に既存のコードを参考にしました。

f:id:yhamaro:20191209054645p:plain
Let's 写経!

Smalltalkでは、わりと忠実な写経が可能だった

一通りハンズオンをこなしてみて、思ったより忠実な写経が出来上がった印象です。
とはいえ、そもそも言語が異なるのでやはり違いはありました。
途中、特に気になった差異をいくつか挙げます。

Javaのコンストラクタのような特別なメソッドが無い

例えば、5ドルを現すオブジェクトのインスタンスが欲しい場合、以下のようにします。

Money dollar: 5

これを実現(コンストラクト)するコードは以下になります。

{ #category : #'instance creation' }
Money class >> dollar: anInteger [
    ^ Money amount: anInteger currency: 'USD'
]

{ #category : #'instance creation' }
Money class >> amount: anInteger currency: aString [
    ^ self new setAmount: anInteger currency: aString
]

{ #category : #'initialization - private' }
Money >> setAmount: anObject currency: aString [
    amount := anObject.
    currency := aString.
    ^ self
]

『Smalltalk Best Practice Patterns』(『ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集』)を参考にしましたが、
一番上のクラスサイドのメソッド(Shortcut Constructor Methodパターン)を入り口として、
汎用的なクラスメソッドの中でインスタンスを生成(new)し、必要なインスタンス変数初期化のためのメッセージを呼び出します(Constructor Parameter Methodパターン)。
インスタンスサイドに初期化時のみに呼び出すSetting Methodを作り、それを正しいプロトコルinitializetion -privateにカテゴライズするところが肝だと考えています。

インスタンス変数のスコープと意図を伝えるためのプロトコル

上記でも登場したMoneyクラスの定義は以下になります。

Class {
    #name : #Money,
    #superclass : #Object,
    #instVars : [
        'amount',
        'currency'
    ],
    #category : #'Example-TDD'
}

Smalltalkではインスタンス変数のスコープはすべてJavaでいうprivate、またはprotectedになります。
なので、インスタンス変数にアクセスするにはGetting/Setting Methodを介すことになります。

{ #category : #private }
Money >> amount [
    ^ amount
]

ただ、そのメソッド自体はアクセス制限されません(publicになります)。
privateプロトコルにカテゴライズすることでこのクラス以外から使用されない意図を伝えます。
「第4章 意図を語るテスト」でamountインスタンス変数をprivateにするタスクを行う際、 上記のような言語仕様の違いはありますが、Smalltalkでもテストコードでamountセレクタを使用しなくなるので、
「テスト対象オブジェクトの新しい機能を使い、テストコードとプロダクトコードの間の結合度を下げた。」という目的に適うタスクになります。

型が無い(untyped)

一番の違いはこれかと思います。
Smalltalkでは型が無く、かつ処理を全てメッセージ送信で表現するため、メッセージのシグニチャ(もちろん、それに型は含まれない)だけで多態を実現します。
なので、Javaのインタフェースのようなものが無い、というか必要無いです。
したがって、「第12章 設計とメタファー」から登場するExpressionインタフェースは必要無くなり、
「第13章 実装を導くテスト」で「Expression に reduce(String) メソッドの定義を引き上げる」というタスクを行いますが、 以下のようにSumとMoneyが等しくreduceTo:メッセージに応答するようにするだけです。

{ #category : #enumerating }
Money >> reduceTo: aCurrency at: aBank [
    | rate |
    rate := aBank rateFrom: self currency to: aCurrency.
    ^ self class amount: self amount / rate currency: aCurrency
]

{ #category : #enumerating }
Sum >> reduceTo: aCurrency at: aBank [
    | sum |
    sum := (augend reduceTo: aCurrency at: aBank) amount
        + (addend reduceTo: aCurrency at: aBank) amount.
    ^ Money amount: sum currency: aCurrency
]

上記の違いはありましたが、このハンズオンが意図する本筋から外れるものはなく、1章ずつステップバイステップで進めていけました。 (章ごとに出来上がったコードのスナップショットはコミット単位で分けています

個人的にはSmalltalkのシンプルが気に入ったので、
この本をさらに読み進めるのと、今まで読んだOOP関連の本(『実装パターン』など)をSmalltalk視点でもう一度読み直すなどし、
SmalltalkでOOPやTDDをしばらく学んでみたいと考えています。