Prototype パターン

id:hyuki さんのデザインパターン本から Prototype パターンを移植してみます。

#!/usr/local/bin/ruby

class Manager
  def initialize
    @showcase = Hash.new
  end

  def register (name, proto)
    @showcase[name] = proto
  end

  def create(protoname)
    p = @showcase[protoname]
    p.create_clone
  end
end

class MessageBox
  def initialize(decochar)
    @decochar = decochar
  end

  def use(str)
    length = str.length
    (length + 4).times { print @decochar }
    puts ""
    puts "#@decochar #{str} #@decochar"
    (length + 4).times { print @decochar }
    puts ""
  end

  def create_clone
    self.dup
  end
end

manager = Manager.new
manager.register('warning box', MessageBox.new('*'))
manager.register('slash box', MessageBox.new('/'))

[manager.create('warning box'), manager.create('slash box')].each do |product|
  product.use("Hello, World!")
end

このスクリプトの実行結果は、

$ ruby prototype.rb
*****************
* Hello, World! *
*****************
/////////////////
/ Hello, World! /
/////////////////

となります。

書籍では Product というインタフェースが出てきて、MessageBox はその Product を継承しています。Product が MessageBox のインタフェースを規定します。一方の移植版は、Ruby なのでインタフェースのような概念がないため、このような実装になりました。

書籍ではもう一つ Underline という、Product を継承したクラスがでてきます。Product インタフェースによって MessageBox と Underline はどちらも同じインタフェースを持つことになり、また Manager クラスはそれらサブクラスに束縛されることなく Product インタフェースを扱うことで、結合が疎になるという例。

Ruby の場合はこの辺はダックタイピングで、ということになるかと思います。

ところで MessageBox の create_clone は移植のために便宜的に用意してますが、Ruby は Kernel#clone や Kernel#dup があるためすべてのオブジェクトが複製可能です。従ってこのメソッドは削除してしまって、Manager の create で直接 p.dup してしまっても良さそうです。

すると Manager は

class Manager
  def initialize
    @showcase = Hash.new
  end

  def register (name, proto)
    @showcase[name] = proto
  end

  def create(protoname)
    p = @showcase[protoname]
    p.dup
  end
end

となるわけですが、なんとなく Hash を中途半端に抽象化しているだけのクラスになってしまいました。いっそ Manager は Hash を継承させてしまって、create を追加したものでも良いかもしれません。

あるいは、Hash と同じインタフェースであるが、値を取り出すときはインスタンスのコピーが取り出されるというクラスにしてみるのも面白いかなと思います。これを以下のように実装してみました。

class PrototypeHash
  include Enumerable
  def initialize
    @internal = Hash.new
  end

  def each
    @internal.each do |key, value|
      yield(key, value)
    end
  end

  def []=(name, proto)
    @showcase[name] = proto
  end

  def [](proto)
    p = @showcase[proto]
    p.dup
  end
end

class MessageBox
  ...
end

manager = PrototypeHash.new
manager[:warning] = MessageBox.new('*')
manager[:slash] = MessageBox.new('/')

[manager[:warning], manager[:slash]].each do |product|
  product.use("Hello, World!")
end

うまく動きました。こういう実装の方が Ruby らしいような気がしますが、どうでしょうか。