Design Pattern: Ruby Companion
(ずばっと略)
結城浩さんのサンプルを元に,Rubyで実装してみました.
Ruby版では,4つのソースからなります.
factory.rb
listfactory.rb
tablefactory.rb
main.rb
「〜factory.rb」というファイルで定義されるクラスは,実はファクトリクラスだけではありません.そのファクトリが生成するパーツ(List,Tray,Pageなど)のクラスも定義しています.
素朴にRubyで実装したものです.
factory.rb
class Factory def Factory.getFactory(klass) begin factory = Object.const_get(klass).new return factory rescue NameError print "undefined class: #{klass}.\n" end end end ## abstract class Item def initialize(caption) @caption = caption end end ## abstract class Link < Item def initialize(caption, url) super(caption) @url = url end end ## abstract class Tray < Item def initialize(caption) super(caption) @tray = Array.new() end def add(item) @tray << item end end ## abstract class Page def initialize(title, author) @title, @author = title, author @content = Array.new() end def add(item) @content << item end def output begin filename = @title + ".html"; File.open(filename, "w"){|f| f.write(makeHTML()) } print "#{filename} was created.\n" rescue print $!+"\n" print $@.join("\n")+"\n" end end def makeHTML raise NotImplementedError end end
listfactory.rb
class ListFactory < Factory def createLink(caption, url) ListLink.new(caption, url) end def createTray(caption) ListTray.new(caption) end def createPage(title, author) ListPage.new(title, author) end end class ListLink < Link def makeHTML() return " <li><a href=\"#{@url}\">#{@caption}</a></li>\n"; end end class ListTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML }.join('') buffer = <<EOB <li> #{@caption} <ul> #{items} </ul> </il> EOB buffer end end class ListPage < Page def makeHTML() items = @content.collect{|item| item.makeHTML() }.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <ul> #{items} </ul> <hr><address>#{@author}</address> </body></html> EOB buffer end end
tablefactory.rb
class TableFactory < Factory def createLink(caption, url) TableLink.new(caption, url) end def createTray(caption) TableTray.new(caption) end def createPage(title, author) TablePage.new(title, author) end end class TableLink < Link def makeHTML "<td><a href=\"#{@url}\">#{@caption}</a></td>\n" end end class TableTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML()}.join('') buffer = <<"EOB" <td> <table width="100%" border="1"><tr> <td bgcolor="#cccccc" align="center" colspan="#{@tray.size()}"> <b>#{@caption}</b> </td> </tr> <tr> #{items} </tr> </table> </td> EOB buffer end end class TablePage < Page def makeHTML items = @content.collect{|item| "<tr>#{item.makeHTML()}</tr>"}.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <table with="80%" border="3"> #{items} </table> <hr> <address>#{@author}</address> </body> </html> EOB buffer end end
main.rb
require 'factory.rb' require 'listfactory.rb' require 'tablefactory.rb' def usage() print "Usage: ruby main.rb <class name of ConcreteFactory>\n" print "Example 1: ruby main.rb ListFactory\n" print "Example 2: ruby main.rb TableFactory\n" end ## main if ARGV.length != 1 usage() exit(0) end factory = Factory.getFactory(ARGV[0]) asahi = factory.createLink("ASAHI newspaper", "http://www.asahi.com/") yomiuri = factory.createLink("YOMIYURI newspaper", "http://www.yomiuri.co.jp/") us_yahoo = factory.createLink("Yahoo!", "http://www.yahoo.com/") jp_yahoo = factory.createLink("Yahoo!Japan", "http://www.yahoo.co.jp/") excite = factory.createLink("Excite", "http://www.excite.com/") google = factory.createLink("Google", "http://www.google.com/") traynews = factory.createTray("Newspaper") traynews.add(asahi) traynews.add(yomiuri) trayyahoo = factory.createTray("Yahoo!") trayyahoo.add(us_yahoo) trayyahoo.add(jp_yahoo) traysearch = factory.createTray("Search Engine") traysearch.add(trayyahoo) traysearch.add(excite) traysearch.add(google) page = factory.createPage("LinkPage", "YUKI, Hiroshi") page.add(traynews) page.add(traysearch) page.output()
「Constant Method」というパターン(イディオム)を使っています.これは,Template Methodパターンの一種で,メソッドの返り値として「クラスそのもの」を表すオブジェクトを返すようにするものです.
factory.rb
## Constant Method Solution ## DPSC p.38 class Factory def Factory.getFactory(klass) begin factory = Object.const_get(klass).new return factory rescue NameError print "undefined class: #{klass}\n" rescue raise end end def createLink(*args) linkClass.new(*args) end def createTray(*args) trayClass.new(*args) end def createPage(*args) pageClass.new(*args) end end ## abstract class Item def initialize(caption) @caption = caption end end ## abstract class Link < Item def initialize(caption, url) super(caption) @url = url end end ## abstract class Tray < Item def initialize(caption) super(caption) @tray = Array.new() end def add(item) @tray << item end end ## abstract class Page def initialize(title, author) @title, @author = title, author @content = Array.new() end def add(item) @content << item end def output begin filename = @title + ".html"; File.open(filename, "w"){|f| f.write(makeHTML()) } print "#{filename} was created.\n" rescue print $!+"\n" print $@.join("\n")+"\n" end end def makeHTML raise NotImplementedError end end
listfactory.rb
class ListFactory < Factory def linkClass() ListLink end def trayClass() ListTray end def pageClass() ListPage end end class ListLink < Link def makeHTML() return " <li><a href=\"#{@url}\">#{@caption}</a></li>\n"; end end class ListTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML }.join('') buffer = <<EOB <li> #{@caption} <ul> #{items} </ul> </il> EOB buffer end end class ListPage < Page def makeHTML() items = @content.collect{|item| item.makeHTML() }.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <ul> #{items} </ul> <hr><address>#{@author}</address> </body></html> EOB buffer end end
tablefactory.rb
class TableFactory < Factory def linkClass() TableLink end def trayClass() TableTray end def pageClass() TablePage end end class TableLink < Link def makeHTML "<td><a href=\"#{@url}\">#{@caption}</a></td>\n" end end class TableTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML()}.join('') buffer = <<"EOB" <td> <table width="100%" border="1"><tr> <td bgcolor="#cccccc" align="center" colspan="#{@tray.size()}"> <b>#{@caption}</b> </td> </tr> <tr> #{items} </tr> </table> </td> EOB buffer end end class TablePage < Page def makeHTML items = @content.collect{|item| "<tr>#{item.makeHTML()}</tr>"}.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <table with="80%" border="3"> #{items} </table> <hr> <address>#{@author}</address> </body> </html> EOB buffer end end
main.rb
require 'factory.rb' require 'listfactory.rb' require 'tablefactory.rb' def usage() print "Usage: ruby main.rb <class name of ConcreteFactory>\n" print "Example 1: ruby main.rb ListFactory\n" print "Example 2: ruby main.rb TableFactory\n" end ## main if ARGV.length != 1 usage() exit(0) end factory = Factory.getFactory(ARGV[0]) asahi = factory.createLink("ASAHI newspaper", "http://www.asahi.com/") yomiuri = factory.createLink("YOMIYURI newspaper", "http://www.yomiuri.co.jp/") us_yahoo = factory.createLink("Yahoo!", "http://www.yahoo.com/") jp_yahoo = factory.createLink("Yahoo!Japan", "http://www.yahoo.co.jp/") excite = factory.createLink("Excite", "http://www.excite.com/") google = factory.createLink("Google", "http://www.google.com/") traynews = factory.createTray("Newspaper") traynews.add(asahi) traynews.add(yomiuri) trayyahoo = factory.createTray("Yahoo!") trayyahoo.add(us_yahoo) trayyahoo.add(jp_yahoo) traysearch = factory.createTray("Search Engine") traysearch.add(trayyahoo) traysearch.add(excite) traysearch.add(google) page = factory.createPage("LinkPage", "YUKI, Hiroshi") page.add(traynews) page.add(traysearch) page.output()
「パーツカタログ」というイディオムを使っています.これは,ファクトリのインスタンスにハッシュオブジェクトを持たせます.ハッシュには,キーとして生成したいものを表すシンボルを,値としてそれに対応するクラスのクラスオブジェクトを,それぞれ与えておきます.そして,各オブジェクトを生成する際には,そのファクトリが持っているハッシュの値にnewメソッドを適用させ,インスタンスを作ります.
Factoryのサブクラスでは,インスタンス変数を1つ定義しておくだけです.メソッドはスーパークラスのものをそのまま継承して使うことになります.
factory.rb
## partsCatalog ## DPSC p.xx class Factory def Factory.getFactory(klass) begin factory = Object.const_get(klass).new return factory rescue NameError print "undefined class: #{klass}\n" end end def initialize @partsCatalog = nil end def create(part, *args) @partsCatalog[part].new(*args) end end ## abstract class Item def initialize(caption) @caption = caption end end ## abstract class Link < Item def initialize(caption, url) super(caption) @url = url end end ## abstract class Tray < Item def initialize(caption) super(caption) @tray = Array.new() end def add(item) @tray << item end end ## abstract class Page def initialize(title, author) @title, @author = title, author @content = Array.new() end def add(item) @content << item end def output begin filename = @title + ".html"; File.open(filename, "w"){|f| f.write(makeHTML()) } print "#{filename} was created.\n" rescue print $!+"\n" print $@.join("\n")+"\n" end end def makeHTML raise NotImplementedError end end
listfactory.rb
class ListFactory < Factory def initialize @partsCatalog = { :Link => ListLink, :Tray => ListTray, :Page => ListPage, } end end class ListLink < Link def makeHTML() return " <li><a href=\"#{@url}\">#{@caption}</a></li>\n"; end end class ListTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML }.join('') buffer = <<EOB <li> #{@caption} <ul> #{items} </ul> </il> EOB buffer end end class ListPage < Page def makeHTML() items = @content.collect{|item| item.makeHTML() }.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <ul> #{items} </ul> <hr><address>#{@author}</address> </body></html> EOB buffer end end
tablefactory.rb
class TableFactory < Factory def initialize @partsCatalog = { :Link => TableLink, :Tray => TableTray, :Page => TablePage, } end end class TableLink < Link def makeHTML "<td><a href=\"#{@url}\">#{@caption}</a></td>\n" end end class TableTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML()}.join('') buffer = <<"EOB" <td> <table width="100%" border="1"><tr> <td bgcolor="#cccccc" align="center" colspan="#{@tray.size()}"> <b>#{@caption}</b> </td> </tr> <tr> #{items} </tr> </table> </td> EOB buffer end end class TablePage < Page def makeHTML items = @content.collect{|item| "<tr>#{item.makeHTML()}</tr>"}.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <table with="80%" border="3"> #{items} </table> <hr> <address>#{@author}</address> </body> </html> EOB buffer end end
main.rb
require 'factory.rb' require 'listfactory.rb' require 'tablefactory.rb' def usage() print "Usage: ruby main.rb <class name of ConcreteFactory>\n" print "Example 1: ruby main.rb ListFactory\n" print "Example 2: ruby main.rb TableFactory\n" end ## main if ARGV.length != 1 usage() exit(0) end factory = Factory.getFactory(ARGV[0]) asahi = factory.create(:Link, "ASAHI newspaper", "http://www.asahi.com/") yomiuri = factory.create(:Link, "YOMIURI newspaper", "http://www.yomiuri.co.jp/") us_yahoo = factory.create(:Link, "Yahoo!", "http://www.yahoo.com/") jp_yahoo = factory.create(:Link, "Yahoo!Japan", "http://www.yahoo.co.jp/") excite = factory.create(:Link, "Excite", "http://www.excite.com/") google = factory.create(:Link, "Google", "http://www.google.com/") traynews = factory.create(:Tray, "Newspaper") traynews.add(asahi) traynews.add(yomiuri) trayyahoo = factory.create(:Tray, "Yahoo!") trayyahoo.add(us_yahoo) trayyahoo.add(jp_yahoo) traysearch = factory.create(:Tray, "Search Engine") traysearch.add(trayyahoo) traysearch.add(excite) traysearch.add(google) page = factory.create(:Page, "LinkPage", "YUKI, Hiroshi") page.add(traynews) page.add(traysearch) page.output()
パーツカタログをクラスインスタンス変数を使って実装したものです.先ほどの例とは違い,カタログを持っているのは各ファクトリクラスになります.
factory.rb
## partsCatalog as Class Instance Variable ## DPSC p.xx class Factory @partsCatalog = nil def self.partsCatalog() @partsCatalog end def Factory.getFactory(klass) begin factory = Object.const_get(klass).new return factory rescue NameError print "undefined class: #{klass}\n" end end def create(part, *args) self.class.partsCatalog[part].new(*args) end end ## abstract class Item def initialize(caption) @caption = caption end end ## abstract class Link < Item def initialize(caption, url) super(caption) @url = url end end ## abstract class Tray < Item def initialize(caption) super(caption) @tray = Array.new() end def add(item) @tray << item end end ## abstract class Page def initialize(title, author) @title, @author = title, author @content = Array.new() end def add(item) @content << item end def output begin filename = @title + ".html"; File.open(filename, "w"){|f| f.write(makeHTML()) } print "#{filename} was created.\n" rescue print $!+"\n" print $@.join("\n")+"\n" end end def makeHTML raise NotImplementedError end end
listfactory.rb
class ListLink < Link def makeHTML() return " <li><a href=\"#{@url}\">#{@caption}</a></li>\n"; end end class ListTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML }.join('') buffer = <<EOB <li> #{@caption} <ul> #{items} </ul> </il> EOB buffer end end class ListPage < Page def makeHTML() items = @content.collect{|item| item.makeHTML() }.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <ul> #{items} </ul> <hr><address>#{@author}</address> </body></html> EOB buffer end end class ListFactory < Factory @partsCatalog = { :Link => ListLink, :Tray => ListTray, :Page => ListPage } end
tablefactory.rb
class TableLink < Link def makeHTML "<td><a href=\"#{@url}\">#{@caption}</a></td>\n" end end class TableTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML()}.join('') buffer = <<"EOB" <td> <table width="100%" border="1"><tr> <td bgcolor="#cccccc" align="center" colspan="#{@tray.size()}"> <b>#{@caption}</b> </td> </tr> <tr> #{items} </tr> </table> </td> EOB buffer end end class TablePage < Page def makeHTML items = @content.collect{|item| "<tr>#{item.makeHTML()}</tr>"}.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <table with="80%" border="3"> #{items} </table> <hr> <address>#{@author}</address> </body> </html> EOB buffer end end class TableFactory < Factory @partsCatalog = { :Link => TableLink, :Tray => TableTray, :Page => TablePage, } end
main.rb
require 'factory.rb' require 'listfactory.rb' require 'tablefactory.rb' def usage() print "Usage: ruby main.rb <class name of ConcreteFactory>\n" print "Example 1: ruby main.rb ListFactory\n" print "Example 2: ruby main.rb TableFactory\n" end ## main if ARGV.length != 1 usage() exit(0) end factory = Factory.getFactory(ARGV[0]) asahi = factory.create(:Link, "ASAHI newspaper", "http://www.asahi.com/") yomiuri = factory.create(:Link, "YOMIURI newspaper", "http://www.yomiuri.co.jp/") us_yahoo = factory.create(:Link, "Yahoo!", "http://www.yahoo.com/") jp_yahoo = factory.create(:Link, "Yahoo!Japan", "http://www.yahoo.co.jp/") excite = factory.create(:Link, "Excite", "http://www.excite.com/") google = factory.create(:Link, "Google", "http://www.google.com/") traynews = factory.create(:Tray, "Newspaper") traynews.add(asahi) traynews.add(yomiuri) trayyahoo = factory.create(:Tray, "Yahoo!") trayyahoo.add(us_yahoo) trayyahoo.add(jp_yahoo) traysearch = factory.create(:Tray, "Search Engine") traysearch.add(trayyahoo) traysearch.add(excite) traysearch.add(google) page = factory.create(:Page, "LinkPage", "YUKI, Hiroshi") page.add(traynews) page.add(traysearch) page.output()
ファクトリのサブクラスではもはや何も定義しません.クラスそのものを定義するだけです.その代わり,各パーツのクラスは同じ規約に従って,名前を揃えておく必要があります.例えば, LinkクラスをListFactoryとTableFactoryで生成するなら,それぞれListLink,TableLinkとする,という具合いです.これはつまり,ファクトリのクラス名が決まると,それが生成するパーツの名前が決まってしまう,ということです.
もっとも,すでに使ってきたサンプルでも,クラスの名前はこのような規則に従ってつけられてきたので,クラス名を大変更しなければいけない,ということはありません.
この実装は,使用するパーツのクラス名がソース内のどこにも書かれなくなるため,ソースコードが少し分かりにくくなるかもしれません.コメントなどで注意を払う必要があるかもしれません.
factory.rb
## Single Factory Class ## DPSC p.xx class Factory def Factory.getFactory(klass) begin factory = Object.const_get(klass).new return factory rescue NameError print "undefined class: #{klass}\n" end end def create(part, *args) klassname = self.type.to_s.sub(/Factory$/, part.to_s) Object.const_get(klassname).new(*args) end end ## abstract class Item def initialize(caption) @caption = caption end end ## abstract class Link < Item def initialize(caption, url) super(caption) @url = url end end ## abstract class Tray < Item def initialize(caption) super(caption) @tray = Array.new() end def add(item) @tray << item end end ## abstract class Page def initialize(title, author) @title, @author = title, author @content = Array.new() end def add(item) @content << item end def output begin filename = @title + ".html"; File.open(filename, "w"){|f| f.write(makeHTML()) } print "#{filename} was created.\n" rescue print $!+"\n" print $@.join("\n")+"\n" end end def makeHTML raise NotImplementedError end end
listfactory.rb
class ListFactory < Factory end class ListLink < Link def makeHTML() return " <li><a href=\"#{@url}\">#{@caption}</a></li>\n"; end end class ListTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML }.join('') buffer = <<EOB <li> #{@caption} <ul> #{items} </ul> </il> EOB buffer end end class ListPage < Page def makeHTML() items = @content.collect{|item| item.makeHTML() }.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <ul> #{items} </ul> <hr><address>#{@author}</address> </body></html> EOB buffer end end
tablefactory.rb
class TableFactory < Factory end class TableLink < Link def makeHTML "<td><a href=\"#{@url}\">#{@caption}</a></td>\n" end end class TableTray < Tray def makeHTML items = @tray.collect{|item| item.makeHTML()}.join('') buffer = <<"EOB" <td> <table width="100%" border="1"><tr> <td bgcolor="#cccccc" align="center" colspan="#{@tray.size()}"> <b>#{@caption}</b> </td> </tr> <tr> #{items} </tr> </table> </td> EOB buffer end end class TablePage < Page def makeHTML items = @content.collect{|item| "<tr>#{item.makeHTML()}</tr>"}.join('') buffer = <<EOB <html><head><title>#{@title}</title></head> <body> <h1>#{@title}</h1> <table with="80%" border="3"> #{items} </table> <hr> <address>#{@author}</address> </body> </html> EOB buffer end end
main.rb
require 'factory.rb' require 'listfactory.rb' require 'tablefactory.rb' def usage() print "Usage: ruby main.rb <class name of ConcreteFactory>\n" print "Example 1: ruby main.rb ListFactory\n" print "Example 2: ruby main.rb TableFactory\n" end ## main if ARGV.length != 1 usage() exit(0) end factory = Factory.getFactory(ARGV[0]) asahi = factory.create(:Link, "ASAHI newspaper", "http://www.asahi.com/") yomiuri = factory.create(:Link, "YOMIURI newspaper", "http://www.yomiuri.co.jp/") us_yahoo = factory.create(:Link, "Yahoo!", "http://www.yahoo.com/") jp_yahoo = factory.create(:Link, "Yahoo!Japan", "http://www.yahoo.co.jp/") excite = factory.create(:Link, "Excite", "http://www.excite.com/") google = factory.create(:Link, "Google", "http://www.google.com/") traynews = factory.create(:Tray, "Newspaper") traynews.add(asahi) traynews.add(yomiuri) trayyahoo = factory.create(:Tray, "Yahoo!") trayyahoo.add(us_yahoo) trayyahoo.add(jp_yahoo) traysearch = factory.create(:Tray, "Search Engine") traysearch.add(trayyahoo) traysearch.add(excite) traysearch.add(google) page = factory.create(:Page, "LinkPage", "YUKI, Hiroshi") page.add(traynews) page.add(traysearch) page.output()
Abstract Factoryパターンというのは,たいして大きくないものを作る時には重要じゃないような気がします.逆に融通はきかなくなりますし.一方,何かのフレームワークを作る時など,同じパターンの部品を別々のクラスで作りたい場合には有用のようです.
固めておきました.こちらからどうぞ.
<URL:src-abstractfactory.tar.gz>
このソースは,結城浩さんによる『Java言語で学ぶデザインパターン入門』を元に,たかはしが Ruby用に手を入れたものです.Rubyとして自然なソースにするようにしたため,あんまり原型を留めてません.
オリジナルのソースのライセンスは, <URL:http://www.hyuki.com/dp/index.html#download> にあります.このソースの扱いも上記と同様でお願いします.
文責: たかはし(maki@rubycolor.org)