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 を記述してる箇所がある

というところ以外はほとんど一緒の実装でした。その他に

  • Constant メソッド
  • パーツカタログ
  • クラスインスタンス変数によるパーツカタログ
  • 命名の規約によるファクトリ

というものがありました。参考になります。いまから熟読します。