http://www.object-arts.com/content/navigation/products/dce.html
と、言うわけで、Model-View-Presenter で評判の Dolphin Smalltalk で遊んでみる。と言っても始めて触るのでさっぱりどこから手をつけていいやら。。。
メモ
- 最初のウインドウを System Folder と呼ぶ
- アプリケーションは Package Browser というので管理する。
- メソッドは複数のカテゴリに登録できる。
- 画面は View Composer で設計する。
- 設計した画面は Presenter のサブクラスのクラスメソッド resource_Default_view に格納される。
準備
自分の作ったプログラムが他と混ざらないように Package を作ります。Package は Squeak の Change Set のような物です。System Folder から Package Browser を開けて、適当にパッケージを作り、右クリックで Set as Default Package とすると次から更新が新しいパッケージに記録されます。
華氏摂氏変換器の設計
例によって華氏摂氏変換器を作って Model-View-Presenter がどういう物なのか調べてみる。多少冗長だけど、設計は次のとおり。
- モデル FCConverter はインスタンス変数 celsius を持つ。
- アクセッサ celsius と celsius: は摂氏の温度を書いたり読んだりする。
- アクセッサ fahrenheit と fahrenheit: 変換して華氏の温度を書いたり読んだりする。
- プレゼンター FCConverterPresenter は celsiusPresenter と fahrenheitPresenter を持つ。
- それぞれ摂氏と華氏の温度を表示する。
- 摂氏を入力したら華氏が、華氏を入力したら摂氏がそれぞれ更新される。
華氏摂氏変換器 Model の実装
クラス定義はこんな感じ
Model subclass: #FCConverter instanceVariableNames: 'celsius' classVariableNames: '' poolDictionaries: '' classInstanceVariableNames: ''
celsius のアクセッサー trigger: というのは、値が変わったことを周りに教える役目があります。
celsius ^ celsius celsius: aNumber celsius := aNumber. self trigger: #celsiusChanged.
fahrenheit のアクセッサー
fahrenheit ^ self celsius * 1.8 + 32 fahrenheit: aNumber self celsius: aNumber - 32 / 1.8
初期化子
initialize
celsius := 0
テストをクラスメソッドとして書いておく。
runTest "self runTest" | c | c := FCConverter new. c fahrenheit: 86. self assert: [c celsius = 30]. c := FCConverter new. c celsius:40. self assert: [c fahrenheit = 104]
華氏摂氏変換器 Presenter の実装
Presenter とは耳慣れない言葉ですが、View と Model の仲立ちをします。
クラス定義
Shell subclass: #FCConverterPresenter instanceVariableNames: 'celsiusPresenter fahrenheitPresenter' classVariableNames: '' poolDictionaries: '' classInstanceVariableNames: ''
初期化子には createComponents というメソッドを使い sub-presenters を生成します。name: で指定する名前は、あとで View を作る時に対応していないといけません。sub-presenters というのは、摂氏、華氏それぞれの表示を管理する子オブジェクトです。
createComponents super createComponents. celsiusPresenter := self add: NumberPresenter new name: 'celsius'. fahrenheitPresenter := self add: NumberPresenter new name: 'fahrenheit'.
model: でモデルを決定する時それぞれに sub-model を割り当てます。sub-model は aspectValue: で作られる ValueAspectAdaptor オブジェクトで、典型的には親モデルのそれぞれの状態を監視する物です。今回モデルのインスタンス変数は celsius fahrenheit という二つのアクセッサを持つので、それぞれに ValueAspectAdaptor を作成します。
それから aspectTriggers: で、sub-presenters がモデルの更新通知を受け取るようにします。
model: aFCConverter super model: aFCConverter. celsiusPresenter model: (aFCConverter aspectValue: #celsius). fahrenheitPresenter model: (aFCConverter aspectValue: #fahrenheit). celsiusPresenter model aspectTriggers: #celsiusChanged. fahrenheitPresenter model aspectTriggers: #celsiusChanged
礼儀として標準のモデルを設定します(クラス側)。
defaultModel
^ FCConverter new
華氏摂氏変換器 View の実装
Model と Presenter の定義が終わったら、ツールを使って View を定義します。
- ブラウザの FCConverterPresenter 右クリック Views - New - Resource name 'Default vew' のままで OK
- View Toolbox- TextField - TextPresenter.Default view を二回ドロップ
- それぞれ name プロパティを celsius, fahrenheit に変更
- ShellView のプロパティ - usePreferredExtent を好みで false に
- 好みでキャプションをつけたりする。
- File - Save で FCConverterPresenter class >> resource_Default_view に保存される。
- 実行は FCConverterPresenter show です。
更新の詳細
上記で aspectTriggers: を使って省略した更新の詳細について調べます。aspectTriggers: の実装には when:send:to: が使われています。これとtrigger: と組み合わせて更新通知に使います。単純な例を作ってみました。
model := Object new. counter := 0. model when: #fire: send: #value: to: [:aNumber | counter := counter + aNumber ]. model trigger: #fire: with: 7. Transcript print: counter; cr.
意地悪して、イベントハンドラの中でさらに trigger: するとあっけなく無限ループになりました。そうするとなぜ摂氏華氏変換器の例で、無限ループにならないのかが謎です。
Aspect
あるオブジェクトのインスペクタを開くと、最初に Published Aspects という項目が表示されます。aspect はオブジェクトの特に重要な項目だけ表示したい時に使います。詳しくは PublishedAspectInspector のクラスコメントに書いてあります。例えば
FCConverter class >> publishedAspectsOfInstances ^(super publishedAspectsOfInstances) add: (Aspect integer: #fahrenheit ); add: (Aspect integer: #celsius ); yourself.
のようにすると、華氏のインスタンス変数が無いにも関わらず摂氏と華氏両方の値がインスペクタに表示されて便利です。これは最高に面白い機能です。