Rails勉強会@東京 第10回

Rails勉強会@東京 第10回 に行ってきた。

今回もドリコムのオフィスを貸していただいての開催。

前半

なんか、いっぱいセッション案が挙がってて、一部、すぐに終わっちゃいそうなネタもあったので統合してこんな感じのセッションを行なった。

私はRIA/Ajax/Cometのやつに出た。この辺の最近の動向についてのりおさんに色々お話を伺った。このセッションは Rails Chat にて 生中継 を行った。

Ruby on Rails RIA SDK by Adobe

なんか期待されて話題になってるけど、たいしたものではない。SDKと銘打ってはいるもの、なんか自前のライブラリを提供している訳ではなく、ただの接続チュートリアル

Railsをバックエンドにして、UIはFlex2で作成という、誰もが考えそうで実際にいくつかの案が出ているのをAdobeブランドを関してまとめただけ。とくに接続が便利になるとかそういう訳ではない。

ただ、Rails 1.2のActionController#respond_to{|format|}を使うと、HTML版とFlex版を柔軟に切替えられて良い。これからはそういうのが便利かもしれないとのこと。

ちなみに、Flex2の開発環境は日本語版も出たところ。EclipseベースのIDEがあって、体験版を無料で利用できる。買うと6万円くらい。

Flexの競合製品

そもそもRIA自体がAjaxに食われた感があると、のりおさん。そういう感覚は私も持ってた。他の出席者にも大体共有されてた模様。 リッチクライアント ソリューションカンファレンス とか、リッチクライアント製品への期待が最高に高まったところにAjaxが来ちゃったからね。

あと、競合製品としてはOpenLazloとか? 私はCurlが好きですが、何か? LLDNCurl団扇を貰って喜ぶぐらいCurlに期待してますが、何か? でも、「Curlは解説書見た時点で、これはダメだと思った」とも言われてしまった。課金モデルが変わったり、サーバー環境が高いバージョンしかなかったり、敷居が高い面はあったよなー。最近ようやく 個人向け無償版 が出たけど、遅すぎたかも知れない。個人の利用を通じて開発者層を形成するのが大切なのにね。これをもっと早くやってたらね。AdobeにせよMacromediaにせよ競合がたくさんいるところに進出するには少々戦略がまずかった気もする。

あとはXUL? XULのいいところはpiroたんが萌えキャラなところだと思ってる。

Cometって何さ

のりおさんによるCometの解説。

  • クライアント側主導のRequest - Response型通信を、無理矢理サーバーpush型にする。

  • クライアントの開いた通信路を必要になるまで開いたままにして、サーバーは必要なときにそれを使ってクライアントにメッセージをpush

  • クライアントはメッセージを受けたらすぐさま繋ぎ直す。これで常時サーバーとの間に通信路が開かれているも同然

  • パフォーマンスを考えなければ実装は簡単

  • パフォーマンス面で、セッション数が爆発しないように実装を工夫する必要がある。

  • Rails ChatではWebアクセス用とは別に専用のpushサーバーを持ってる。

  • 複数人で連携して何かをやるのに便利。

  • Chat

  • 同時編集Wiki
  • 同時編集スプレッドシート
  • 同時編集ホワイトボード

RailsChatの場合、pushサーバーへの接続にはFlashのXMLSocketを使ってソケット通信してる。XMLSocketは実はFlash 5の頃からある枯れてる機能。

資料をいくつか。

ちなみに、AjaxもCometも洗剤の名前から来てるそうだ。SOAPのパロディで。

Ajaxとかのライブラリ

Ajaxライブラリの利用調査 がある。一番使われてるのはやっぱりprototype.js

各種ライブラリを混用すると、大抵は問題なく動くもののとてつもなくメモリーを食う。……といっても、所詮は1ページあたり2MB程度なので、開きなおってMoo.fxもRicoも入れてなんとかならなくはない。ただし、開発環境ではさくさく動くのにお客さんの環境では重かったりして泣くことになるかもしれない。

