リファクタリング - Rubyエディション読書会(1)
cuzic です。
「リファクタリング - Ruby エディション」読書会を主催してきました。
「リファクタリング - Ruby エディション」は、かの名著 Martin Fowler のリファクタリングをサンプルコードを Ruby で書き換え、説明しなおすという野心的な試みを行っている書籍です。
その読書会を行った結果について、簡単に書くと、
- 結構人が集まった、(10人以上)
- みんないいペースで一章を読み終えることができた。
- わりとなごやかで発言しやすい雰囲気で進めることができた。
- 懇親会を非常に安価で実現できた。(近くにショッピングモールがあると便利)
- リファクタリング - Ruby エディションはいろいろと不具合が多い書籍
というようなかんじでした。
「リファクタリング - Ruby エディション」には、下記の修正すべき点がありました。
- コードのインデントが一部ずれている
- P27 の下にある図の days_rented の矢印が誤り(正しくは、a rental まで)
- P28 の Movie.NEW_RELEASE は Movie::NEW_RELEASE の誤り(ほかにも同じ誤り多数)
- 文章中には、テストの重要性を繰り返し述べているにもかかわらず、テストコードが記載されていない。
- P33 P40 の each.movie.title は element.movie.title の誤り
- P71 の include Price は include DefaultPrice の誤り
- P72 の Movie クラスの定義は def initialize が欠けている。
あと、個人的な感想として本にツッコミたい点として、
- P35 のコードは書き換えた後に、変数 result があるが、これはそもそも不要な変数と感じました。
具体的に引用すると、
def amount_for(rental) result = 0 case rental.movie.price_code when Movie::REGULAR result += 2 result += (rental.days_rented - 2)*1.5 if rental.days_rented > 2 when Movie::NEW_RELEASE result += rental.days_rented * 3 when Movie::CHILDRENS result += 1.5 result += (rental.days_rented - 3) * 1.5 if rental.days_rented > 3 end result end
のところについては私であれば
def amount_for(rental) case rental.movie.price_code when Movie::REGULAR if rental.days_rented > 2 return 2 + (rental.days_rented - 2)*1.5 else return 2 end when Movie::NEW_RELEASE result rental.days_rented * 3 when Movie::CHILDRENS if rental.days_rented > 3 return 1.5 + (rental.days_rented - 3) * 1.5 else return 1.5 end end return 0 end
のように書いて、不要なローカル変数を削除するかな、と思いました。
P65 から P72 で導入されているポリモーフィックにするための変更だが、ポリモーフィズムな振る舞いをさせる必要があるメソッドが1つである間は、私なら case when 文のままにしておきます。この本の中では今後の料金計算の方法の変更など仕様変更が待っているとのことであるので、そのような場合であればポリモーフィズムを導入させるかもしれませんが。
最後にこれはたんなる感想になるが、最終形のコードが
# 最新作の映画の作成 movie = Movie.new("The Watchman", NewReleasePrice.new) # 最新作から通常作品に価格体系が変更 movie.price = RegularPrice.new
というように、 Dependency Injection というか Constructor Injection のパターンにしているのは、自分としては少し違和感を感じた。私であれば、
class Movie def self.createNewRelease return self.new("The Watchman", NewReleasePrice.new) end end movie = Movie.createNewRelease("The Watchman")
というようにあわせて Factory Method も作成して、使う側からはファクトリメソッドを使わせるようなインタフェース設計にするように思った。
しかしながら、今書きながら考えていると、結局、
movie.price = RegularPrice.new
とするケースがあることなど、内部の実装を使う側も意識する必要があり、完全な隠蔽はできないし、この場合する必要もないことから、この本の書いている方法でよいですね。
MessagePack-JS (4)
cuzic です。
MessagePack-JS をさらに更新しました。
- MessagePack.pack() による、MessagePack へのエンコーディング機能の追加。
- エンコーディングに対応したテストコードも追加
- MessagePack.unpack() のテストコードに XMLHTTPRequest を使った例を追加した。
- XHR を使う対抗側のコードの追加。
MessagePack-JS へのエンコーディングも可能にしたことで、一応、基本的なやりたいことについては、実現しました。
MessagePack-JS を作ったときのもともとのモチベーションは JavaScript の勉強だったのですが、実装してみて、MessagePack にはいろいろと利点があることが確認できたので、整理してみたいと思います。
非常にコンパクト
MessagePack の特徴として、同じ内容のデータでも少ない容量しか必要としないという点があります。
これは、非常に容量が限られている Cookie (最大でもわずか 3.8KB)や、Flash の SharedObject(最大でもわずか 1 MB 程度)といった状況では、この容量制限の中で効率的にデータを格納できる MessagePack 形式は有効だと思います。
最近では HTML5 の Web Storage API もありますが、それでも容量は Google Chrome の場合で 最大 2.5MB 程度と制限されています。
コンパクトにデータを格納できる形式への需要は高いように思います。
通信時間が短縮できる。
パース処理だけで比較した速度面では JSON.parse よりも劣るものの、データ量そのものがコンパクトなので、通信時間は短く済みます。
クライアント側はアイドル時間が多いが、サーバ側は負荷が高いケースも多いかと思いますし、XHR で取得したデータをパースするような場合は、通信時間が大半を占めることも多いですので、メッセージサイズが小さくなり通信時間を短縮できるメリットは大きいでしょう。
MessagePack-JS (3)
cuzic です。
前回の投稿は ベンチマークテストにバグがあって、うまく処理できていなかったようです。ベンチマークテストのバグを修正すると、私の実装は eval に比べて 10倍以上遅いということが分かりました。
今日は一日かけて MessagePack のバイナリ文字列を Internet Explorer 環境で高速に処理する方法について、模索していました。
Internet Explorer 環境の特徴としては、以下の点があります。
- 単純に JavaScript エンジンが遅い
- responseText ではバイナリ文字列が処理できない。( FireFox 等のような overrideMimeType によるハックが使えない)
- responseBody でバイト配列を返す
- バイト配列は、 JavaScript では直接扱えない。
- バイト配列の処理には JavaScript 以外の技術の利用が必須(VBScript の利用、XMLDOM の COM オブジェクトの利用など)
バイト配列の処理手法として当初は VBScript の利用による解決を模索したのですが、次の問題点を克服できませんでした。
- メモリ使用量が非常に大きくなり、スタックオーバーフローが発生する場合さえある
- 耐え難く遅い。(上記と同根の問題?)
そこで、現時点で github にコミットしているコードでは次の方式を採用しました。
- Microsoft.XMLDOM の COM オブジェクトを利用して、一旦 base64 エンコードする
- base64 エンコードした文字列を JavaScript で書いたデコード処理で 0〜255 までの数の配列に変換
- 2で得た配列を元に MessagePack の形式として解釈
いろいろ実験した中ではこの方式が IE8 環境でもっとも高速に動作するように思われました。
base64 へのエンコード処理は、Microsoft.XMLDOM を利用することで、
function binary_to_string(binary){ var xmldom = new ActiveXObject("Microsoft.XMLDOM"); var bin = xmldom.createElement("bin"); bin.dataType = "bin.base64"; bin.nodeTypedValue = binary; return bin.text; // base64 文字列 }
と簡単にできます。
base64 デコードについては、http://www.webtoolkit.info/javascript-base64.html を参考にしつつ、独自に実装しました。
あと、Microsoft.XMLDOM ではなく ADODB.Stream を使って、バイナリ文字列を扱う手法についても見当したのですが、私の InternetExplorer のセキュリティ設定では利用できない COM オブェジェクトであったため、採用しませんでした。
MessagePack-JS (2)
投稿時は、MessagePack-JS の結果の方が eval より高速としていましたが、それは ベンチマークテストのバグに由来するものでした。計測しなおしまして、記事を訂正しましたので下記を参照ください。
cuzic です。
MessagePack-JS ライブラリをちょっと更新しましたので、報告します。
主な変更点としては、以下のとおりです。
といったところです。
ベンチマーク結果ですが、以下のとおりとなり、eval の方が速いという結果になりました。 (Firefox なら 20倍以上遅い)
MessagePack | JSON | |
Firefox | 9,140msec | 380msec |
---|---|---|
Chrome | 6,883msec | 213msec |
Internet Explorer 8 でも検証したのですが、あまりに遅くて計測に耐えられる速度ではありませんでした。。。(1万行が長すぎたか・・・。
Internet Explorer 8でも計測しました。
MessagePack | JSON | |
Firefox | 9,140msec | 380msec |
---|---|---|
Chrome | 6,883msec | 213msec |
IE8 | 88,563msec | 2,422msec |
sort benchmarkで使われている gensort コマンドを使って
./gensort -a 10000 gensort.txt
でできた gensort.txt を元に生成した配列を 10回デコードするという処理にしました。
配列は、
[ [string_10_bytes_1 , string_32_bytes_1, string_52_bytes_1],
[string_10_bytes_2 , string_32_bytes_2, string_52_bytes_2],
[string_10_bytes_3 , string_32_bytes_3, string_52_bytes_3],
...
[string_10_bytes_10000 , string_32_bytes_10000, string_52_bytes_10000],
]
というようなかんじの文字列の配列の配列です。
JSON のデコードには単なる eval を利用しています。
MessagePack.load_url 関数を削除した理由は、Firefox 等でバイナリ文字列として処理するために必要な overrideMimeType を利用するには自分で実装するしかないかな、と思っていたのですが、Prototype.JS でも
new Ajax.Request(url, { method: 'get', onCreate: function(request, response){ if(request.transport.overrideMimeType){ request.transport.overrideMimeType("text/plain; charset=x-user-defined"); } }, onSuccess: function(transport) { // 処理したい内容 } });
とすることで利用可能であることが分かったので、MessagePack ライブラリから不要と判断したためです。
今後は、
などを進めていけたらいいな、と思っております。
MessagePack-JS
cuzic です。
MessagePack の JavaScript 実装を作成しましたので、公開しました。
GitHub においております。
サーバ側で生成した MessagePack の文字列をクライアント側の JavaScript で、
var data = MessagePack.unpack(unescape("%a1%61")); alert(a);
と書けば Ruby で 'a'.to_msgpack に相当する MessagePack のシリアリゼーション結果を変数 data に格納します。
現在は以下の状態です。
- MessagePack 形式のデコードが可能。
- 整数、nil 、false 、true、浮動小数点、文字列、ハッシュ、配列 に対応
- FireFox 3.6.3 と IE 8.0.6 で動作検証を実施。正常動作を確認。
作成した理由としては、以下の理由です。
- 最近 JavaScript を勉強しているので、なにかを作って公開してみたかった。
- MessagePack の仕様の美しさについて、某所で話題になっていた。
- JSON が主流な JavaScript とのデータ連携を MessagePack に変更することで、サーバ負荷の低減、高速化が可能になる。
今後の予定としては、次の内容を進めていきたいと考えております。
- MessagePack 形式のデコードだけでなくエンコードを可能に。
- テストコードを追加。クロスブラウザな自動テストを可能に。
- 各種ブラウザで利用可能かの検証の実施。
- JSON フォーマットとの速度比較の実施
MessagePack-JS の作成にあたっては、http://github.com/nayutaya/msgpack-pure での Ruby 実装を参考にさせていただきました。
vim勉強会#6 参加
3月21日の vim 勉強会で学んだことを簡単にメモ。
テキストオブジェクト
vim では ct) とすることで、 ) までの手前の文字を置き換えることができる。その ct) と同じ内容を ci( でできるらしい。cf) は ca( で同じ操作になる。ci( や ca( を使うと、一度、編集したい範囲の最初の文字まで移動する操作が不要になり、その分、ラクに編集できる。
smartchr.vim
smartchr を使うと、たとえば = の連打で
" = " # 両側を スペースではさんだ = " += " # lhs = lhs + rhs の略記法 " == " # 等号のような比較の時に使う演算子 " === " # PHP や Ruby に存在する演算子 "=" # 通常の =
のような一連の入力を簡単にできるみたい。
vim スクリプトの文法
ちょっと学んだけど、正直キモい。あまり深入りする気持ちにはなれなかった。
私自身、vim はかなりの初心者の上、素の vi を触る機会も多いためあまり高度な使い方はむしろしない。
範囲指定とか、置換とかのベーシックな機能を(今でも使い方を知っているのですが)もっと使いこなせるようになった方がいいかな、と思い中である。
今度、vim 勉強会があったらあまり使い慣れていないベーシックな機能を説明して、自分の勉強に役立てるようにしようかな。
私が使いこなせていない vim のベーシックな機能としては、
- キーボードマクロ
- ftFT による移動。; . とか。
- cc と C の違い
- インデント。 == 、 >> 、<<
とか。
ちなみにこの日記はemacs で書いています。
update-alternatives
Ruby Advent Calendar の記事で http://blog.udzura.jp/2009/12/13/switch-your-ruby-on-ubuntu910/というのがあった。
RVM はいいものだが、利用している環境が Ubuntu でもあるので、この方法についても試してみることにした。
ただ、冗長な書きぶりが気になったので、update-alternatives を生成する Ruby スクリプトを作成。
このスクリプトを利用すれば、rails コマンドなど新たにコマンドが増えたときにも簡単に対応できる。
1 RUBY186 = ["/usr/local/ruby/ruby-1.8.6-p383", "", 120] 2 RUBY187 = ["/usr/local/ruby/ruby-1.8.7-p174", "", 150] 3 system1 = ["/usr/", "1.8", 140] 4 system2 = ["/usr/", "1.9", 100] 5 RUBY191 = ["/usr/local/ruby/ruby-1.9.1-p376", "", 110] 6 7 commands = %w(ruby irb gem rake) 8 9 [RUBY186, RUBY187, system1, system2, RUBY191].each do |dir, suffix, priority| 10 args = commands.map do |command| 11 link = "/usr/bin/#{command}" 12 fullpathcmd = "#{File.join(dir, "bin", command)}#{suffix}" 13 if command == "ruby" then 14 "--install #{link} #{command} #{fullpathcmd} #{priority}" 15 else 16 "--slave #{link} ruby-#{command} #{fullpathcmd}" 17 end 18 end.join(" ") 19 altcmd = "sudo update-alternatives #{args}" 20 system altcmd 21 end
update-alternatives は、切り替えるのに管理者権限が必要となるようだ。ユーザ権限で頻繁に利用するRuby を変更したいと思う用途にはなかなか難しいかもしれない。
update-alternatives --list によって、インストールされている Ruby を得ることができるため、その結果を利用して、インストールしているすべての Ruby で実行する all-ruby コマンドの作成なども可能となりそうだ。時間があれば取り組みたい。