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)