Template Method パターン

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

#!/usr/local/bin/ruby

class AbstractDisplay
  def display
    self.open
    5.times do
      self.print
    end
    self.close
  end
end

class CharDisplay < AbstractDisplay
  def initialize(ch)
    @ch = ch
  end
  
  def open
    Kernel.print "<<"
  end

  def print
    Kernel.print "#@ch"
  end

  def close
    puts ">>"
  end
end

class StringDisplay < AbstractDisplay
  def initialize(string)
    @string = string
  end

  def open
    print_line
  end

  def print
    puts "|#@string|"
  end

  def close
    print_line
  end

  def print_line
    Kernel.print "+"
    @string.length.times do
      Kernel.print "-"
    end
    puts "+"
  end

  private :print_line
end

displays = [
  CharDisplay.new('H'),
  StringDisplay.new('Hello, world.'),
  StringDisplay.new('こんにちは')
]

displays.each do |d|
  d.display
end

実行結果は、

<<HHHHH>>
+-------------+
|Hello, world.|
|Hello, world.|
|Hello, world.|
|Hello, world.|
|Hello, world.|
+-------------+
+----------+
|こんにちは|
|こんにちは|
|こんにちは|
|こんにちは|
|こんにちは|
+----------+

です。一瞬、はまちちゃんが出たかと思いました。

Template Method はいわばコールバックをサブクラスで実装するようなものです。なので yield との相性がよさそうです。このコードでは AbstractDisplay#display の中で

    self.open
    5.times do
      self.print
    end
    self.close

として、前処理(open) と後処理 (close) の間の処理が 5.times.do とか決めうちな感じになっています。(だからこそ Template Method なのですが) が、出力されるテキストは別に 5 回に限定する必要もなく、クラスを利用する側から決められてもよさそう。

ということでここを yield にしてみます。

class AbstractDisplay
  def display
    self.open
    yield(self)
    self.close
  end
end

すると、

displays.each do |d|
  d.display do |d|
    3.times { d.print }
  end
end

と中核の処理を、クラスを利用する側が記述できるようになります。ただ、これだとクラスを利用する側が中核の処理を必ず実装しなければならなくなってちょっと嫌なので、デフォルトでは 5.times do print; end をするとして、ブロックを受け取ったときだけその処理を実行するようにしてみます。

あるメソッドがブロックを受け取ったかどうかを調べる方法はいくつかあるようです。

  • Kernel.block_given? で調べる
  • アンパサンドつき引数 (Proc オブジェクト) を使う

アンパサンドつき引数は、クロージャを実現するためにも使われたりします。ここでは柔軟性の高い後者でやってみます。

class AbstractDisplay
  def display(&block)
    self.open
    if (block)
      block.call(self)
    else
      5.times do
        self.print
      end
    end
    self.close
  end
end

とすると、

displays.each do |d|
  d.display
end

では先と同じ出力、

displays.each do |d|
  d.display do |d|
     3.times { d.print }
  end
end

だとブロックの処理が優先されて、出力が三回になります。