特異メソッド
特定のインスタンスだけが実行できるメソッドを特異メソッド、というそうです。特異メソッド以外にも Ruby では"特異"という単語がよくでてくるような気がします。特異メソッドは英語では "Singleton Method"、と呼ばれるようです。
a = "hello" b = a.dup def a.to_s "The value is '#{self}'" end
あるいは
class << a def to_s "The value is '#{self}'" end end
と書けるとのこと。夢の中で見た謎の文法 "class << a" の謎が解けました。
使いどころはまだよく分かっていません。特定のインスタンスの動きをちょっとだけ変えたい、という時などでしょうか。a と b は同じインスタンスだけれども、a の動きだけちょっと変えたい、でも同じインタフェースでそれらは扱いたい、といったケースなど...。どういう場合にそれが発生するか、が問題です。
ちょっと今日は Ruby を勉強している時間があまりありません。残念です。
method_missing と instance_eval を使った Decorator
Kernel#method_missing を使ってみたいです。
method_missing は、任意のクラスに対してメソッドが呼び出されたとき、対象のメソッドがそのクラスのクラス階層のどこにも実装されてなかった場合に呼び出されるメソッド、だそうです。Perl の AUTOLOAD に似ています。
そこで、method_missing を使った Decorator パターンを実装してみました。Dog オブジェクトを渡すとその犬が強くなる StrongDog です。
dog = Dog.new('ポチ') sdog = StrongDog.new(dog) dog.bark sdog.bark
という風に使います。すると、
ポチ: わんわん ポチ+1: brrr... ポチ+1: わんわん
という風な結果が返ります。Decorator の StrongDog でくるむと +1 されて(ロングソード+1、のようなイメージです)、威勢がよくなります。クラスの実装は以下のようにしてみました。
class Dog attr :name def initialize(name) @name = name end def bark puts "#@name: わんわん" end end class StrongDog def initialize(dog) @dog = dog @dog.name << "+1" end def method_missing(method_id) puts "#{@dog.name}: brrr..." @dog.instance_eval(method_id.id2name) end end
StrongDog#bark は実装していないので、それが呼び出されると method_missing が呼び出され、前処理を挟んだあと @dog に同名のメソッドに処理を転送します。
instance_eval の理解がまだ不完全ですが、なんとなく使ってみたらうまくいきました。
クラスはオブジェクトであり、クラス名は定数である
ウサギ本を読んでいたところ、先の Object#const_get の理解を助けてくれる解説を見つけました。P.292 の "クラス名は定数である" の箇所です。
また、Ruby のクラスは Class オブジェクトというオブジェクトでした。したがって、クラスメソッドを呼び出すのは Class オブジェクトにメッセージを送信しているということになります。Ruby の特徴である「すべてがオブジェクト」と聞いてすぐ想像するのは Java のプリミティブ型もオブジェクトであるというあたりですが、クラスもオブジェクトであるというのは面白いです。
$ irb irb(main):001:0> Object.const_get(:String).class => Class
たしかに、クラスを取得してそのクラスを調べると Class クラスです。
クラスがオブジェクトであるということは、クラスも他のオブジェクトと同じように扱うことができ、
- クラスをコピーしたり
- クラスにメソッドを渡したり
- クラスを敷きとして使用したり
といったことができるとウサギ本に書いています。
irb#1(main):001:0> def factory(klass, *args) irb#1(main):002:1> klass.new(*args) irb#1(main):003:1> end => nil irb#1(main):004:0> factory(String, "Hello") => "Hello" irb#1(main):005:0> factory(Dir, ".") => #<Dir:0x31e3c0>
おお。
Klass
"Class" ではなく "Klass" と書くのはなぜ? の答えは、id:drawnboy さんによると
ローカル変数なクラスオブジェクトを指すときに ”class” は予約語なので ”klass” と書くようです。
とのことです。(昨晩、夢の中でまた別の人が同じことを言っていた気がします。) id:drawnbody さんによれば、Klass は Smalltalk 由来かもしれない、とのこと。面白そうでしたので少し Google で調べてみましたが、由来について解説しているページは簡単には見つかりませんでした。残念です。
alias を使った Adapter パターン
ruby には alias 式でメソッド、演算子、グローバル変数、正規表現後方参照の別名を作ることができるようです。Adapter パターン を Ruby っぽくするために alias を使って書き替えてみます。
#!/usr/local/bin/ruby class Banner def initialize(string) @string = string end def show_with_paren puts "(#@string)" end def show_with_aster puts "*#@string*" end end class PrintBanner < Banner alias print_weak show_with_paren alias print_strong show_with_aster end p = PrintBanner.new("Hello") p.print_weak p.print_strong
これでうまくいきました。元のコードが Banner#show_with_paren を PrintBanner#print_weak に委譲しているだけだったので alias を使ってみた、というものです。
わざわざ PrintBanner を用意しなくても
class Banner alias print_weak show_with_paren alias print_strong show_with_aster end
でも良いかもしれません。後からいくらでもクラスの挙動を変えられるのが動的言語の良いところです。
ところでこの alias、文法的には「新しい名前 古い名前」の順ですが、なんとなく逆の方が直感的な気がします。しかしこの順になっているのは UNIX の alias コマンドを意識しているからなのでしょうか。
Object#const_get
クラス名からクラスを得る方法は、リファレンスマニュアルによると eval を使う方法、Object#const_get を使う方法の二通りがあるようです。
#!/usr/local/bin/ruby class Player attr_accessor :name, :age def initialize(name, age) @name = name @age = age end end player1 = Object.const_get('Player').new('rubyo', 19) puts player1.name puts player1.age player2 = eval('Player').new('rubco', 18) puts player2.name puts player2.age
この時クラス名が Net::HTTP のようにネストしている場合の方法についてもマニュアルに解説がありました。
eval で文字列を"コード上のクラス名"として扱うことについては理解できました。一方の const_get は ri すると
------------------------------------------------------- Module#const_get mod.const_get(sym) => obj ------------------------------------------------------------------------ Returns the value of the named constant in _mod_. Math.const_get(:PI) #=> 3.14159265358979
とありました。named 定数の値を取得する。引数はシンボル。この Math の例を見ると特定のモジュール内で定義されている定数を取得できる、という風に見れます。 Math モジュールの中には PI という定数が定義されていて、その値は 3.14159... であり、それを Math モジュールの外から取得する手段として const_get が用意されている、ということでしょう。
- Math モジュールの中の
- PI 定数を取得する
- その値は 3.14159...
モジュールのクラスに相当する Module クラスには constans というメソッドがあり、これで定数一覧を取得できます。
$ irb irb(main):001:0> Math.constants => ["E", "PI"]
と、確かに Math モジュールには PI 定数があることが分かります。
もとい、
Object.const_get('Player') #=> Player
はどう理解すれば良いかというと、先の Math のものをそのまま当てはめると
- Object クラスの中の
- Player 定数を取得する
- その値は Player というクラス名( 'Player' という文字列ではなく、コード上でクラス名と解釈される Player という値)
と言う風になります。ということは Object クラスに Player という定数が定義されていることになるのでしょう。そこで、
#!/usr/local/bin/ruby class Player end Object.constants.each {|const| puts const }
というコードを書き確かめてみました。
$ ruby const_get.rb | grep 'Player' Player
確かに Player という定数が見つかりました。一方、class Player を定義していない段階で const_get してみます。
$ irb irb(main):001:0> Object.const_get(:Player) NameError: uninitialized constant Player from (irb):1:in `const_get' from (irb):1 irb(main):002:0> Object.const_get(:String) => String
組み込みクラスの String では結果が返るのに対して、定義されていないクラスの Player には結果が返りませんでした。
つまり、Object クラスは、そのアプリケーションの中で扱えるクラスのクラス名を定数として持つような機構になっている、と理解しました。
Abstract Factory パターン
id:hyuki さんのデザインパターン本から Abstract Factory パターンを移植してみます。
#!/usr/local/bin/ruby class Item def initialize(caption) @caption = caption end end class Link < Item def initialize(caption, url) super(caption) @url = url end end class Tray < Item def initialize(caption) super(caption) @tray = Array.new end def add(item) @tray.push(item) end end class Page def initialize(title, author) @title = title @author = author @content = Array.new end def add(item) @content.push(item) end def output self.make_html end end class Factory def Factory.get_factory(classname) eval(classname).new end end class ListFactory < Factory def create_link(caption, url) ListLink.new(caption, url) end def create_tray(caption) ListTray.new(caption) end def create_page(title, author) ListPage.new(title, author) end end class ListLink < Link def make_html return "<li><a href=?"#@url?">#@caption</a></li>?n" end end class ListTray < Tray def make_html result = String.new result += "<li>#@caption?n" result += "<ul>?n" @tray.each do |item| result += item.make_html end result += "</ul>?n" result += "</li>" result end end class ListPage < Page def make_html list = @content.map{ |item| item.make_html }.join('') return <<EOS <html> <head><title>#@title</title></head> <body> <h1>#@title</h1> <ul> #{list} </ul> <hr><address>#@author</address> </body> </html> EOS end end if ARGV.size != 1 puts "Usage: #$0 <classname>" exit end factory = Factory.get_factory(ARGV.shift) hatena = factory.create_link("はてな", "http://www.hatena.ne.jp/") google = factory.create_link("Google", "http://www.google.com/") yahoo = factory.create_link("Yahoo!", "http://www.yahoo.com/") yahoo_jp = factory.create_link("Yahoo! Japan", "http://www.yahoo.co.jp/") yahoo_tray = factory.create_tray("Yahoo!") yahoo_tray.add(yahoo) yahoo_tray.add(yahoo_jp) page = factory.create_page("LinkPage", "るびお") page.add(hatena) page.add(google) page.add(yahoo_tray) puts page.output
このコードを、引数に "ListFactory" と与えて実行すると、
$ ruby abstract_factory.rb ListFactory <html> <head><title>LinkPage</title></head> <body> <h1>LinkPage</h1> <ul> <li><a href="http://www.hatena.ne.jp/">はてな</a></li> <li><a href="http://www.google.com/">Google</a></li> <li>Yahoo! <ul> <li><a href="http://www.yahoo.com/">Yahoo!</a></li> <li><a href="http://www.yahoo.co.jp/">Yahoo! Japan</a></li> </ul> </li> </ul> <hr><address>るびお</address> </body> </html>
となりました。んー、書籍の例は Java の例で、抽象クラスでサブクラスの実装に縛りをかけながら Factory でごっそりそれらを切り替えるというのがとても美しいのですが、縛りをかけられない Ruby ではいまいちしまりがない感じがします。Item や Tray はコンストラクタのみの実装ですし。
そういえば、一つ学びました。
class Factory def Factory.get_factory(classname) begin eval(classname).new rescue NameError puts "Unknown factory class: #{classname}." end end end
クラス名の文字列からインスタンスを生成する方法です。eval で文字列を評価してからメソッドを起動するとうまくいきました。
に高橋さんが実装したものを見つけました。"素朴な実装" のところが普通に移植したものです。
- eval ではなく Object.const_get をつかっている。const_get って何でしょう。
- ファイルを factory.rb / listfactory.rb / tablefactory.rb に分割している
- raise NotImplementedError を記述してる箇所がある
というところ以外はほとんど一緒の実装でした。その他に
というものがありました。参考になります。いまから熟読します。