Ruby初心者がRuby on Rails + Mongoidを試してみた
この記事はMongoDB Advent Calendar 2013の二日目です。
後の人のハードルを下げるために、私はレベルが低い話を。
私の勤務先は中堅SIerでして、そこの小さなチームでつかそうな案件のときはMongoDBを使おうということで、出番を虎視眈々と狙っている次第です。以前は Scala に Play! Framework に MongoDB を組み合わせるという案件をやりまして、そのときのことは↓に書いたりしました。
しかし私の部署の中で最大与党は Ruby on Rails (以下 Rails) で図書館管理アプリを作っているメンバーでして、ここのワークロードが低いときにでもぱっと案件取れるようにするには、Rails で MongoDB 使えますよ! と、説得できないといけません。
しかし Rails といえばなんつっても ORM (Object-Relation Mapper) である Active Record でして、Active Record は ORM というからして、当然 RDBMS を叩くことしか考えられていません。そんなわけで、Active Record 的な I/F を持った ODM (Object-Document Mapper) な Mongoid をちょっとお試ししてみました。
Mongoid とは何か
Mongoid とは何か、ということについては本家のページを参照してください。イントロだけ引用すると:
Mongoid (pronounced mann-goyd) is an Object-Document-Mapper (ODM) for MongoDB written in Ruby.(snip)
The philosophy of Mongoid is to provide a familiar API to Ruby developers who have been using Active Record or Data Mapper, while leveraging the power of MongoDB’s schemaless and performant document-based design, dynamic queries, and atomic modifier operations.
(参考訳)
Mongoid (発音は mann-goyd) はRubyで書かれたMongoDBのオブジェクト・文書マッパー (ODM) です。(略)Mongoidの哲学はActive RecordやData Mapperに慣れている Ruby 開発者に親しみやすい API を与えつつ、MongoDB のスキーマレスや強力なドキュメントベースデザイン、動的クエリ、アトミックな変更操作などの強みを使えるようにすることです。
このようにMongoidはActive Recordの置き換えを強く意識しているので、Railsエンジニアには使いやすいのではないかと思います。
Mongoidを試す
さすがに実プロダクトのバックエンドを差し替えるほどの Rails スキルはないので、新規プロジェクトで Mongoid を使ったものを作ってみます。私は Rails に詳しくないので新規プロジェクトを作成するのは Rails Tutorial を参考にしました。
rbenv + ruby-build を用いて Rubyは 2.0、Railsは 4.0 を導入した環境で:
rails new mongo_sample_app --skip-active-record
で新規のRailsアプリを生成します。--skip-active-recordがミソ。
そしてGemfileに次の一行を加えます。
gem "mongoid", github: 'mongoid/mongoid'
ここで Github の master を取ってきているのは Mongoid の stable である 3.x 系は Rails4.0 (正確にいうとActive Modelの最新) と依存関係が当たるので gem で普通に入れると非常に古いバージョンが入ってしまうから。これ調べたのちょっと昔 (11月頭ぐらい) なのでもしかしたら今は大丈夫かもしれません。なお、ドキュメントによっては bson_ext という gem が必要と書いてあるものがありますが、この gem は Mongoid 3.x 系で取り込まれたので不要になりました。単に Mongoid だけを指定すればいいです。
次に:
rails g mongoid:config
でconfigファイルを出力。config/mongoid.ymlというファイルが生成されます。開いて中をチェックしてみます。コメントが丁寧に入っているので見ればわかると思うのでここで引き写すことはしませんが、ローカルに MongoDB が動いている環境*1 ならばほぼ素で使えますが、そうでない場合も設定はとても簡単です。今回は深入りしませんけど、Read Preference とか Write Concern などもここで定義できる感じですね。
さてここまでできたら単純に一つモデルを定義して scaffold を生成してみましょう。次のようなモデル User を考えます。
User | |
---|---|
name | String |
String |
rails g scaffold User name email
なお型指定がないのは、Mongoid はデフォルトの型が文字列と決まっているから。
試しに rails s して localhost:3000/users にアクセスすると、ちゃんとユーザー作成ができるようになります。
ここで注目したいのは
db:migrate が不要なこと。
MongoDB の場合はスキーマ定義も不要であるしデータベースもコレクションにドキュメントをinsertしたところで自動で生成されるので、マイグレーションがいらないのです。正直 Rails のちょっとめんどくさいところってソースいじってるだけじゃ完結しなくて rake コマンド叩かないといけないところだと私は思っているので、これは結構ありがたい気がしてます。カジュアルな MongoDB っぽいなあと。
なお、この段階なら、要素を増やすのももう一度scaffoldの生成を行う (そして、コンフリクトは全部overrideで解決) するだけでよいです。
rails g scaffold User name email nick
さすがに出来合いの View や Controller 使う人はいないと思うのですが、Model だけは上書きしてあとは手書き、というスタイルで、開発途中の要素追加も割と容易なのではないかと思います。なお、View や Controller 側から Model を触るときには当然 Active Model にラップされていますので、Rails に慣れた人なら素直にコーディングできるのではないかな??
そんなわけで「スキーマ定義ができないけれど開発には着手しなければならない」場合、Rails + Mongoid + MongoDB という組み合わせはわりと「アリ」ではないかと思います。
埋め込みドキュメントを使うときとか、あと JOIN をしないといけないときとかについてはまだ試してないですけど、まあそういうことは誰かが書いてくれるでしょう多分。
と、ここまで書いたらモロおんなじ内容が Qiita にあったことに今気づきましたが、知らん顔して投稿することにします。重なっていない部分もあるしね。
あすは key さんですね。よろしくおねがいします!
デザパタを一人でこっそり振り返ろう #4 (Factory Method)
なんと前の記事を確認したら一年以上前だよ…… orz
ということで誰も読んでないと思いつつも続きを書きますが、へっぽこプログラマのぼくがひょんなきっかけから、Javaプログラマ向けデザインパターンの入門書として有名な:
- 作者: 結城浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2004/06/19
- メディア: 大型本
- 購入: 51人 クリック: 762回
- この商品を含むブログ (400件) を見る
Design Patterns Smalltalk Companion, The (Software Patterns Series)
- 作者: Sherman Brown, Kyle Woolf, Bobby Alpert
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 1998/02/10
- メディア: ペーパーバック
- 購入: 1人 クリック: 23回
- この商品を含むブログ (9件) を見る
過去の記事は次のとおり*1。
- デザパタを一人でこっそり振り返ろう #1 (Iterator)
- デザパタを一人でこっそり振り返ろう #2 (Adapter)
- デザパタを一人でこっそり振り返ろう #3 (Template Method)
ということで今回は Factory Method パターン。JDP から引用すると(p.46)、
Template Method パターン (中略) をインスタンス生成の場面に適用したもの
なんだそーです。
リフレクションとかの機能がなくて、動的にクラスにメソッドを追加できなくて、文字列をもとにオブジェクトを操作するみたいな作業がニガテな C++ や Java に比べると、Smalltalk や Ruby なんかだとあんまり大げさなことやらなくてもいい気がするんですが、はてどうでしょう。
ひとまず実践
えーと、JDP で挙げられている例は、「IDカード」を抽象化したクラスを作るためのフレームワークを提供するってものです。入門書って性格上しょうがないと思うんですが、JDP の例はこういう「例のための例」が多くてちょっと歯がゆいです。
まあ文句をいってないで実装しましょう。
JDP では Java なのでパッケージ機能を使って、framework パッケージと idcard パッケージに分けてますが、これは Smalltalk ならカテゴリーにしましょうか。framework はクラスを「作る」「使う」という抽象概念を定義し、idcard はそれを具象化するパッケージというわけ。
まずは framework 側。カテゴリー名は当たらないように長ーくなってます。通常の Smalltalk の名前付けルールとは違うけど許して。
んで具象側のオブジェクト側はこんな感じ。
出力は手抜きで Transcript に出すので、Transcript を表示させておいて:
hoge := IDCardFactory new. pro1 := hoge createOf: 'なるひこ'. pro2 := hoge createOf: 'なるおが'. pro3 := hoge createOf: 'naru0ga'. pro1 use. pro2 use. pro3 use.
とかやると、
なるひこのカードを作ります。 なるおがのカードを作ります。 naru0gaのカードを作ります。 なるひこのカードを使います。 なるおがのカードを使います。 naru0gaのカードを使います。
てーな感じになるわけです。
super new って大丈夫なん?
で、これを書くときに意外とハマったのが、コンストラクタで値を初期化するよーな書き方。って、今までやったことなかったんかいな自分……。
具体的には、
IDCardFactory class>>new ^ super new initialize. IDCardFactory>>initialize owners := Bag new.
あたりですね。
C++ や Java、Ruby なんかだとコンストラクタでメンバー変数代入すればいいんで (C++ だと初期化子を使うほうが行儀がいいのか?) 最初は:
IDCardFactory class>>new owners := Bag new.
って書いたら怒られた。「クラスインスタンス変数には owners なんてねーよ」って。おうふ。
じゃあってんでインスタンス生成のときに値を初期化しているようなオブジェクトなんかねーかなって考えて、そうだ Complex とかどうしてるんだろって見たら、
Complex class>>new ^ self real: 0 imaginary: 0. Complex class>>real: aNumber1 imaginary: aNumber2 | newComplex | newComplex := super new. newComplex real: aNumber1; imaginary: aNumber2. ^ newComplex.
みたいになってる。ここで real: と imaginary: はどっちも Complex で定義されてるメソッド。
んー? super new したものに Complex のメッセージ投げて、なんでうまく動くんだろう?? と思って、ついったに:
Hoge>>new ^super new initialize. とかやってちゃんと動くのが不思議な感じがする。super newしたから親クラスのオブジェクトができてるはずなのになんでinitializeメッセージが投げられるんだろうと不思議な感じ
ということは、super new して得られるオブジェクトはあくまでも super の持つオブジェクト (= IDCardFactory クラス) のインスタンスオブジェクトなんだけど、new を処理するのはその上位 (= Factory -> Object -> ...) ってーことなんやね。なんかややこしいけど、イディオムとして覚えとこ。
まあ結果としては、今回の書き方で正しいらしいです。
このあとの議論も面白かったし、ここ深堀するといろいろ面白そうだけど、主題じゃないのでまたいつの日か。
考察的な何か
で、えーと、とりあえずこの例だと:
- Factory の時点で「作るために必要なもの (Factory class>>createFor: anOwner の anOwner)」が決まってしまっているのは果たしてどうなのか。ものすごく使いにくいような気がする
- なんで Register しているのか説明がなんにもないので意味がわかんない。これはこのパターンに必要なのか?
とか思っちゃうわけですよね。
後者のほうはどっちかというとテキストの問題ですな。IDCardFactory クラスで register するのは多分、作ったオブジェクトに対してブロードキャストとか処理の委譲をしたいからだと思うんだけど、JDP だとそういう話はぜんぜん出てこないし、そもそもそういうことしたいなら具象クラスじゃなくて抽象クラスの Factory の方でやらないとマズイ気がするし、教科書としては不要な例じゃないかなーと思うのだけど、まあ、先を読み進むと意味があるのかもしれませんし、そこはあまり追求しますまい。
むしろ前者が辛いですよねー。これって実用できる範囲をとても狭めてしまうと思うんだけど……そこのところ、もうちょっと解説が欲しいすな。例えばウィジェットの生成なんかは Factory をよく使う例だと思うんだけど、ウィジェットの場合、生成時に必要な初期化パラメータって微妙に異なったりしますよね。そういう場合、このパターンでやるのが正解なのか、違うパターンでやるのがいいのかとか、もし Factory Method でやるとしたら Java の場合にはどう実装するのが正解なのかってのが知りたいなーと。
DPSC を読んでみた
さて DPSC の Factory Method パターンは p.63 から。
前にも書いたけど DPSC の場合は「このパターンは Smalltalk のクラスライブラリの中で実際こう使われてるよ」って書いてあることで、ここで例に出されているのはズバリ、MVC (Model-View-Controller)。MVC の元祖は Smalltalk なんですよね。でまぁ、例として挙がってるのは複数の種類 (Text と Drawing) の Document を扱える Editor アプリケーション。これは Editor を Factory、Document を Product として考えれば実現できますよと。
Factory Method で MVC のクラスの管理コストを下げる
MVC の辛いところは、要素が増えてくるととにかくクラスの量が爆発的に増えることですよね。DPSC で挙げられているのは View と Controller の関係なのですが、基本的には View は「違う見え方」に対して一つずつクラスを作るのに対して、Controller は対応するだけ作る必要はなくて上位のビューと共通化できることも多いです (Smalltalk の例であれば View の BooleanWidgetView > ActionButton に対し、Controller は両方とも WidgetController でよい)。となると、両者の「どのビューはどのコントローラを使うか」っていう管理をしないといけないです。MVC の元祖的な Smalltalk-80 の場合は Dictionary で管理してたらしいですが、これはクラスが増えるたびに管理が大変。
ということでこの点について VisualWorks で使っているテクニックが紹介されてます。さっくり引用(p.68)。
View>>getController "レシーバーの現在のコントローラーを返す。もしレシーバーのコントローラーが nil だったら (通常の場合)、レシーバーのデフォルトコントローラーの初期化されたインスタンスが導入され、 そのインスタンスを返す。" controller == nil ifTrue: [self setController: self defaultController]. ^controller.
んで、defaultController を Factory Method としてみなせば、それぞれの View に対応して defaultController の定義をこんなふうに書けばよくなる。
View>>defaultController ^Controller new. Text>>defaultController ^TextController new.
しかしもうひと工夫。どちらの defaultController も「あるクラスに対して new を投げてるだけ」なんだから、これも括り出しちゃえばいいじゃないかと。そんなわけでこんな感じ。
View>>defaultController ^defaultControllerClass new. View>>defaultControllerClass "レシーバーのデフォルトコントローラーのクラスを返す。" "もし Controller 以外のコントローラを使いたければ、サブクラスは" "defaultControllerClass を再定義すること。" ^Controller. Text>>defaultControllerClass ^TextController.
……正直、きれいになったとは思うけど、うれしさがさっきの例とくらべてよくわからない自分がいます……。
あ、でも下位クラスでオーパーライドすべきメソッド (逆にいえば、同じコントローラで構わなければオーバーライドしなくてもいい) がわかりやすいのはいいですね。
ここでの defaultControllerClass みたいなメソッドは Constant Method と呼ばれるらしくて Kent Beck の:
ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集
- 作者: ケントベック,Kent Beck,梅沢真史,皆川誠,小黒直樹,森島みどり
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2003/03
- メディア: 単行本
- 購入: 7人 クリック: 94回
- この商品を含むブログ (55件) を見る
それから、同様の方法はクラスインスタンス変数を用いてもできますよねという話。さっきの例を書き換えればこんな感じかな?
View class instanceVariableNames: 'defaultControllerClass'. View>>defaultController ^self class defaultControllerClass new. View class>>initialize defaultContorllerClass := Controller. TextView class>>initialize defaultContorllerClass := TextController.
あと p.73 からの「Known Smalltalk Uses」で、豊富な実例が紹介されていてなかなか素敵なのだけども省略。クラス生成だけじゃなくて「外部 (external)」のなにかを呼び出すときに、呼び出すパラメータ文字列を生成する奴なんかも Factory Method パターンで扱えるよってのはふむふむだった。
まとめ
さっきの疑問「インスタンス生成のときに決まった引数を渡さなきゃいけないのって、なんかうれしさがわからない」という件、DPSC では「ただ new するだけ (引数とか取らない) でも嬉しいパターンはたくさんある」ということを提示していて、これなら便利さが朧気ながら分かりますね。MVC の例のように、「あるクラスと関係するクラスを new して関連を保持しておく」みたいな処理はみんなこのパターンになるわけだから。むしろ「工場と製品」アナロジーから離れたほうが広く応用が考えられそうですね。
次回は Singleton。いつになるやらわかりませんが(^^;
*1:タイトルが一貫してないことが発覚したのでこそこそ修正。
デザパタを一人でこっそり振り返ろう #3 (Template Method)
この企画は「動的言語においてはデザパタの多くは意味を成さないか違うパターンになる」というよくある俗説?を体感したくて、自分にとってはNative LanguageじゃないSmalltalkで結城先生のJavaのデザパタ本 (以下JDP) をなぞってみるという企画でございます。
さて今日のお題からJDPとしては第2部に入りまして「サブクラスにまかせる」というもの。
んで最初がTemplate Methodパターンと。ふむ。
これって要は「親クラスの外部インターフェースを定義している処理を、より詳細にしたメソッドの単純な呼び出しになるよう分割して、その下の具象クラスに実際の処理を隠蔽する」って奴ですよね。
まずは実践
つーことで、JDPの例を、なるべくJDPを見ないようにSmalltalkで書くと*1、まずは AbstractDisplay は:
んで CharDisplay、StringDisplay*2 はこんな感じかな。
あー分かってます分かってます。String>>open と String>>close の実装が愚直すぎることは。コンストラクタで string が与えられたときにサイズ決まるんで、そのときに文字列作って持っておけばいいんだよね。ちょこちょこ試した感じだと、WriteStream 使えばいいのかなって思うんだけど。こんなふうに。
stream := WriteStream with: '+'. ('hogehoge' size) timesRepeat: [stream nextPut: $-]. stream nextPut: $+. stream contents. "print it."
まあ、それはいいとして。
a := CharDisplay with: 'A'. a display. b := StringDisplay with: 'hogehoge'. b display.
ってやって全選択して do it すると、
<<AAAAA>> +--------+ |hogehoge| |hogehoge| |hogehoge| |hogehoge| |hogehoge| +--------+
まぁ、これで一応、やるべきことはできた……けど、なんだか当たり前だのクラッカーだなぁ。デザパタってのはそゆもんっていえばそゆもんだけど。
とゆことで DPSC へ
では DPSC ではどんなことが書いてあるかというと、ちと見てみましょう。
継承って難しいよね実は?
OOPにおいては継承って意外と難しい概念なんで、SELF って言語だと継承をとっぱらっちゃって、代わりに delegate (前回出てきた「委譲」ですな) という概念を導入しちゃってますよとか何とか。
私のへっぽこ英語でさらっと読んだ範囲と自分の経験からいうと、継承が混乱を生むのはその上位下位の関係でなにを抽象化するのかがごちゃごちゃになるからであって*3、「この継承関係はこういうことを抽象化するの!」ときちんとポリシーさえあれば、やっぱり継承は便利だよねと。
で、Template Method はまさにそういう「ポリシーをきちんと決める」例であって。
abstract なメッセージをどう実現するか
えっと、ここは前回からのちょっとした進歩。
私の Smalltalk はまだぴよぴよなので、前回のエントリでは、Java でいう abstract、C++ でいう pure virtual なメッセージの実装方法がわかんなかったわけ。で、コメントだけ書いて定義空っぽにしておいたのね。
でも、ここではまだ実装しないよ、サブクラスの責務だよ、というときには Object>>subclassResponsibility ってメッセージを self に投げとくと、よきに計らってくれるんだそうです。つか、「サブクラスでオーバーライドしないとダメだよん」って実行時エラー出してくれるだけだけど。
でも、実は前回のやり方も間違いってわけじゃなくて、「別に実装しないんだったら、なにもしないでもいいよ」というインターフェースってのもあるでしょうと。そゆ場合には空っぽの実装おいておけばいいよと。
これはあれだよね、Java だったら abstract にしないで stub にしとけばいいとかそういう話だよね。
クラス階層設計の原則
えーと、Auer って人がクラス階層を定義するんだったらこんなふうにしなさいよって指針を出してるそうです。詳しくは DPSC p.359 を読んでください。一応メモ。
- クラスは振る舞いによって定義しなさい。状態によって定義してはいけません。
つまり最初はクラスの構造 (平たくいえば属性;インスタンス変数とか) を考えちゃダメだよと。 - 振る舞いを抽象化された状態を使って実装しなさい。
逆の言い方をすれば、抽象的な振る舞いを実装するのに必要な状態 (属性) だけを考えて、後のことは後でかんがえろということなんだろね。 - メッセージのレイヤを認識しなさい。
つまり親クラスで実現されるべき振る舞いを、サブクラスによって実装されるべきいくつかの小さなメッセージを呼び出す形で考えとけってことかしら。 - 状態変数の特定を後回しにしなさい。
んっとこれは、親クラスでは処理の多くをサブクラスにお任せするわけだから、その処理に必要な状態を親クラスが持つべきではないよねと。サブクラスでそれぞれの処理を実装するときに、じゃあどういう状態 (属性;インスタンス変数) が必要かを考えなさいよと。
まぁ、おっしゃるとおりで。このとおりにやってれば、Template Method って形に自然になるよねーと。
あと Delegate (委譲) について「なにを委譲するの」って説明とか*4、クラス階層のリファクタリングのケーススタディとかあって、面白いんだけど省略して。
めんどくさいメソッドを Template Method で実装する方法
というのが書いてあったのでサラッと紹介。詳しくは DPSC 363-364 を。
- 最初はまず実装しちゃえ
- それをいくつかのステップに細分化しよう
- それぞれのステップをメソッド化しよう
- そのメソッドを呼ぶように書き換えよう。
- メソッドの中にクラス名やリテラルやその他定数になるものがあったら、「定数を取り出すメソッド (constant method)」に分割しよう
- 1〜5を繰り返そう。
多分5が一番ミソっぽいよな。ここで constant method をうまく抽出できれば、それを下位のクラスに追い出すことができるわけだから。
あと Smalltalk で Template Method パターンがどう使われてるかってのが延々続くわけだけど、そしてそれがとても面白いのだけど、私が力尽きたので省略。つかみんな DPSC 買って読め。
まとめ
正直 Template Method はあんまり Smalltalk らしさがないんで、前回の Adapter パターンほどワクワクしなかったんだけど、クラス階層設計のポリシーとか Template Method の抽出のしかたとかの丁寧な説明を読んで、DPSC のテキストとしての良さを再認識した気がします。
JDP だとパターンの説明は分かりやすいんだけど、「ほらね、継承使えば便利なことがあるってことがわかったでしょ。でも、どういうふうに継承を使うかってのは決まりはないんだよねー」とか説明が腰が引けてるんだけど、それって DPSC に比べるとズルいと思う。いや、対象読者層が違うといえばそうなんだけど、さ。
単なる公式集じゃなくて、そこからもう一歩上がった「この公式ってどういう考え方で出てくるの?」ということをちゃんと書いてるのが好感度。ここらへん GoF はどうだったっけかなぁ。
さてお次は Factory Method パターンか……これも楽しみですな。しかし、いつ書けるのやら。
*1:最近ムダにgist使うのがお気に入り。
*2:どうでもいいが、Char と String で名前の縮め方のルールが違うのが気になる……。
*3:私の見た酷いコードの例だと、あるクラスの責務が外部仕様の変更によって大きく変わったときに、それを継承によって表現してて (つまり親が I/F なのはいいとして、子が「旧世代のクラス」「新世代のクラス」で兄弟になってる)、新世代のクラスで I/F が変わっていくと、それに合わせて親を変えなきゃいけなくなって、親の I/F が変わるということはそのサブクラスも I/F を合わせなきゃいけないわけだから、もう使われるはずがない旧世代のクラスもせっせこ stub 書かなきゃいけないという最悪のがあった。
*4:これがけっこう面白いのだけど、紹介は機会があれば。
デザパタを一人でこっそり振り返ろう #2 (Adapter)
シリーズ第2回。今回はAdapterパターン*1。
まぁ平たくいっちゃえば「オブジェクト間のインターフェースをコンバートしてやることで、いろんなオブジェクトを統一的に扱うことができるようにしようね」ってもの。まあ「アダプター」だもんね。
インターフェースを使う側を Client、アダプターインターフェースを提供するものを Target、Target の定義するインタフェースを橋渡しするのが Adapter、Adapter から呼ばれて機能を提供するものを Adaptee というようです。
Tailored Pattern
GoF /JDP で説明しているパターンがこれ。
二通りの実装があるよって書いてある。ぐぐれば出てきそうなもんなのでクラス図書いて説明したりはしないけど。
継承方式というのは、Target を抽象クラスにしておいて、Target と Adaptee を多重継承して、Adaptee で定義されているインタフェースを self で使って Adapter がインタフェースを実現するというやり方。
委譲方式というのは、Adapter は具象クラスで、その代わりに内部に Adaptee のインスタンスを持って、そのインスタンスにメッセージを送ることでインターフェースを実現する方法。Adaptee のインスタンスに「この処理お願いっ」ってするから「委譲」っていうんだね。
ということは、「Adapter が、Target に提供すべきインターフェースと、Adaptee の提供するインターフェースをがっちり知っていなければならない」ですよね。
ということで、DPSC ではこれを「Tailored Adapter」と呼んでます。「仕立てアダプター」っつか「オーダーメイドアダプター」とでも言うのか。
んでDPSCでは「Smalltalkには多重継承ないから、委譲でしかできないんだよね」ってことで GoF の例を実装してるけど、せっかくだから JDP の例をやってみませうか。
でも JDP の例ってあんまよくなくて、モノが一個しかないのになんでインターフェースを併せてあげなきゃいけないの? って思うよね。ココらへんはLineとTextという違うインタフェースを持つものを統一的に扱いたいから……っていうGoF/DPSCの例のほうが分かりやすいとは思うけど、動かして結果が見やすいからこういう例なのかな?
まあさておき、ぼくが Smalltalk で愚直に実装するとなるとこうかな。
Banner クラスはこんなんで、
Print クラスはすごくシンプル。
PrintBanner クラスはこんな感じかなぁ。
トランスクリプトとワークスペース開いてワークスペースに以下のコードを書いて。
testBanner := Banner has: 'Hello'.
testBanner showWithAsters.
testBanner showWithParens.
Transcript cr.
test := PrintBanner with: testBanner.
test printStrong.
test printWeak.
うりゃっと全選択して do it すればトランスクリプトに、
*Hello* (Hello) *Hello* (Hello)
されますねと。
Traits 使ってみよう
Traits というのは Ruby のモジュール mix-in に近いけど、名前がぶつかったときにエイリアスで逃したりできる、もっと緩い感じのモノ、という理解でいいのかな。
Matz さんが Ruby にも入れたいなーと言っているらしいところを見ると、mix-in とは違ったメリットがあるんでしょう。多分。
Squeak の派生である処理系 Pharo と、Squeak 3.9*2 から入りました。
これを使えば継承方式で実装できるのかなーなんて思った。
ということでさささっと書いてみたっす。
まずは Print を Trait として定義するわけだけど、Smalltalk 的には Trait には T という Prefix を付けるようなので、名前を変えまして。
んで、PrintBanner の定義はこんなふうになるのかな。名前は継承版と被らないように一応変えておいた。
使い方は委譲方式とちょい変わって、
test := PrintBanner2 has: 'Hello'.
test printStrong.
test printWeak.
で、ちゃんと実行できました。ぱちぱち。
そもそも Tailored Adapter っているの?
ぼくがこの説明を JDP で読んで、最初に思ったのは、
なにもこんな大げさなコトやらなくても、
Banner>>printStrong self showWithAsters. Banner>>printWeak self showWithParens.でよくね?
ということだった。
DPSC にもその点については書いてあって、「C++ だったらリコンパイルがいるからこういうやり方はちょっとないけど、Smalltalk だったらやってダメな法はないよねー」って書いてあった。やっぱそうか、そうなんか。
多分 ruby でもこんな感じに書けるんじゃないかな。
class Banner def printStrong self.showWithAsters end end
Message-Based Pluggable Adapter
さて Tailored Adapter は「呼ぶ方のも呼ばれる方のもインターフェースが明らか」という仮定をおいていた。でも、この仮定が崩れたとしたら?
つまり、設計ができた「あとから」新しい Adaptee ができるとしたら?
例えば MVC パターンでウィジェットが表示用にモデルから値を取り出すときなんかを考えると、ビューのウィジェットが Client でモデルが Adaptee になって、クライアント側は value メッセージで表示用の値をとって value: メッセージでセットするとかなる。ところがモデルになるクラスって自前で作るとは限らなくて他所からの流用だったりライブラリだったりするわけで、そうなるとそれごとにAdapter書かなきゃいけない。そういう種類が違うモデルが出現するたびにこんなに大騒ぎするのめんどくさいですよねえ。
めんどくさいのもそうなんだけど、クラスってのはオブジェクトの「雛形」なんであって、Adaptee の数だけ雛形が存在するのって、ちょっとなんだかなぁ、って思いません? ふつー思いますよね。
そこで Smalltalk で活躍するのがこの Message-Based Pluggable Adapter なのだ。
要はね、Adaptee が新たに登場するたびに Adapter クラスを作るのって大変じゃないですか。
そこで、インスタンスに対して動的に動作を突っ込める Smalltalk の機能を活用して、「Adapter クラスのこのインスタンスはこの Adaptee 用」ってできれば嬉しいじゃないってこと。だとぼくは理解してる。
例えばさっき例に出した MVC の話で言うと、ビューがモデルから値を取る value ってメッセージと、逆にモデルに値を設定する value: ってメッセージが考えられるじゃないですか。で、こいつらの動きを、インスタンスごとに定義できたらクラスが爆発しないでいいよねと。
DPSC の写経をしてもしょうがないので大雑把な構造を書くと、
- Adapter クラスはコンストラクタで Adaptee のインスタンスを貰っておく。これは以上による Tailored Adaptor と同じですね。
- Smalltalk には "perform:" ってメッセージがあって、以下の二つは等価なんです。
anObject #hogehoge = anObject perform: #hogehoge
- ここで重要なのは、#hogehoge が変数に代入されたものであっても構わないということ。だから例えば getSelector という変数に束縛されているとしたら、以下のように書けるということ。
anObject perform: getSelector
- ということは selector メッセージを使えば、一般的な Adapter というクラスのインスタンスオブジェクトに対して「value に対するメッセージは何?」という設定ができるってことになるわけです。
例を挙げましょう。細かいクラス定義は省略するとして、あ、anObject と printStrongSelector は MessageAdapter のインスタンス変数ね。
と定義しておけば、
adapter := MessageAdapter on: (Banner has: 'Hello'). adapter setPrintStrong: #showWithAsters. adapter printStrong. " => *Hello*"
と実行できますよと。おお、すげー。
さらに文字列をシンボルに変えるメッセージ asSymbol を使えば、シンボル一個渡すだけで、getter / setter ど同時に定義できたりする。
この仕組みを使うことで、クラス数の爆発を抑えつつ、MVC なんかでの M と V の結合を柔軟に変動させることができる。うーん、これぞプログラミングの醍醐味という奴ではないかい?
この仕組みはめちゃんこクールだと思う。詳しくは DPSC p.114-115 を読むとよいですよ。
Parameterized Adapter
Message-Based Pluggable Adapter はとてもクールなんだけど、インタフェースがパラメータを要求するとき、それを渡すすべがないという問題? がある。
ということでそんな場合に使うのが Parameterized Adapter。
すごい平たくいってしまうと「getter / setter にブロックを渡してしまう」って Adaper。GoF や JDP における Adapter パターンとははるか遠くに来てしまいましたが、モダンなプログラミング言語なら似たようなことはできると思う。
あまりまとまってないまとめ。
- GoF や JPD で紹介されている Tailored Adapter は Smalltalk (あるいは他のモダンな言語) でも実装できるけど、追加すべきメソッドが静的に決まるのであれば、既存クラスを直接いぢっちゃても (動的言語の場合) 問題ないので、正直誰得。
- その点、インスタンスごとにあるオブジェクトのメッセージをどうディスパッチするかを指定できる Message-Based Pluggable Adapter はすごい強力。
- ということで、Pluggable Adapter は VisualWorks でもりもり使われてる、そうな。
ここで紹介したことがすべてではないので、動的言語においてデザインパターンっってどうあるべきなのかってことに興味がある人は、ぜひ
Design Patterns Smalltalk Companion, The (Software Patterns Series)
- 作者: Sherman Brown, Kyle Woolf, Bobby Alpert
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 1998/02/10
- メディア: ペーパーバック
- 購入: 1人 クリック: 23回
- この商品を含むブログ (9件) を見る
さて次はなんだっけ。Template Method だっけ。
DPSC 的にはどんな切り口なのか楽しみですね。
デザパタを一人でこっそり振り返ろう #1 (Iterator)
2011.02.27 注記:umejava さんのコメントにより、一部間違った記述を訂正。
最初は読書会に参加して勉強させて貰うつもりだったんですが、本来の趣旨であるところの:
GoF を基礎から丁寧に振り返りましょう
というテーマを無視してヨタ話に突っ走り、それはそれで非常に面白かったんだけど、むしろ基礎を押さえたい人を置いてけぼりにしてしまったことにいたく反省をして、読書会は抜けたわけで。
でも、そのためにせっかく買った
- 作者: 結城浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2004/06/19
- メディア: 大型本
- 購入: 51人 クリック: 762回
- この商品を含むブログ (400件) を見る
そしたら注文してた、
Design Patterns Smalltalk Companion, The (Software Patterns Series)
- 作者: Sherman Brown, Kyle Woolf, Bobby Alpert
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 1998/02/10
- メディア: ペーパーバック
- 購入: 1人 クリック: 23回
- この商品を含むブログ (9件) を見る
つーことで、以下さらさらっと舐めたいと思います。
以下結城先生の本は JDP、Smalltalk のヤツは DPSC と略記します。
Iterator パターン
d:id:naruoga:20110210:1297363662 で振り返った内容なわけですが、DPSC の見解は Ruby などの動的言語でも通用するんじゃないかな。
内部イテレータ
「ある固まり (Aggregator) に対して、その要素すべてにある処理を順に実行していく」という、その「要素すべてに」順次実行していく、という制御そのものを内部に隠蔽してしまうのが内部イテレータ。
外部イテレータ
内部イテレータの方が記述がコンパクトだけど、処理を外部から制御したいときだってある。そういう場合は「進む」「戻る」という概念を外に出して、積極的に制御を外部に取り出してやる。これが外部イテレータ。
- Smalltalk の場合には
Collection のサブクラスであるStream が外部イテレータとして使える。 - Smalltalk では String は Character の Stream なので、レキサとか書くのに便利だよ。
- テキストエディタのアンドゥ・リドゥを実現したいときなんかにも Stream を使うといいよ。
ここまで丁寧に説明して貰うと、「内部イテレータだけでいくない?」という疑問がちょっと解消されるよね。
今 DPSC の Adaptor パターンを読み中なんだけど、こっちもすっげぇ面白い。早く追いつきたいよう。
でも今日はこれでおしまい!
ごめんなさい。
*1:空間効率いかにも悪そうだけど、オブジェクトを Copy On Write しとけば済む話なのかな。←2011.02.17 追記:Shallow Copyなのでそれほど重くないそうです。やっぱそうですよね。
デザインパターンと具象と抽象
昨日の話のちょっと続き。
抽象クラスの導出って難しい
私は元々、大学時代は人工知能の研究室にいました。
まあやってた内容は「人工無能との対話によって人間のもつ概念の曖昧さを具体化することができるか」という非常にウサンクサイ内容で、その対象としてソフトウェアの要求把握という題材を採って、知識表現としてのフレーム表現と、当時翻訳が出たランボーのOMTの本:
- 作者: ジェームズランボー,ウィリアムプレメラニ,ウィリアムローレンセン,マイケルプラハ,フレデリックエディ,James Rumbaugh,羽生田栄一
- 出版社/メーカー: トッパン
- 発売日: 1992/07
- メディア: 単行本
- 購入: 3人 クリック: 48回
- この商品を含むブログ (6件) を見る
で、今の大抵のオブジェクト指向言語って継承概念があるじゃないですか。
でもね、人間って、ものすごく意識しないと、具象とか抽象とかってことを認識できないんですよ、私の感覚からいうと。
知識表現とかオブジェクト指向の例としてよく動物が挙げられますが(鳥は飛べる、鷲も鳩も鳥だから飛べる、けどペンギンは飛べないってのは「飛ぶ」メソッドを上書きして潰しちゃえ、とかね)、これは我々が学校教育で生物の勉強を多少なりともしているからです。だから生物の系統図的なものが頭に入っている。
しかし、そうでないものに対して、常に抽象化してものを考えられるかというと、それは非常に難しい。特にある「モノ」が一個しかない場合はなおさらです。上位下位概念と包含概念とか非常に混同しやすいですしね。
例えばプログラミングの初学者が配列「だけ」を見て、いわゆる「コンテナ」というものの一つであるということを思いつくことは、たぶんムリです。
だから「実世界を、今考えている応用の視点でモデル化して、それをクラスに抽出する」というやり方だと、往々にしてオブジェクト指向のお題目である「クラスの再利用により生産性が上がる」というのがうまくいかないという問題意識があったんではないかなと。
ということで、デザインパターンというのは、「じゃあよくある抽象化を公式としてパターン化しちゃおうよそうだそうだ」ってことなんじゃないかと想像しているのです。よく訓練された人でないと非常に難しい抽象クラスの導入を公式化してやるってことがミソなんではないかと。
そーゆー意味で、オブジェクト指向=継承、という考えをほぐすために、抽象データ型とか、あとJavaScriptみたいなプロトタイプ型オブジェクト指向(親とか子とかいいから、似てる奴からパクッちゃえ)なんかをやっとくといい気がするなぁ。
そして再び継続の話
しつこくてすみませんが、結局「継続」のような概念についても同じなんじゃないかと私は思うんです。
これは言語感覚の話なので、あくまでも私の「感覚」ということでお考えください。
昨日のついったの議論でもあったように、「継続 (continuation)」とは「ある一貫した処理の、途中までやったその続き」というのが一般的な定義です、よね。まぁ Wikipedia の説明でも読んでみてください。昨日ちょっと話に出た、Cのsetjmp()のこともちょっと書いてありますね。例外処理も継続の一種だなんてのは興味深いと思います。
では継続という概念を導入して何が嬉しいか。
それは、「実行状態を保存して続きをやる」ということに名前がつくことがまず一つ。
でもぼくは、それはうれしさも中位なりだと思うんですな。
「ああそれは継続を使うんだね」といったとき、実行状態を「どう」保存するかは各自考えなさい、じゃ、言われたってぜんぜん嬉しくないでしょう? 論文書く人はうれしいかもしれないけどさ。
だから「どう」保存するかも抽象化した、最低でも「パターン」が欲しい。そこまでして、「継続を導入する」とか、そういう用法に重みが出てくると思うんです。だって、それならそのパターンを使うってことで、やることが明確になるから。
そしてどのようなプロセスであっても抽象的に「ここまでやった、残りはあとで」ってことをしたければ、「実装としては」実行コンテクストを保存するしかないじゃないですか。だって、どんな処理かわかんないんだから。そしてそれは多くの言語で実現がとても困難、というのは先に述べたとおり。
ただし、「どんな処理でも、っていうのはムリだけど、こんな処理に限定して、っていうことなら、こういうクラス/関数/なんとかかんとか、を書けば、ある程度抽象化できるよ」ってのはあるでしょうね。それを綺麗にパターン化できるなら、意味があるかなと思う。ただし私は頭のいい方ではないので、これについては具体例が思いつかない。
あんままとまってないけどまとめると、
- 定義として「継続ってのはある処理のある時点からの続き」ってのはある。
- けどそれを「実装する文脈で」使ってもうれしくもなんともない。
- 実装する文脈でうれしいというには、抽象概念に対応した具体的な何かが必要。
- 「具体的な何か」として実装ということなら、実行コンテクストを保存するよりないし、それは多くの言語で困難。
- 「具体的な何か」として、汎用性をある程度捨ててもよければデザインパターン的なもので対応できるかもだけど例は思いつきませんゴメンナサイ。
つまり、なんですな、「なんのために」「なにを」抽象化するかってこと。
結局具象化したものを綺麗に便利に使いたいから抽象化するんでしょうと。
コード書くのではなくて、論文を書くんであれば別でしょうけど……デザインパターンってコード書くための生活の知恵なので、その勉強をするというコンテクストであれば、「継続」も生活の知恵になってくれなきゃ概念を導入する意味がない、というふうに、私は感じています。
デザインパターン読書会 #1
@cesare さん主催のイベント。
- 作者: 結城浩
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2004/06/19
- メディア: 大型本
- 購入: 51人 クリック: 762回
- この商品を含むブログ (400件) を見る
趣旨は少なくともそうだったはず。
ごめんなさい、なんだかディープな雑談ばかりになってしまった。
もっとベーシックな方向で話をしたかったですね。
Iterator パターン
今回は第1章でIteratorパターン。
最初から、Iterator パターンは while ループが外に出たままで、抽象化が不完全、という話が出た。いわゆる、外部イテレータと内部イテレータの話ですね。
Smalltalk だったら例えば*1、
#(1 2 3) do: [:n | Transcript cr; show: n]
というふうに、do: に処理をブロックで渡して、Array 側で処理してもらえるわけで、こういうほうがいくね? っていう。
まあ、私も外部イテレータは気持ち悪いと思うんだ。けど、メソッドそれ自体を引数に渡すって手段がないJavaという言語なら、しょうがないかなあって感じる。でもなぁ、C++なら関数ポインタでイケる気がするんだけど。
ともかく、「データ構造が違っても同じように使える」は、十分嬉しい抽象化だと思うので、それはそれでいいと私は思う。
私としては一番のふむふむポイントは、AggregatorとIteratorを分離することで、Iteratorをnewするごとに「最初から値を取り出す」って処理をどんどこ行うことができるってことかなと。
あとスレッドセーフの話が出ましたね。イテレーションしてる間に外部からデータ書き換えられちゃったらどうするんだと。まあ、それについては応用編なので、結城先生の別の本を読みましょうってことで。
大事なのはデータ構造がどうなってても、hasNext と next という統一したインターフェースで処理が記述できるってことなんだよって話になって、配列とか線形リストとか木構造とか、あるいはファイルシステムだとかデータベースだとかそういうことをぜーんぶ無視して同じように扱えるってことだよねって話をした。
ここで「木構造でnextを実現するには継続が必要じゃない?」という発言が出て、えっ、って感じで、いやいやそんなことないでしょう、次を返すのに必要な情報さえあれば十分なんだから、継続とか持ち出さなくても大丈夫でしょう、という話をしたんだけど、これが後でモメるネタになるとは。
あとGoFだとfirst/get/nextという構造なのに、結城先生の本だとhasNext/nextという作りなのはなんでだろ、という話になったけど、これは多分、GoFだとC++だから
for (it.first() ; !(it.isDone()) ; it.next()) {
val = it.get()
}
と、for文が綺麗に書けるからってことかなぁって話になったような気がする。
じゃあなんでJavaではそうしなかったの? というと、while一個で簡潔に書きたかったからなのかなぁ。
例えば first() も next() も要素が返せなかったら null を返すようにして、
book = it.first() while (book != null) { System.out.println(book.getName()); book = it.next() }
みたいな書き方 (それどこの LotusScript? という声が聞こえた) もできるだろうけど、イディオムとして冗長だし、返却値で動き分けるのって綺麗じゃないよね、C じゃないんだから。だから、妥協点として正しい気がしてきた。
話戻って外部イテレータ対内部イテレータ
外部イテレータってイケてないよねなコト。
無論?私もそう思うんだけど、なんでそうなのかな、と帰る道々考えた結果、次のような屁理屈をあみ出した。
外部イテレータと内部イテレータの大きな違いは「制御構造を隠蔽しているか、外に出ているか」なんだけども、それによって生じる内部イテレータの大きな利点の一つは、制御構造を隠蔽することによって、データ構造に最適なイテレーション (例えば再帰とか) が容易に選択可能なことなんじゃないか、と。
でも本質論からいうと、それって言語の持つ再帰という機能によってスタックフレームに状態の保持を押し込めているだけで、状態を保持しなければイテレーションはできないんだから、それをどうやるかという手段の話でしかない、とも、私は思うのです。
いや、実装する人間としては、きれいに書けることは嬉しいが :)
継続についてあれこれ
そこでついったで一モメした*2 継続について。
えーと、ああいう文脈で「継続」といわれると、もちろん Scheme や Ruby や Smalltalk が言語機構として持っている継続が浮かびますね。
でも先程の再帰の話と同じく、言語として用意されているかどうかは本質じゃなくて、「継続」という概念ってのは何の抽象化なのか、ってのが大事だと思う。
イテレータの例でいうと、配列のイテレーションならindexを内部状態として保持しておけば、next() は作れる。簡単ですね。
でも木構造の場合、スタックに分岐点のノードオブジェクトの参照をプッシュしていって、葉までいったらそれを返すとともに、スタックから分岐点を取り出して次の枝に降りていく、ということをやらなきゃいけない。めんどくさいですね。めんどくさいけど、データ構造に見合った処理をしてるだけです。つまりぜんぜん抽象化していない。めちゃんこ具象です。
そして私は「継続」という「概念」を、上の二つの例を、その具体的な処理内容に依らず、途中で止めて、任意のタイミングで再実行できるしくみを用意することだと自分の中で定義しているわけです。これは、前記の言語が提供している機能がそうなので、言語設計者が抽象化したかった概念がそうだと解釈しています。
もちろん、それは言語にビルトインされていてもいいし、自前で頑張って書いてもいいけど、イテレータがそのイテレートする対象のデータ構造を隠蔽しているが如く、継続においては継続の対象となる処理を隠蔽していなければならない。これは必須だと考えています。
それを実現するためには、実行コンテクストを保持しておくって実装をエイヤコラと書かねばならん。とっても大変だと思うんですが。というか、普通の言語だとそこまで言語自身で制御できないのが多いんじゃないかな。
Z80で実装できるって話が出てたけど、むしろZ80ならできるでしょう。Cでもできるかもしれない……んーインラインアセンブラの力を借りないと難しいか? 普通の言語だと実行コンテキストなんてものは隠蔽されてしまっているから触れないから、非常に難しいと思うなぁ。
あ、Smalltalk ならコンパイルされたバイトコードを手で触れるので、こういう実装は得意……だそうです。受け売り ;-P
つことで、ぼくが「概念」として持っている「継続」は言語の上で実現するのは非常に難易度が高く感じるので、そこまで大げさな話じゃないでしょう? というのが、「木構造のイテレーションだって継続なんか要らない」という主張なんです。
もちろん、「止めてたものを再開できる仕組みなら全部継続」という立場もあるでしょう。でもそうしたら、配列のindexを内部状態として保持する、という実装を「継続」と呼ばない理由がない。「木構造には継続が必要」なんではなくて「イテレートにはすべて継続が必要」ということになります。当たり前すぎますね。
それが正しいのであれば……そんな当たり前なものに名前つけて「概念」だなんていうのは大仰というかもったいないなぁ、というのが私の率直な感想。
という話を140文字で表現しようとした私が悪かったです。猛省しております……。
おわりに
参加して議論に加わってくれた皆さんありがとうございます。
ぼくは Java はぜんぜんわかんないんで、色々勉強になりました。
ただ時間に余裕があるなーと思うと脱線してしまう傾向があるので、2章ぐらいずつやってもいいかなとぼくは思いました。
それとついったでギスギスした雰囲気にしちゃったことは本当に反省しています。
これに懲りず、次回も楽しく議論できればな(できれば脱線方向ではなく、基本に立ち返る方向で ^^;) と思います。