Murayama blog.

プログラミング教育なブログ

Decorator

Rubyによるデザインパ(ry
提供は、

Rubyによるデザインパターン

Rubyによるデザインパターン


本日はDecoratorパターンを勉強します。


Decoratorパターンは、既存のオブジェクトに対しての機能追加を実現するパターンです。
decorateは、装飾する、という意味になります。
オブジェクトに機能という名の装飾を施していくパターンかな、と。


上の最後の1行、スベってる感があるな。今日は不調かも。


JavaのDecoratorパターンといえば、
BuffererdReaderといったIOのAPIが思い浮かびます。
BuffererdReaderの場合は、ファイル読み込みというベース機能に、バッファリング機能を装飾してると考えることができます。


今回は本に載っているとおり、
ファイル出力機能をDecoratorパターンで実装してみます。
ただし、ファイルを出力する際に、必要に応じて以下の機能を追加できるようにします。

  • 行番号の表示
  • タイムスタンプの表示


まずはファイル出力を行うSimpleWriteクラスを作成します。
名前のとおり、シンプルにファイル出力のみを行うクラスです。

class SimpleWriter
  def initialize(path)
    @file = File.open(path, "w")
  end
  
  def write_line(line)
    @file.print(line)
    @file.print("\n")
  end
  
  def pos
    @file.pos
  end
  
  def rewind
    @file.rewind
  end
  
  def close
    @file.close
  end
end

データを出力するwrite_lineメソッド、ファイル出力ポジションをコントロールするposメソッド、rewindメソッド、ファイル出力を閉じるcloseメソッドを実装しています。


SimpleWriteクラスを呼び出すプログラムは以下のようになります。

f = SimpleWriter.new("file1.txt")
f.write_line("Hello world.")
f.close

結果として、file1.txtファイルにHello world.が出力されます。


上記のSimpleWriterクラスに、行番号出力機能を装飾するNumberingWriterクラスを作成します。
NumberingWriterクラスは以下のとおりです。

class NumberingWriter < WriterDecorator

  def initialize(real_writer)
    super(real_writer)
    @line_number = 1
  end
  
  def write_line(line)
    @real_writer.write_line("#{@line_number} : #{line}")
  end
end

NumberingWriterクラスは、
コンストラクタの引数で受け取ったオブジェクトをインスタンス変数に保持します。
write_lineメソッドでは、行番号出力を施したあと、
コンストラクタで受け取ったオブジェクトに処理を委譲します。


つづいて、タイムスタンプ出力機能を装飾するTimestampoingWriterクラスです。

class TimestampingWriter < WriterDecorator
  def write_line(line)
    @real_writer.write_line("#{Time.new} : #{line}")
  end
end


また、NumberingWriterクラス、TimestampingWriterクラスはともにWriteDecoratorクラスを継承しています。
WriteDecoratorクラスは、NumberWriterクラス、TimestampingWriterクラスの共通機能を切り出したクラスになります。
話が前後しますが、このクラスでインスタンス変数real_writerを定義しているのが、Decoratorパターンのポイントになります。

class WriterDecorator
  def initialize(real_writer)
    @real_writer = real_writer
  end
  
  def write_line(line)
    @real_writer.write_line(line)
  end
  
  def pos
    @real_writer.pos
  end
  
  def rewind
    @real_writer.rewind
  end
  
  def close
    @real_writer.close
  end
end


では、呼び出しもとプログラムを作成してみます。

f = NumberingWriter.new(SimpleWriter.new("file1.txt"))
f.write_line("Hello world.")
f.close


ファイルに出力された結果は以下のとおりです。行番号が付加されます。

1 : Hello world.


同様にタイムスタンプの出力は、

f = TimestampingWriter.new(SimpleWriter.new("file1.txt"))
f.write_line("Hello world.")
f.close


ファイルに出力された結果は以下のとおりです。タイムスタンプが付加されます。

Fri Jul 03 17:57:24 +0900 2009 : Hello world.


で、行番号とタイムスタンプの両方の出力を追加してみます。

f = TimestampingWriter.new(NumberingWriter.new(SimpleWriter.new("file1.txt")))
f.write_line("Hello world.")
f.close


ファイルに出力された結果は以下のとおりです。行番号とタイムスタンプが付加されます。

1 : Fri Jul 03 17:58:56 +0900 2009 : Hello world.


以上がDecoratorパターンのサンプルになります。


Decoratorパターンのクラス図を見てみます。

  • Component
  • ConcreteComponent
    • ベースとなる処理をもつクラス。SimpleWriterクラスが該当する。
  • Decorator
    • ベースとなる処理に機能を装飾するクラス。NumberingWriterクラス、TimestampingWriterクラスが該当する。


ここから、RubyらしくDecoratorパターンをイジってみます。
NumberingWriterクラス、TimestampingWriterクラスの基底クラスであるWriterDecoratorクラスはForwardableモジュールを使用するとシンプルになります。

require "forwardable"
class WriterDecorator 
  extend Forwardable
  
  def_delegators :@real_writer, :write_line, :pos, :rewind, :close

  def initialize(real_writer)
    @real_writer = real_writer
  end
end

もともと、WriterDocoratorクラスは、real_writerオブジェクトへ処理を委譲していました。
処理の委譲は、forwardableモジュールのdef_delegatorsメソッドで行うことができます。
#ということを初めて知りました。勉強して良かった。
method_missingメソッドでも似たようなことがでますが、forwardableの方がわかりやすいです。
#method_missingは委譲するメソッドの数が多くなった場合に便利と本には書いています。このへんは上手く使い分けると良いですね。


まだまだあります。
次は実行時にオブジェクトを拡張する方法を見てみます。

f = SimpleWriter.new("file1.txt")
class << f
  alias old_write_line write_line
  
  def write_line(line)
    old_write_line("#{Time.new} : #{line}")
  end
end

面白いのはaliasメソッドで、既存のwrite_lineメソッドにエイリアス(old_write_line)を付けているところですね。
このあと、write_lineメソッドは再定義されて上書きされてしまうのですが、
old_write_lineメソッドは上書きされる前のメソッドを参照したままになります。
#これはスゴい。Rubyのスゴさがまた一つわかった気がする。。


あとは、モジュールを使って機能を追加する方法も紹介しています。

module NumberingWriter
  attr_reader :line_number
  
  def write_line(line)
    @line_number = 1 unless @line_number
    super("#{@line_number} : #{line}")
    @line_number += 1
  end
end

module TimestampingWriter
  def write_line(line)
    super("#{Time.new} : #{line}")
  end
end


NumberingWriter、TimestampingWriterをモジュールとして定義すると、
呼び出しもとプログラムは以下のようになります。

f = SimpleWriter.new("file1.txt")
f.extend(NumberingWriter)
f.extend(TimestampingWriter)
f.write_line("Hello world3.")
f.close


実行時にモジュールを追加する場合はextendメソッドを使います。
#このへんの感覚がRubyは難しいな。まだ慣れない。


委譲、じゃなかった、
以上、Decoratorパターンのお話でした。