Design Patterns: Ruby Companion

2 AbstractFactory Pattern

2.1 Intent

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

2.2 Implementation of AbstractFactory Pattern

There are 4 files in Ruby.

files "foofactory.rb" are not only factory class. they include other parts (List, Tray and Page) creater classes.

2.2.1 Smaple 1: normal implementation

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
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
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
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()

2.2.2 Sample 2: Constant Method

This sample uses Constant Method Pattern (Idiom). Constant Method Pattern is a kind of Template Method Pattern. It return the class itself, not instance of the class.

## 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
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
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
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()

2.2.3 Sample 3: Parts Catalog

This sample uses Parts Catalog Idiom. Parts Cagalog is Hash object, which is belong in the factory object. The hash has symbols as its key, and related Class objects as its value. When you create objects, you can send a method new to hash's value.

No method is defined in subclasses of Factory class; super classes's methods are used in them.

## 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
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
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
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()

2.2.4 Sample 3a: Parts Catalog of Class Instance Variable

This sample uses class instance variable as parts catalog.

## 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
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
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
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()

2.2.5 Sample 4: Factory by Naming Contract

In this sample, there is no definition of methods and variables in subclasses of factory class. We just only define classes themselves. But you must use name their classes regularly.

For example, when you want to create some Link objects using ListFacotry and TableFactory, the name of their classes should be ListLink and TableLink.

The drawback of this method is to degrade readablity of the source code. You can add comments.

## 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
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
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
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()

2.3 Sources

<URL:src-abstractfactory.tar.gz>

2.4 LICENSE

Original version of this sources are written by YUKI Hiroshi-san in "Introduction to Design Patterns with Java", and modified totally by TAKAHASHI Masayoshi to use Ruby.

License of Origanal sources is <URL:http://www.hyuki.com/dp/index.html#download> (in Japanese and English). This modified version is under the same license.

Author: TAKAHASHI 'Maki' Masayoshi (maki@rubycolor.org)