Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第6日目はAdapterです。(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)
6日目 Adapter
6日目はAdapterパターンです。
イメージとしては、名前の通り、変換器のような機能を作る時のパターンです。
具体的なコードに移ります。
Adapter サンプルコード
class Render
def render(text_object)
@text = text_object.text
@size = text_object.size_inches
@color = text_object.color
printData
end
private
def printData
puts @text
puts @size
puts @color
end
end
これは与えられたオブジェクトの情報を表示するクラスです。
次にデータを提供するオブジェクトに移ります。
class TextObject
attr_reader :text, :size_inches, :color
def initialize(text, size_inches, color)
@text = text
@size_inches = size_inches
@color = color
end
end
実際にデータを表示する時には、
text_object = TextObject.new("テキストです", 12, "red")
render_object = Render.new
render_object.render(text_object)
こういった形になると思います。何も問題ありませんね。
さて、次にこの状態から、他にもRenderクラスによって表示させたいオブジェクトができたとしましょう。
属性の名前がちょっと違う!?
さて、次に表示させたいオブジェクトがこんなクラスだった時はどうすればいいでしょうか?
class BritishTextObject
attr_reader :string, :size_mm, :colour
# ....
end
先ほどのTextObject
と比べてみましょう。
class TextObject
attr_reader :text, :size_inches, :color
#....
end
データ型は同じだとして、属性名が異なっています。
これでは、データを表示させるためのRenderクラスで不具合が起きてしまいます!
こういった時に**Adapter(変換器)**が解決法を導いてくれます。
class BritishTextObjectAdapter < TextObject
def initialize(bto)
@bto = bto
end
def text
return @bto.string
end
def size_inches
return @bto.size_mm / 25.4
end
def color
return @bto.colour
end
end
使い方は簡単
class Render
def render(text_object)
@text = text_object.text
@size = text_object.size_inches
@color = text_object.color
printData
end
private
def printData
puts @text
puts @size
puts @color
end
end
class TextObject
attr_reader :text, :size_inches, :color
def initialize(text, size_inches, color)
@text = text
@size_inches = size_inches
@color = color
end
end
class BritishTextObject
attr_reader :string, :size_mm, :colour
def initialize(string, size_mm, colour)
@string = string
@size_mm = size_mm
@colour = colour
end
end
class BritishTextObjectAdapter
def initialize(bto)
@bto = bto
end
def text
@bto.string
end
def size_inches
@bto.size_mm / 25.4
end
def color
@bto.colour
end
end
british_text_object = BritishTextObject.new("Momozono", 12, "red")
fixed_british_text_object = BritishTextObjectAdapter.new(british_text_object) #この時点で属性名が変換される
render_object = Render.new
render_object.render(fixed_british_text_object)
BritishTextObjectAdapterをクラスとして作成し、引数としてBritishTextObjectを迎える形でRenderクラスで出力する際に必要な属性名に変換しています。
Rubyらしい構成に...
上記の方法は結構堅苦しい方法です。ここからコードをRubyらしくリファクタリングしてみましょう。
ここではオープンクラスを利用します。
オープンクラスってなんやっけ?
class Array
def shuffle
"シャッフルでけへんで!!!"
end
end
array = %w[hoge huga foobar]
array.shuffle # => シャッフルでけへんで!!!
オープンクラスを使うとこういった形で、元より定義されているメソッドを上書きしたり、新しいメソッドを追加したりすることができます。モンキーパッチとも言いますね。
AdapterをRubyのオープンクラスで表現してみましょう。
class BritishTextObject
attr_reader :string, :size_mm, :colour
def initialize(string, size_mm, colour)
@string = string
@size_mm = size_mm
@colour = colour
end
end
class BritishTextObject
def text
return string
end
def size_inches
return size_mm / 25.4
end
def color
return colour
end
end
british_text_object = BritishTextObject.new("momozonno", 12, "red")
british_text_object.text # => "momozono"
british_text_object.size_inches# => 0.4724409448818898
british_text_object.color# => "red"
わざわざAdapterクラスを作ることなく、Adapterの機能が完全に再現できていますね。
けど、クラス内部あんまりいじくりたくない...
そんな時もあるでしょう。そういうときは特異メソッドで対応するのがいい解決法です。
Rubyにおける特異メソッドは、オブジェクト(ここではインスタンス)単体だけに特定のメソッドを持たせたいときに便利です。
大事な大事なクラスを汚染したりすることもありません!
bto = BritishTextObject.new('hello', 50.8, "rainbow")
class << bto
def color
colour
end
def text
string
end
def size_inches
size_mm / 25.4
end
end
bto.color # => "rainbow"
まとめ
Adapterは、似たようなオブジェクトの属性名の変更を可能にしました。
Adapterをクラスで定義したり、オープンクラスでメソッドを追加したり、特異メソッドでクラス汚染を避けたりと、いろいろな方法があります。
どれを選択するかは、結局適用するプロジェクトによるでしょう。
ただし、大まかに選択する基準はあるようです。
Ruby式オープンクラスが有効な場合
- 変更が単純なとき
- クラスの仕様、機能が細部までわかるとき
Adapterクラスの構築が有効な場合
- そのクラスの仕様がわからない
- カプセル化という点ではAdapterクラスの構築のほうが有効です。