抽象クラスとモジュール
Ruby のモジュールはインスタンスが生成されないことを約束するという意味では抽象クラスです。一方、Java の抽象クラスというのは、インスタンスが生成されないことを約束する以外に、そのサブクラスに対して自身が持つ抽象メソッドの実装を強制させる役割を果たすことが多いです。
では Ruby のモジュールに、そのモジュールを include したクラスで「このメソッド実装しる」と強制させるメカニズムはあるのでしょうか、ということに興味を持ちました。
そういえば、Enumerable モジュールは Mixin 用のモジュールですが、include した側が each メソッドを持っていることを仮定したモジュールです。もし Ruby に、サブクラスあるいは Mixin したモジュールにメソッドの実行を強制できるメカニズムがあるとしたら、Enumerable では当然それを使いたくなるところです。
つまり、メソッド実装強制メカニズムがあるなら
#!/usr/local/bin/ruby class Foo include Enumerable end Foo.new.class.each do |foo| foo.class.display end
というのは(Foo に each を実装せずにモジュールを Mixin してるので) 実行時エラーではなく、コンパイルエラー(?) になるはず、ということです。
しかし、このコードは実行時エラーでした。ということで、Ruby には Java の抽象メソッドのようなメカニズムはないのだろうと思いました。
Factory Method パターン
id:hyuki さんのデザインパターン本から Factory Method パターンを移植してみます。
まずは素で移植したものです。
#!/usr/local/bin/ruby class Factory def create(owner) p = self.create_product(owner) self.register_product(p) return p end end class IDCard attr_reader :owner def initialize(owner) puts "#{owner} のカードを作ります。" @owner = owner end def use puts "#{owner} のカードを使います。" end end class IDCardFactory < Factory attr_reader :owners def initialize @owners = Array.new end def create_product(owner) IDCard.new(owner) end def register_product(p) @owners.push(p.owner) end protected :create_product, :register_product end factory = IDCardFactory.new products = [ factory.create("るびお"), factory.create("るびこ"), factory.create("まっつさん") ] products.each do |product| product.use; end factory.owners.each do |owner| puts owner end
書籍では Factory クラスは抽象クラスになっていますが、ここでは普通のクラスとして実装しています。IDCardFactory はその Factory を継承します。
実行すると以下のようになります。
$ ruby factory_method.rb るびお のカードを作ります。 るびこ のカードを作ります。 まっつさん のカードを作ります。 るびお のカードを使います。 るびこ のカードを使います。 まっつさん のカードを使います。 るびお るびこ まっつさん
たまたま目を通した Ruby のモジュールのマニュアルユーザーズガイド に
モジュールはインスタンスを生成しない(抽象クラスであるこ とが保証される).
とあって、モジュールを抽象クラスとして利用することができることが分かりました。そこで Factory クラスをモジュールに変更しました。
#!/usr/local/bin/ruby module Factory def create(owner) ... end end class IDCardFactory include Factory ... end
Ruby っぽくなったし、無理矢理抽象クラスみたいなこともないのですっきりしました。抽象クラスを実現したい場合にはモジュールで、と覚えておきます。
ところで、IDCardFactory という名前が Ruby っぽくないかなと思ったので、IDCard::FactoryFactory::IDCard に変更してみたところ
factory_method.rb:33:in `initialize': wrong number of arguments (1 for 0) (ArgumentError) from factory_method.rb:33:in `create_product' from factory_method.rb:5:in `create' from factory_method.rb:43
とエラーになってしまいました。どうも、IDCard.new
しているところで IDCard クラスではなく Factory::IDCard を対象にしてしまっているような感じです。Foo::Bar は Perl の名前空間と同じようなものだと思っていましたが、ちょっと違うみたいです。スコープのメカニズムか何かが入っているのでしょうか。
ERB を使って RSS を JavaScript に変換する
ERB の使い方を覚えたので、ERB を使って RSS を JavaScript に変換したいです。よくある類の処理ですが、これを ERB を使ってやってみます。
#!/usr/local/bin/ruby require 'open-uri' require 'rss/1.0' require 'rss/2.0' require 'erb' module RSS::JavaScriptfy def to_html @erb = ERB.new(DATA.read, nil, "-") @erb.result(binding) end def to_js self.to_html.split("?n").collect { |line| line.gsub!(/'/, "?'") "document.writeln('#{line}');" }.join("?n") end end class RSS::RDF include RSS::JavaScriptfy end class RSS::Rss include RSS::JavaScriptfy end if ARGV.size != 1 puts "Usage: ruby rss2js.rb <url>" exit end rss = RSS::Parser::parse(open(ARGV.shift).read) puts rss.to_js __END__ <h2><a href="<%= channel.link %>"><%= channel.title %></h2> <ul> <% items.each do |item| -%> <li><a href="<%= item.link %>"><%= item.title %></a></li> <% end -%> </ul>
RSS::JavaScriptfy というモジュールを作って、その中に ERB で RSS オブジェクトを HTML に変換する to_html
、HTML string の各行を document.write
で囲む to_js
メソッドを作りました。これを RSS::RDF や RSS::Rss に Mixin しておき、実行側では取得した rss オブジェクトに対して rss.to_js
を呼ぶという仕組みです。
$ ruby rss2js.rb http://d.hatena.ne.jp/rubyo/rss > rubyo.js
として保存し、HTML からこの rubyo.js を script タグでインクルードしたところ、
と表示されました。
ERB を使ってみる
Ruby でテンプレートエンジンを使ってみたいです。
Rails の Action View では ERB が使われるようです。まずは単体で ERB を使ってみたいと思います。ERB は Ruby 1.8 以降には標準で含まれているようなので require 'erb'
するだけで使えました。
#!/usr/local/bin/ruby require 'erb' class Player def initialize(name, age) @name = name @age = age end def to_s @erb = ERB.new(DATA.read) @erb.result(binding) end end puts Player.new('rubyo', 19).to_s __END__ name: <%= @name %> age: <%= @age %> <%= Time.now %>
ERB.new にテンプレートの文字列を渡します。ここでは __END__ 以降に書いた文字列を取得する DATA.read
を使いました。便利です。
テンプレート上からアクセスするオブジェクトを、ERB インスタンスに渡す方法は他言語のテンプレートエンジンと比較するとちょっと特殊です。例えば Perl の HTML::Template や Template Toolkit では、テンプレートインスタンスに user => $user_object
のような形で渡すことになりますが、ERB ではそれとは違い Kernel#binding
を使って、ERB インスタンスをテンプレートに渡したいオブジェクトに組み込むような形で実装するようです。
なお、Kernel#binding
は ri によると
Returns a +Binding+ object, describing the variable and method bindings at the point of call. This object can be used when calling +eval+ to execute the evaluated command in this environment. Also see the description of class +Binding+.
だそうです。binding を渡された ERB 側は、Binding オブジェクトを使って内部処理を進めているのでしょう。
このコードを実行すると
$ ruby erb.rb name: rubyo age: 19 Wed Apr 05 11:52:20 JST 2006
となりました。
Ruby の '=> '#2
まつもとさんにコメントをいただきました。びっくりしました。:foo => 'bar'
と書くのはめんどくさいよ、という話をしてみたところ Ruby 1.9 では
foo: bar
と書けるそうです。なるほどー。実装されるのが楽しみです。
これを 1.8 でできるようなマジックはないのかな、と思いました。Perl には Acme::Dot など、文法そのものを変更してしまうマジカルモジュールが結構あります。まだ修行中なので、Ruby の文法そのものを変更する手段などについてはほとんど知りません。
なお、まつもとさん曰く Perl 6 では左側の Bareword を文字列として解釈しなくなるそうです。ええー。
Ruby で文字コード変換
nuna さんに教えてもらいました。1.8.2 以降では NKF#nkf
を使うと良いそうです。リモートの RSS を取得して、そのタイトルを EUC-JP で出力してみます。
#!/usr/local/bin/ruby require 'open-uri' require 'rss/1.0' require 'nkf' content = Kernel.open('http://d.hatena.ne.jp/rubyo/rss') rss = RSS::Parser::parse(content.read) rss.items.each do |item| puts NKF.nkf('-m0 -e', item.title) end
NKF.nkf('-m0 -e', item.title)
のところで UTF-8 から EUC-JP に変換しています。第一引数が謎のオプションです。おそらく nkf コマンドと同一なのでしょう。こんなプログラマブルでないインタフェースでいいのでしょうか...ほかにちゃんとしたインタフェースがあるのかもしれません。
実行すると、
$ ruby euc2utf8.rb Ruby リファレンスマニュアル initialize に渡ってきたパラメタをインスタンス変数に丸投げする RSS を YAML に変換する#2 Template Method パターン initialize に渡された値を自動的にインスタンス変数にする Adapter パターン Enumerable で拡張してみます Iterator パターン RSS を YAML に変換する RSS フィードを処理する HTTP でコンテンツを取得する irb で文法確認 Ruby で YAML Ruby で grep るびおです。
と、確かに EUC-JP な端末に文字化けせずに出力させることができました。nuna さんありがとうございました。
Ruby リファレンスマニュアル
nuna さんによるとこのオンラインのマニュアルが一番充実しているのではないか、とのこと。