色々なひとから出たコメント。

  • MochiKitは速いし、書くのも楽
  • jQueryは面白い

  • デモサイト が良い

  • Railsで使うのための jQuery on Rails もある。
  • なんでQueryなのかは謎につつまれている。最初クエリ言語かと思った人が多数
  • 拡張されていく入力欄 が素敵。ブログのコメント入力欄のように、初期状態であまり場所をとってほしくないけれども書くときは広いほうが嬉しい類のケースに良いかもしれない。
  • ThikBox も良い感じ。

  • でも、あまりにもダイナミックなUIは好みが分かれる。"It's cooool"と思ってても「使いにくい」と切り捨てられたりする。

他のセッション

  • Rails時代のRuby入門(第1回)

    ひととおりいろいろやったらしい。

  • Prelude, Selenium

    すぐ終わったらしい。 PreludeRubyHaskellライクな表記を実現するライブラリで、まだまだ発展途上という評価らしい。Seleniumは、 前回 よりも少しだけRailsよりの所を扱ったけれども、基本的には前回と同じ内容みたい。

  • Rails 1.2を先取り

    1.1.6以降のChangelogを淡々と読んだけれども、大きな変更は無いらしい。

後半

3つに分かれた。

私はAction Web Serviceのオーナーを務めさせていただいた。

勉強会の第5回 でAction Web Serviceを触ってみるのは一応やってあるけれども、今回はじゃあ中身を1つ見てみようという催し。

私のマシンの画面をプロジェクタに写せなくて、参加者のみなさんには少し不便を強いてしまった。申し訳ない。お詫びに、もし希望する方がいたら次回もう少し丁寧にやり直します。 shachi さんからは前回、「MacユーザーはVGA変換アダプタを買って持ってきなさい」という忠告をいただいていたのに土壇場まで忘れていて入手が間に合わなかったのが敗因。

エントリーポイント

ActionWebServiceのソースを見てみると、action_web_service/dispatcher/action_controller_dispatcher.rbというそのまんまの名前のファイルがある。 これを見てみると、ActionWebService::Dispatcher::ActionControllerというモジュールがある。このモジュールにはself.includedも定義されている。拡張機能をモジュールで定義してあとでincludeというRailsの慣習からすると、やっぱりこれがエントリーポイントらしい。これをあとでどこかでActionController::Baseからincludeするんだなというのはなんとなく想像がつく。

ActionWebService::Dispatcher::ActionController::included(base)を見ると、その中でやっぱり色々機能を足してるのが分かる。

class << base
  include ClassMethods
  alias_method_chain :inherited, :action_controller
end

モジュールを元にクラスメソッドを足すのはRailsのコードではおなじみ。

base.class_eval do
  alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke
end

これは何やってるんだろうね。取り合えず、web_service_direct_invokeに何かフィーチャーを足してる可能性はあり。

base.add_web_service_api_callback do |klass, api|
  if klass.web_service_dispatching_mode == :direct
     klass.class_eval 'def api; dispatch_web_service_request; end'
  end
end

