Rubyデザインパターン 8日目 : Decorator

  • 9
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第8日目はDecoratorです。(http://www.amazon.co.jp/gp/product/4894712857/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4894712857&linkCode=as2&tag=morizyun00-22)

スクリーンショット 2015-07-27 11.25.28.png

 8日目 Decorator

今回のパターンは、オブジェクトにレイヤ状に機能を追加できるようにするパターンです。

アイスに例えると、みなさんもアイス屋さんでアイスを買うときにいろんな味を重ねたりした事があると思います。チョコレート味の上に抹茶味を載せたり、またあるときは、ストロベリー味の上にミント味を載せ、またその上にピスタチオ味をのせたりと。

そういったように、機能同士を柔軟に組み合わせる事ができるようなパターンになります。

Decorator サンプルコード

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

class WriterDecorator
  def initialize(real_writer)
    @real_writer = real_writer
  end

  def write_line(line)
    @real_writer.write_line
  end

  def pos
    @real_writer.pos
  end

  def rewind
    @real_writer.rewind
  end

  def close
    @real_writer.close
  end
end

最も基本的なクラスであるSimpleWriterクラスを作ります。
そして次にデコレーションをするためのWriterDecoratorクラスを作成します。
構成としてはほぼ同じインターフェイスを提供しています。

ここで単なるテキストを出力するSimpleWriterの機能を拡張するためにのコンポーネントを作成します。


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}")
    @line_number += 1
  end
end

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

NumberingWriterクラス、TimeStampingWriterクラスのインターフェイスはともに基礎となるSimpleWriterとコアの部分はほとんど変わりません。
どれも共通してwrite_lineメソッドを持っています。

writer = NumberingWriter.new(TimeStampingWriter.new(SimpleWriter.new('final.txt'))) 
writer.write_line('気力にかくるなかりしか')

プログラム実行の際はこういった形で、委譲に次ぐ委譲形式になります。
タイムスタンプだけ表示したければ下記のようにすればいいです。

writer = TimeStampingWriter.new(SimpleWriter.new('hoge.txt'))
writer.write_line('不精にわたるなかりしか')

こういったように、欲しい機能だけ委譲形式で渡せばいいのです。
なので、すべてのコンポーネントでインターフェイスを揃えておく必要があります。
つまり、
1. ConcreteComponentクラス(SimpleWriter)
2. Decoratorクラス(WriterDecorator)
3. Componentクラス(NumberingWriter,TimeStampingWriter)

このすべてのクラスがwrite_lineメソッドを持ち合わせている事が必要だということです。
以上がDecoratorパターンの基本形となります。

Rubyらしくしてみましょう!

お待たせしました。Ruby祭りの時間です〜〜〜
上記で実現したことをRuby特有の形にしていきましょう。

パターン 1(特異メソッド形式)

w = SimpleWriter.new('out')

class << w
  alias old_write_line write_line
  def write_line(line)
    old_write_line("#{Time.new}: #{line}")
  end
end

w.old_write_line("old!") # 文章のみ "old!"
w.write_line("new!")     # 時刻も出力される "2015-08-04 14:14:56 +0900: new!"

特異メソッドを使うと、わざわざ機能拡張のためのクラスを作らなくて済みます。
aliasを呼び出したところではSimpleWriterオブジェクトの元々持っていたwrite_lineが呼び出されてold_write_lineがエイリアスに設定されます。
aliasが先に書いてないとold_write_lineが定義できず、今回の結果にならないので注意しましょう。

パターン 2(モジュール形式)


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

module NumeberingWriter
  attr_reader :line_number

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

w = SimpleWriter.new('out')
w.extend(NumberingWriter)
w.extend(TimeStampingWriter)

w.write_line('hello')

機能をモジュール形式にして追加するやり方です。
ただしこの方法だと欠点が一つあり、後でmoduleを追い出す方法がありません。

どちらの方法もメリットデメリットがありそうですので、導入の際にはしっかり検討したほうがいいでしょう

Rubyでの実例

module Logging
  def log(message)
    puts message
  end

  def log_with_timestamp(message)
    log_without_timestamp("#{Time.now}: #{message}")
  end

  alias_method :log_without_timestamp, :log
  alias_method :log, :log_with_timestamp
end

include Logging

log('hello') # 2015-08-04 14:45:17 +0900: hello
log_without_timestamp('hello') # hello

alias_methodを使うことによってタイムスタンプありなしそれぞれのメソッドを持ったモジュールを作成することができます

Railsでの実例

module Logging
  def log(message)
    puts message
  end

  def log_with_timestamp(message)
    log_without_timestamp("#{Time.now}: #{message}")
  end

  alias_method_chain :log, :timestamp
end

include Logging
log('hello')

RailsのActiveSupportライブラリを使用すると、alias_method_chainメソッドを使用することができます。
こうすることによって、非常にシンプルに拡張メソッドとエイリアスを作ることができますね!
こちらはあくまでRailsでしか使えないので、その点だけは注意です!
(ちなみに素のRubyでどうしても事前にActivesupportCoreExtensionをロードしておけばできます)

require 'active_support'
require 'active_support/core_ext'

 まとめ

Decoratorパターンは、基礎となる一つのクラスと、それと組になる一つのデコレータを作成します。
コンポーネントはその基礎となるクラスやデコレータと同じインターフェイスを持っており、コンポーネントに多様性を持たせることによって基礎となるクラスの機能を拡張します。

Decoratorの特に便利な部分は、すべてのクラスでインターフェイスが統一してあるので、設計した後で心配せずにどんどん機能を追加できるところでしょう。

このパターンを候補に入れておくと、似たようなクラスをたくさん作りたいからといってどんどん継承関係を構築していったら、いつの間にかコントロール不可能なプログラムができあがっていた、という悲惨な事態を防げるかもしれません。

参照