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)
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('不精にわたるなかりしか')
こういったように、欲しい機能だけ委譲形式で渡せばいいのです。
なので、すべてのコンポーネントでインターフェイスを揃えておく必要があります。
つまり、
- ConcreteComponentクラス(
SimpleWriter
) - Decoratorクラス(
WriterDecorator
) - 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の特に便利な部分は、すべてのクラスでインターフェイスが統一してあるので、設計した後で心配せずにどんどん機能を追加できるところでしょう。
このパターンを候補に入れておくと、似たようなクラスをたくさん作りたいからといってどんどん継承関係を構築していったら、いつの間にかコントロール不可能なプログラムができあがっていた、という悲惨な事態を防げるかもしれません。