酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

デコレータ Ruby 2.0.0 デザインパターン速攻習得[Decorator][Design Pattern]

GoFのデザインパターン(Design Pattern)の一つ、デコレータ(Decorator)をRubyのサンプルコードで紹介します。

デコレータは、既存のオブジェクトに対して簡単に機能の追加をするためのパターンです。 デコレータパターンを使うと、レイヤ状に機能を積み重ねて、必要な機能を持つオブジェクトを作ることができます。


Included file ‘custom/google_ads_yoko_naga.html’ not found in _includes directory

デコレータの構成要素

デコレータは次の2つの要素で構成されます。

具体コンポーネント(ConcreteComponent):ベースとなる処理をもつオブジェクト
デコレータ(Decorator):追加する機能を持つ

デコレータのメリット

* 既存のオブジェクトの中身を変更することなく、機能を追加できる
* 組み合わせで様々な機能を実現できる
* 継承よりも変更の影響を限定しやすい

サンプルソース

デコレータの概要を次のモデルを使って説明します。

SimpleWriter(具体コンポーネント): ファイルへの単純な出力を行う
NumberingWriter(デコレータ): 行番号出力を装飾する機能を持つ
TimestampingWriter(デコレータ): タイムスタンプを追加する機能を持つ

まず「ファイルへの出力機能」をもつSimpleWriterクラスを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ファイルへの単純な出力を行う (ConcreteComponent)
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

このクラスを動かしてみます。

1
2
3
writer = SimpleWriter.new('sample1.txt')
writer.write_line('飾り気のない一行')
writer.close

このコードを実行すると、sample1.txt飾り気のない一行が入っていました。

タイムスタンプ/行番号クラスを作成する前に、それらのクラスの共通する機能を切り出したWriteDecoratorクラスを定義します。これは、Decoratorを複数作る場合に重複したコードをできるだけ書かないための工夫です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 複数のデコレータの共通部分(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

タイムスタンプを出力するNumberingWriterクラスを定義します。 write_lineメソッドは、"#{@line_number} : #{line}"でlineに行番号を付加しています。そして、コンストラクタで受け取ったオブジェクト(SimpleWriter)のwrite_lineメソッドに処理を委譲しています。 このクラスは、デコレータパターンのDecoratorの役割を持ちます。

1
2
3
4
5
6
7
8
9
10
11
12
# 行番号出力機能を装飾する(Decorator)
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メソッドは、"#{Time.new} : #{line}"でlineにタイムスタンプを付加して、オブジェクト(SimpleWriter)のwrite_lineメソッドに処理を委譲しています。 このクラスもデコレータパターンのDecoratorの役割を持ちます。

1
2
3
4
5
6
# タイムスタンプ出力機能を装飾する(Decorator)
class TimestampingWriter < WriterDecorator
  def write_line(line)
    @real_writer.write_line("#{Time.new} : #{line}")
  end
end

ここまでがコーディング部分です。では、上のサンプルを動かしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
f = NumberingWriter.new(SimpleWriter.new("file1.txt"))
f.write_line("Hello out there")
f.close
# file1.txtに以下の内容が出力される
#1 : Hello world

f = TimestampingWriter.new(SimpleWriter.new("file2.txt"))
f.write_line("Hello out there")
f.close
# file2.txtに以下の内容が出力される
#2012-12-09 12:55:38 +0900 : Hello out there

f = TimestampingWriter.new(NumberingWriter.new(SimpleWriter.new("file3.txt")))
f.write_line("Hello out there")
f.close
# file3.txtに以下の内容が出力される
#1 : 2012-12-09 12:55:38 +0900 : Hello out there

このようにデコレータパターンでは既存のクラス(SimpleWriter)を変更することなく、 機能を自由に組み合わせて使うことがてきています。

Rubyの標準ライブラリ Forwardableによる委譲

ここではクラスに対しメソッドの委譲機能を追加するForwardableを使って先ほどのソースをリファクタリングします。この委譲とは、ある機能を持つオブジェクトにその機能での処理を依頼することです。

先ほどのサンプルソースのWriterDecoratorクラスを以下の様に修正できます。

1
2
3
4
5
6
7
8
9
10
11
12
require "forwardable"

class WriterDecorator
  extend Forwardable

  # forwardableで以下のメソッドの処理を委譲している
  def_delegators :@real_writer, :write_line, :pos, :rewind, :close

  def initialize(real_writer)
    @real_writer = real_writer
  end
end

Rubyの委譲:Forwardableとmethod_missingについて

Rubyでのメソッドの委譲は、forwardablemethod_missingを使う方法があります。それぞれの特徴を生かしてうまく使い分けるとよさそうです。

* forwardableを使う場合、委譲しているメソッドを明確にすることができる
* method_missingを使う場合は、メソッドが多い場合に有利、間違いがなくなる

Decoratorのモジュール化

Decoratorをモジュールにすることでも同様の機能を実現できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module TimestampingWriter
  def write_line(line)
    super("#{Time.new} : #{line}")
  end
end

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

このサンプルソースはGitHubにも置いています。

サンプルソース(GitHub)

Adapter/Proxy/Decoratorの違い

Adapter/Proxy/Decoratorはいずれも「別のオブジェクトの代理」パターンと言えます。 この3つの違いをシンプルに説明すると次のようになります。

Adapter: オブジェクトの不適切なインターフェイスをラップする
Proxy: ラップするオブジェクトと同じインターフェイスを持ち、一部の機能を受け持つ
Decorator: 基本的なオブジェクトにレイヤ状に機能を追加する

Special Thanks

Decorator

デザインパターン-Decorator

RubyでDecoratorパターン

デザインパターンdecorator

Amazon.co.jp: Rubyによるデザインパターン: Russ Olsen, ラス・オルセン, 小林 健一, 菅野 裕, 吉野 雅人, 山岸 夢人, 小島 努: 本

変更来歴

12/09 13:30 新規作成
12/10 09:00 GitHubへのサンプルソースの設置。導入文の修正
12/10 10:45 Adapter/Proxy/Decoratorの違いを追加
12/11 00:00 書籍へのリンクをAmazon アフィリエイトに変更
12/11 10:50 サンプルコードに説明書きを追加
06/21 17:00 Ruby2.0.0対応、読みづらい部分を修正

おすすめの書籍