エエェェ(´д`)ェェエエ。必要もないのに文字列evalは止めようよ。しかも、ですよ。ActionWebServiceを使ったときにAPIコールのためのエンドポイントになるのは /<var>controller_name</var>/api というpathなのだけれど、そのpathに対してHTTP requestを掛けるとWebService呼び出しになるっていうのは、別にマジックでも何でもなくて、ただ単に、「そういう名前のアクションメソッドを勝手に定義してくれてる」だけってことね。

「それって何かでセキュリティ的にまずかったりするケースがないのか?」と危惧する人多数。

base.add_web_service_definition_callback do |klass, name, info|
  if klass.web_service_dispatching_mode == :delegated
    klass.class_eval "def #{name}; dispatch_web_service_request; end"
  elsif klass.web_service_dispatching_mode == :layered
    klass.class_eval 'def api; dispatch_web_service_request; end'
  end
end

AWDwR や第5回のレポートでも触れてるdispatching modeによって切り分けしてる。でも、この程度の定義に文字列evalはやっぱり止めてほしい。

これを括ってるadd_web_service_api_callbackやadd_web_service_definition_callbackの中身はセッションでは追わなかったけれども、なんか面白そうだからあとで書く。

base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods)

で、シメはやっぱりモジュールのincludeによる拡張。

API呼び出し

さて、上によって定義された <var>YourController</var>#api というアクションによって、dispatch_web_service_requestが呼ばれるということは分かった。じゃあ、そこから先は?

同じaction_web_service.dispatcher/action_controller_dispatcher.rbの下のほうに定義がある。 ActionWebService::Dispatcher::ActionController::InstanceMethods#dispatch_web_service_request ね。

exception = nil
begin
  ws_request = discover_web_service_request(request)
rescue Exception => e
  exception = e
end

まずは、普通のControllerで使ってるリクエストオブジェクトを、ActionWebServiceの世界に固有のリクエストオブジェクトに変換してる。プロトコル(XML-RPC/SOAP)依存の処理が入るのはここのところがメイン。で、ここで例外を捕捉して保存してるのはなぜかというと、下のほうを見ると分かる。

if ws_request
  # 正常系の処理
 else
  exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message")
  log_error(exception) unless logger.nil?
  send_web_service_error_response(ws_request, exception)
 end

あー、そうね。エラーメッセージを返してるんね。で、省略してしまった正常系のところは、ログを取ってるのと、例外発生時にはやはり同様に捕捉してるのをのぞけば、

  • ws_response = invoke_web_service_request(ws_request)
  • send_web_service_response(ws_response, bm.real)

これだけ。 bminvoke_web_service_request の処理時間を計ってるBenchmarkオブジェクトだけど。なんで計ってるんだろ。

プロトコルの特定と解析

さて、これ以上先に進む前に、さっき流してしまった discover_web_service_request(request) を見てみる。このメソッドはaction_web_service/protocol/discovery.rbにある。

(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
  protocol = protocol.create(self)
  request = protocol.decode_action_pack_request(action_pack_request)
  return request unless request.nil?
end

ここで、read_inheritable_attributeってなんぞ、と一瞬焦ったけれども、舞波さんが解説してくれた。 RoR Wikiにも説明がある 。本当に名前そのまんま、継承される属性なのね。

クラス変数はその変数を定義しているクラスだけじゃなく、サブクラスからもアクセス可能。だけれども、クラス階層を通じてオブジェクトを共有しているので、サブクラスでその値をいじってしまうと親クラスにまで影響してしまう。

じゃあサブクラスごとに値を持てばいいんでしょ、と思って「Classオブジェクトのインスタンス変数」を使う手も有る。でも、これの問題はそのままではサブクラスから親クラスの属性にアクセスできないこと。サブクラスのClassオブジェクトと親クラスのClassオブジェクトは当然オブジェクトとしては別だから。

まぁ、この問題に対処する方法はいくつかあるけど、その辺をよしなに処理しておいてくれるのが(read|write)_inheritable_attributeだそうな。あー、完全に忘れてたよ。ありがとう舞波たん。

さて、そういうわけで、"web_service_protocols"属性に対してeachを回す。中に入ってるのはActionWebService::Protocol::AbstractProtocolのサブクラスのClassオブジェクトね。デフォルトではSOAPとXMLRPC。

protocol.create(self) で、上手いことインスタンス化する。それで、protocol.decode_action_pack_request(action_pack_request)で、そのプロトコルのメッセージとしてparseしてみる。それで、うまいことparseできたらその結果を返す。

じゃあ、decode_action_pack_requestはどうなってるの? SOAPは関連ファイルがいっぱいあって、ややこしそう。なのでXMLRPCのほうを見てみる。

XMLRPCの解析

action_web_service/protocol/xmlrpc_protocol.rbを見る。ActionWebService::Protocol::XmlRpc::XmlRpcProtocolクラスが定義されてる。

まず、さっき protocol.create(self) として呼んでたやつの実装がある。

def self.create(controller)
  XmlRpcProtocol.new
end

本当にインスタンス化してるだけ。でも、本当は self.new のほうが望ましいけどナー。

で、decode_action_pack_requestのほうは、というと

def decode_action_pack_request(action_pack_request)
  service_name = action_pack_request.parameters['action']
  decode_request(action_pack_request.raw_post, service_name)
end

raw_postで、生のHTTP Request bodyにアクセスしてる。ふーん。次に行こう。

def decode_request(raw_request, service_name)
  method_name, params = XMLRPC::Marshal.load_call(raw_request)
  Request.new(self, method_name, params, service_name)
end

あー、ここでファイルの最初を見ると

require 'xmlrpc/marshal'

XMLRPCメッセージのMarshalには標準添付ライブラリのやつを使ってるのね。

API呼び出しの実行

というわけで、requestをparseするところまでは読んだので、上に戻って、parseに成功した正常系の場合にどうやって実行されるのかを見てみる。今、parseして作成したリクエストオブジェクトを変数ws_requestに代入しておいて、

ws_response = invoke_web_service_request(ws_request)

しているのであった。

invoke_web_requrestはaction_web_service/dispatcher/abstract.rbにある。

def invoke_web_service_request(protocol_request)
  invocation = web_service_invocation(protocol_request)
  if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
    xmlrpc_multicall_invoke(invocation)
  else
    web_service_invoke(invocation)
  end
end

protocol_requestをここで更にinvocationに変換しなければならない理由がよくわからないんだけど、とにかくinvocationというのはこのファイルの下の方で定義されているActionWebService::Dispatcher::InstanceMethods::Invocationのインスタンスだ。

class Invocation # :nodoc:
  attr_accessor :protocol
  attr_accessor :protocol_options
  attr_accessor :service_name
  attr_accessor :api
  attr_accessor :api_method
  attr_accessor :method_ordered_params
  attr_accessor :method_named_params
  attr_accessor :service
end

ようするに、呼び出しに必要な情報を束ねた構造体のようだ。

  • protocol: protocol_requestを構築した段階で既にSOAPかXMLRPCかは分かっている。その情話法を格納しておく。ActionWebService::Protocol::AbstractProtocolのインスタンス
  • protocol_options: HTTPヘッダなどの、付加情報
  • service_name: SOAPならば"SoapAction" HTTPヘッダフィールドから、XMLRPCならばXML中の対応する属性から取得する。
  • api: ActionWebService::APIインスタンスDSLで定義されたAPI定義を表すオブジェクト。これはdispatching modeによって取得方法が違って、Direct dispatching modeではcontrollerの定義で宣言してあるから、controllerクラスのinheritable attributeから取り出せば良い。dispatching modeがdelegated, layeredの場合は、実際にAPIを実装しているオブジェクトをcontrollerが知っている訳なので、そのオブジェクトに問い合わせればよい。
  • api_methodには、呼び出されるAPI実装を表すActionWebService::API::Methodオブジェクトが入る。これ、セッションのときは組み込みのMethodオブジェクトかと思ったら、よく見たら違うのね。API宣言クラスでapi_method :name, :excepcts => [...], :returns => [...] を呼んだときに内部に構築されるオブジェクトで、メソッドの名前と型情報が入ってる。
  • method_ordered_paramsは、api_methodが持ってる型情報に応じて変換された引数の列
  • method_named_paramsは仮引数名をキーに、実引数を値に持つHash
  • service: このAPIの実装を提供しているオブジェクト。direct dispatching modeではコントローラー自身。delegated, layeredの場合は転送先のオブジェクト

さて、そんな感じで、invoke_web_service_request(protocol_request)メソッドでは、invocationを構築した訳だ。で、次の行はktkr!

if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)

こういうコードがあと何ヶ所かあったんだよね。is_a?で分岐してる類のが。そこはAbstractProtocolのpolymorphismでなんとかしてほしい。そうでないとSOAP, XMLRPC以外のプロトコルを足すのが難しくなってくる。もうね、 GoF を100回読めと。せめて、respond_to?で分岐して欲しい。

XMLRPCのmulticallだけ特別扱いしてる模様で、よくわからないけど、とりあえず「普通」のほうを見ることにする。「普通」の場合に呼ばれるのは

ActionWebService::Dispatcher::InstanceMethods#web_service_invoke(invocation)

で、ここでもまたdispatching modeによる場合分けをしていて、directならはweb_service_direct_invokeを、delegatedやlayeredならばweb_service_layered_invokeを呼んでる。

def web_service_invoke(invocation)
  case web_service_dispatching_mode
  when :direct
    return_value = web_service_direct_invoke(invocation)
  when :delegated, :layered
    return_value = web_service_delegated_invoke(invocation)
  end
  web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value)
end

んー。なんかこれも、dispatching modeはinvocationを構築したときの場合分けで既知なわけだし、これこそ「呼び出しの内容」の実装の詳細だから、invocationのpolymorphismで対処してほしいなぁ。

まぁ、それで、web_service_direct_invokeの場合はちょっとした処理が入るけれども、どちらも最終的にはActionWebService::Dispatcher::InstanceMethods#web_service_filtered_invokeに帰着する。これも、例外処理やコールバックの呼び出しを除けば、invocation.serviceにsendしてるだけ。かくて、呼び出しができたのであった。

そして、最後はweb_service_create_responseを呼ぶ訳だ。これも、こまごまと色々してるけれども、容易に想像がつくように、やりたいのはprotocol実装に返り値をmarshalさせることだけ。このあたりは時間がなかったので少々駆け足でとばしてしまった。

Include

そういえば、最初に見たようにActionWebServiceの機能はModule#includedを通じてコントローラーに提供されてる。じゃあ、そもそも関連モジュールがIncludeされるのはいつなんだろう。method_missingでも捕まえて、webサービス機能関連の宣言をするとincludeされる仕掛けでもあるんだろうか。

トップレベルのディレクトリにあるaction_web_service.rbを見ると書いてあった。

ActionController::Base.class_eval do
  include ActionWebService::Protocol::Discovery
  include ActionWebService::Protocol::Soap
  include ActionWebService::Protocol::XmlRpc
  include ActionWebService::Container::Direct
  include ActionWebService::Container::Delegated
  include ActionWebService::Container::ActionController
  include ActionWebService::Invocation
  include ActionWebService::Dispatcher
  include ActionWebService::Dispatcher::ActionController
  include ActionWebService::Scaffolding
end

ちょww 無差別wwww

他のセッション

懇親会

ここしばらく同じ店で懇親会やってたので、ちょっと飽きが来はじめるころ。そのへんを考えてか、今回はほかの店にしてくれた。

  • もろはしさん、ご結婚おめでとうございます。
  • のりおさんのデザイナ向けRails本は遅れ気味
  • Rails和レシピ本も遅れ気味

  • レビューは喜んでお引き受けしますよ

  • ABD: Activity系とEvent系の違いがちょっとだけ分かってきたかも? マクタガートの時間論に言う2つの時制の違いに近いかも。でも、概念としての違いは分かったのかもしれないけど、それを結合しちゃいけない理由は未だに分からない。

  • Rubyの多値代入/多値returnの件って、前にMultiValue < Arrayとかいう案が上がってたけど、もう少し汎用的にTupple < Arrayじゃだめかね。Symbol < Stringが許されるならこれもいいんじゃないか?
  • 先日私が 触ってみたNTTデータマスカット 、違う意味で大受けだった。

  • なんていうかさ、外部の技術者の自発的な協力を得て発展させて、デファクトスタンダードを目指そうっていうなら、少なくとももっとプロジェクト体制をオープンにしないと受け入れられないよね。

  • そもそもが、"みかか"という負のブランドイメージを背負ってるんだから、せめてもう少しオープンさのアピールがないと。
  • でも「負のブランドイメージって、内部からはなかなか見えないんだよね」と、もろはしさん評。
  • まぁ、ひょっとしたら、現場は分かってるに上の頭が固くてあれがギリギリ頑張った結果という可能性もあるけど。NTTデータにもスーパーハカーがいるのは知ってるしね。
  • とにかく、頑張れマスカット。