Rails勉強会@東京第5回 に行ってきた。前回はプロジェクトが既に遅れ気味にて参加できなかったので2ヶ月ぶりとなる。
小雨の振る中、秋葉原駅前というか、雨を避けてダイビル前に寄ったあたりに集まった。怪しい集団が飲物片手に集まっているので、ダイビルの守衛さんに「敷地内飲食禁止だよ」と怒られる。でも、どこまで公道でどこから敷地なのか不明。
今回は勉強会・懇親会ともに参加者が過去最高ということである。大阪の勉強会の大規模さと本格さには及ばないけれど、東京も盛り上がってきた。
毎度おなじみ、前半・後半に分けてそれぞれオープンスペース形式を採った。
前半
3つのセッションに分かれた。
私は、「『たのしいRailsレシピ』(仮)打ち合わせ」に出た。ソフトバンク・クリエイティブから出版予定のRailsレシピ本のネタ出しである。もろはしさんと高橋会長が著者ということでok? 著者・編集者のみなさまが出した章立てとネタを見ながら、みんなで「こんなレシピも欲しい」とか色々いいたいことを言って案を出していった。
Pragmatic Bookshelfの本 も出るけれど、 Ruby-GetText とか スペジェネ とか、日本ならではの話題も盛り沢山。標準のvalidationセットは日本における開発の要求に応えるにはまだ不足とかいう意見もあったので、その辺も載るかもしれない。
Pragmatic Bookshelfの本はAWDwRを読んでrailsを使ったことがあるぐらい層を中心に応用技を教えるのに対して、こちらのレシピはRubyについてもあんまり知識は要求せず、web開発の経験があるとかそれぐらいの知識を前提に、railsのあれこれを易しく紹介していく。
私ゃさんざん色々言わせてもらったけれど、中でも特にお願いしたいのは「高橋会長の実演写真付き『 正しいポーズ でスペシャルジェネレーションするには』」。
Railsはフレームワークがカバーする領域が広いし、比較的使ってる層であるはずの勉強会常連でも、他の人のレシピ提案を聞いて「それ知らなかった」というのが結構あった。とても勉強になった。出席者がアクティブに発言しすぎて時間内ではインストール、ジェネレーション、ActiveRecordまでしかチェックできず、残りは懇親会にて行った。入門的であることを配慮しながらも非常に内容が濃いので、出版が楽しみである。
今秋に出版予定ということなので、皆さん買いましょう。勉強会@東京の知恵が詰まっています。
他のセッション
Rails 1.1の新機能
Cascades Eager Loadingすごいらしい。pagenationで使える。rakeコマンドが変わつたり追加されたりしたらしい。
AWDwR chap.2
訳が素晴らしいと。声に出して読みたいRailsと。本当はpart 2のつもりで提案されたのに、間違ってchap.2と題してしまったためにすぐ終わってしまったらしい。
後半
4つのセッションに分かれた。
私はActionWebServiceセッションのオーナーになった。数日前に終わったプロジェクトの方で、LLによるSOAP実装の候補としてRailsのActionWebServiceも評価した。 そのときのレポート も公開してある。
報告のまとめ
まず、レポートを下敷にそのときの評価結果を報告。WebServiceについて、実際の開発経験はない人もいたので、その辺を軽く捕捉しながら報告した。 下記はその内容。前述のレポートも併せて御覧ください。
開発規模と期間、システム負荷から考えてLLを選択
Perl5ならもちろんCPANを見る
- PHP5なら標準関数とかいろいろ有るけれど、高機能なのはPEARのSOAP(beta)
Rubyだとsoap4r
でも、これらは動的型のため、型情報を補わないとSOAPにうまく載せられない。
そもそもLL界隈があんまりXML好きでないのも手伝って、バグもありがち。
互いの生成するメッセージをパースできなかったり、非互換性もある。Axisに対する互換性は意識していてもLL同士はあまり意識していないのかも。
WSDL を手で書くのは死ぬ。
- ActionWebServiceは型補足のためにDSLを取り入れて、比較的簡単に書けるようになっている。
- メソッドコールの方法としてのSOAP/XML-RPCと割り切って、WSDLの柔軟性やら何やらは端折る。その割り切りのセンスによって、現実の開発で使い易くなっている。
WebService用のscaffoldで、自動生成されるフォームを使ってブラウザからWebサービスをcallできるのは大きい。
しかも、request, resposne, return valueがきれいに表示される
それからAWDwR chap.20を順に読んでいった。
- ActionWebService、略してAWS
- 同一のコードでSOAP/XML-RPC両方をサポートするサーバーを書ける
- SOAP/XML-RPCの完全な実装ではない。あくまでも、これらのプロトコルを、メソッドコールを外部システムに公開する手段と割り切っている。
- Apiクラス内で、DSLっぽく外部インターフェースを定義
- Controllerまたは専用のServiceクラス内で外部インターフェースを実装
API定義DSLの仕組み
動的型言語において、どうやってSOAPなどに必要な型情報を捕捉するかは問題であるが、ActionWebServiceではDSLで解決する。
一揃いののAPI(SOAPでいうportに対応)は ActionWebService::API::Base
のサブクラスで表現される。このサブクラスの中で api_method
クラスメソッドを利用してメソッド型定義を宣言する。
class HogeApi < ActiveRecord::API::Base api_method :foo api_method :bar end
メソッドの引数や戻り値やの型は api_method
にオプションを付けて表現する。利用できる型は次の通り
- XSchemaの基本データ型の一部: これらはAWSのDSL内では
:string
,:int
,:datetime
のようなシンボル値で表現する ActiveRecord::Base
のサブクラス: これらはDSL内ではClass
オブジェクトそのもので表現する- 構造化型: 利用できる型をメンバーとする任意にネスト可能な構造体。
ActionWebService::Struct
を継承したClass
オブジェクトで表現する。 - 配列型: 利用できる型をメンバーとする配列。DSL内では型表記を唯一のメンバーとするような配列で表現する
# 整数計算API class CalcApi < ActiveRecord::API::Base # 加算 api_method :add, :expects => [:int, :int], :returns => [:int] # 減算 api_method :sub, :expects => [:int, :int], :returns => [:int] # 約数を列挙 api_method :divisors, :expects => [:int], :returns => [[:int]] end
Apiクラスは外部にAPI定義を示すという意味で、Javaのインターフェースに近い存在である。Javaで書くと大体こんなインターフェースと等価である。
interface CalcApi { int add(int param1, int param2); int sub(int param1, int param2); int[] divisors(int param1); }
ここで、AWSのAPI定義にはパラメータ名が含まれていないことに気づく。パラメータ名を設定するには単項の Hash
を使う。
# 整数計算API class CalcApi < ActiveRecord::API::Base # 加算 api_method :add, :expects => [{:lhs => :int}, {:rhs => :int}], :returns => [:int] # 減算 api_method :sub, :expects => [{lhs => :int}, {:rhs => :int}], :returns => [:int] # 約数を列挙 api_method :divisors, :expects => [{:base => :int}], :returns => [[:int]] end
これで、次と等価となる
interface CalcApi { int add(int lhs, int rhs); int sub(int lhs, int rhs); int[] divisors(int base); }
言語内に型定義の専用語彙を持つ静的型言語に比べればいくらか落ちるが、それでもRubyのDSLを生み出す力を最大限に引き出して、動的型言語としては比較的綺麗にAPIを定義できていると思う。増してWSDLを手で書くのに比べれば100倍楽だ。
上の例では構造化型などを使っていないが、 ActiveRecord::Base
のサブクラス、つまるところRailsアプリケーションにおけるModelクラスをそのまま返り値にできるのは便利だ。内部モデルとAPI側のデータ構造が本質的に同一である場合、わざわざ^「詰め替え」をしなくて済む。
Javaなら内部モデルが「たまたま」外部インターフェースに表れる構造型と同等な場合でも、疎結合性を保つために別のinterfaceを定義することを検討するだろう。しかし、ここで動的型言語とWebServiceの利点が活きて、メンバー構造さえ保てばそれが実際にどういう型であるかは問題にならないので(Duck typeだね)、Modelクラスをそのまま外に出しても結合は密にならない。いくらRailsが密を是とするフレームワークでも、外部システムと密なのは一般には嬉しくないものね。
AWDwRには明記していなかった気がするが、前掲の私のレポートにある通り、 ActiveRecord::Base
のサブクラスは引数には指定できない。指定したら"ActiveRecord model classes not allowed in :expects"と言われた。これは残念。
generation
上のようなAPI定義の雛型を作るためのgeneratorが標準で用意されている。次を呼ぶ。
$ script/generate web_service ApiName [method_name, ...]
これにより、 <var>ApiName</var>
Api(API定義クラス)、 <var>ApiName</var>
Controller(API実装クラス)、 <var>ApiName</var>
ApiTest(API機能テスト)が生成される。
$ script/generate web_service Calc add sub divisors => create app/apis exists test/functional create app/apis/calc_api.rb create app/controllers/calc_controller.rb create test/functional/calc_api_test.rb
生成されたあとは、
- メソッド定義に
:expects
と:returns
を設定 - コントローラーのメソッド実装にも、対応するパラメータを指定
- メソッドの中身を実装
:expects
と :returns
のほうはよいだろう。次に、生成された段階では CalcController
はこんな感じなので、
class CalcController < ApplicationController wsdl_service_name 'Calc' def add end def sub end def divisors end end
これに、パラメータを足して実装する。
class CalcController < ApplicationController wsdl_service_name 'Calc' def add(lhs, rhs) lhs + rhs end def sub(lhs, rhs) lhs - rhs end def divisors(base) (1..Math.sqrt(base).to_i).inject([]) { |divs, candidate| div, mod = base.divmod(candidate) if mod == 0 divs << candidate divs << div unless div == candidate end divs } end end
これでおしまいである。ポイントは、パラメータを ActionController::Base#params
ではなく、普通に受け取ること。
scaffold
素晴らしいのは、scaffold機能である。上のControllerクラス定義に次の宣言を足す。
web_service_scaffold :invoke
:invoke
というのは任意に指定できる、scaffold画面にアクセスするためのaction名である。これで、 script/server を起動して http://localhost:3000/calc/invoke にアクセスしてみよう。
メソッドの一覧がででくる。そして、試しにaddメソッドを選択してみるとパラメータの入力フォームが出でくる。
これをsubmitするとrequest, resposneのXMLダンプ付きで結果が表示される。
結果が配列でも同様。
もちろん、実際の開発ではサブコンポーネントの単体テストをまずしっかりやってからなのだけれど。でも開発してるAPIを試せるいい感じのGUIが、こうやって只で手に入るのは嬉しい。Webサービスを開発した経験があれば いいもの使ってる感 があるだろう。
このscaffoldingの仕組みは純粋に開発中のテストのためのものなので、運用時にはweb_service_scaffold宣言を削って外してしまう。このあたり、ActionViewなんかのテンプレート生成よりも"scaffold"っていう意味合いがより明確だ。本当に構築作業用の「足場」なのね。
Dispatchingとか
今は、Controller内にベタにAPIを実装してしまった。でもこの方法には一つ問題がある。1つのweb-serviceエンドポイントURLに1つのAPIしか配置できないのだ。RailsのRoutingの仕組みでは、1つのPATH_INFO componentは1つのコントローラーにマップするしかなくて、上のやりかたでは1つのコントローラーには1つのAPIしか実装できないからだ。
ジェネレーターがデフォルトで生成する上のようなやりかたは"Direct dispatching mode"と言って、AWSが提供する3つのやりかたのうちの1つである。この他に"Layered Dispatching mode"と"Delegated dispatching mode"がある。
"Layered dispatching mode"では、1つのコントローラーで複数のAPIをサポートできる。変更点すべき点は、
- サポートするそれぞれのAPIにつき、
ActionWebService::Base
を継承したクラスを作成する。コントローラー内でAPIを実装する代わりに、このクラスで実装を提供する。 - コントローラークラスで"
web_service_dispatching_mode :layered
"を宣言する - コントローラークラスで、API名と
ActionWebService::Base
の具象インスタンスの対応を宣言する。
例えば、controllerクラスが
class CalcController < ApplicationController wsdl_service_name 'Calc' web_service_dispatching_mode :layaered web_service :calc, CalcService.new end
で、serviceクラスが
class CalcService < ActionWebService::Base def add(lhs, rhs) lhs + rhs end def sub(lhs, rhs) lhs - rhs end def divisors(base) (1..Math.sqrt(base).to_i).inject([]) { |divs, candidate| div, mod = base.divmod(candidate) if mod == 0 divs << candidate divs << div unless div == candidate end divs } end end
配置とか
- 構造体型の定義は、これはAPIの一部であると考えて app/apis の中が良いのかな。
Serviceクラスの定義は、私はcontrollerに近い存在だと思うから(あと、generatorが作ったcontrollerのファイルをコピーして書いたので) app/controllers に置くのがいいんじゃないかと思う。
app/services っていうのを作って
$LOAD_PATH
に追加するのもアリではないかという意見も
公開とか
上の例で、 http://localhost:3000/calc/service.wsdl にアクセスすればSOAP用のWSDLが手に入る。 生成されたもの を見てみてほしい。これを手で書くと大変だけれど、DSLの力で上のAPI定義だけで生成されるのだ。
XML-RPCの場合は http://localhost:3000/calc/api がエンドポイントとなる。
テストの自動化とか
AWSのテストをサポートする機能もしっかり提供されていて、generatorでweb_serviceを生成した時にはテストケースのスケルトンも生成される。
controllerのテストでgetやpostを呼んでアクションのの挙動をテストできるように、テストケース内でinvokeというメソッドを呼ぶことで公開されたメソッドの挙動をテストできる。
クライアント機能とか
一応、AWSにもクライアント機能はある。 ActionWebService::Client::Soap
と ActionWebService::Client::XmlRpc
だ。しかし、 new
にAPI定義クラスを渡さないといけない。つまり、外部システムのインターフェース定義をAWSのDSL形式に変換しないと理解してくれないのだ。外部システムのためにわざわざAPI定義を書き下すのは面倒なので、この点だけはsoap4rの方が便利そうに思える。
AWSのクライアント機能はRails同士をWeb serviceでつなぐ場合のため、ということだろうか?
script/generate wsdl2api ができてもいいのにね。という御意見あり。ごもっとも。soap4rのwsdl2ruby.rbのラッパーでもいいから、generatorが欲しい。
やってみた
そんな感じで、みんなで実際にAWSを試してみた。
- scaffoldのパワーに感嘆する。
- Calcを作ってみた。
web service未経験だった人が、既存のrailsアプリケーションの機能を20分でWebサービスとして公開できた。
駅名検索API凄い。
- AWSを流行らせれば有用なAPIがどんどん公開される予感
結論
Rails使いの皆さん、初めてscaffold generatorを使ってみたとき、初めてスペジェネを使ってみたとき、初めて色つきログを見た時のあの気持ちを思い出してください。
- 「おぉすごい」
- 「ここまでやるか。アホちゃうか」
- 「いいものつかってるぞ」
- あの気持ちをWebServiceの世界で今一度味わえます。
大規模開発ならSOAP周りも充実しているしJavaや(C++や)ASP.NETが現実的でしょう。でも、中小規模のWeb Service開発をアジャイルにやるならActionWebServiceでしょう。
他のセッション
俺と一緒にRecipe28を読まないか
かくたにさんのセッション。Pragmatic BookshelfのほうのRails Recipesのβ版を読んだらしい。レシピが増えたため、かくたにさんの想定していたレシピは当日の最新バージョンではrecipe 43になっていたとか。
Rails 1.1で導入されたintegration testはすごそう。
- unit testはmodelクラスをそれだけでテストする。
- functional testはcontrollerを直接呼び出すことでcontrollerがmodelをコントロールする様子をテストする。
- integrationテストは、Routingまで含めたRailsフレームワーク全体の挙動を呼び出して、controllerをまたがった処理、セッションの状態に依存する処理をテストする
しかも、Integration testのテストケースは、ほとんど英文を読んでいるかのような自然なDSLで書かれるらしい。 Bob.login.as.administrator
とか。
Gruffライブラリ
オーナーはもろはしさん、だったかな?
GruffやらRMagicやらCSS-graphやら、いろいろ面白そうなグラフライブラリを比較して、Railsで利用することを検討したらしい。途中でささださんが乱入して盛り上がったとか。
RJSさらにくわしく
前回のRJSセッションを引き継いでさらに詳しく、らしい。
- RJSは、Rubyコードを書くとJavaScriptを生成して、そのスクリプトがDOMを通じてブラウザの画面をいじくる仕組み。
- RJSを使ったページの挙動を調べるには、JavaScriptが弄った後のソースを見られないと不便。そのためにはFirefoxのView generated sourceプラグインが便利、らしい。
view generated sourceってどこだろう? addons.mozilla.orgやgoogleでは見付からなかったんだけど。 このブックマークレット だろうか。
懇親会=宴会
一次会では、昨今Rubyistが集まれば必ず始まるHaskell話とか、レシピ本ネタ出しの続きとかをした記憶がある。二次会の記憶がほとんどないのは何故だろう。そんなに飲んでないのに。
入門Haskell を読んで、ようやく数学概念とHaskellの概念がすっきり結び付いた。 <-
が∈のことだと分かってまずすっきりした。クラスはそのままクラスで良いよね。モナドはそのままモナドでいいよね。でも、モナド律がどうやってIOの挙動に結び付くのかよく分かってなかった。その辺、しっくりきた。数学の世界までやってくれば、あとはホームグラウンドだ。
とか、そんな話をしたり。Railsで20分でWikiを作ってしまった方の話を聞いてびっくりしたり。
ささださんに「『ぱるま』の続ききぼんぬ」と言ったら「そんな暇あるわけない」と言われた。