Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第10日目はFactoryです。(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)
10日目 Factory
Factoryは、オブジェクトの生成とその動作部分を分けた上で、生成すべきオブジェクトを正しく選択するのためのパターンです。
Factoryの中でも二通りの表現方法があります。
- Factory Method
- Abstract Factory
これから順にサンプルコードとともに解説していこうと思います。
Factory サンプルコード
1. Factory Method
動物であるアヒルと、植物であるスイレン。どちらも池の中で生息している状態を表現してみましょう。
class Duck
def initialize(name)
@name = name
end
def eat
puts("アヒル#{@name}は食事中です")
end
def speak
puts("アヒル#{@name}がガーガー鳴いています")
end
def sleep
puts("アヒル#{@name}は静かに眠っています")
end
end
class WaterLily
def initialize(name)
@name = name
end
def grow
puts "スイレン #{@name}は浮きながら日光を浴びて育ちます"
end
end
class Pond
def initialize(number_animals, animal_class, number_plants, plant_class)
@animal_class = animal_class
@plant_class = plant_class
@animal = []
number_animals.times do |i|
animal = new_organism(:animal, "動物#{i}")
@animals << animal
end
@plants = []
number_plants.each do |i|
plant = new_organism(:plant, "植物#{i}")
@plants << plant
end
end
def simulate_one_day
@plants.each {|plant| plant.glow }
@animals.each {|animal| animal.speak }
@animals.each {|animal| animal.eat }
@animals.each {|animal| animal.sleep }
end
def new_organism(type, name)
if type == :animal
@animal_class.new(name)
elsif type == :plant
@plant_class.new(name)
else
raise "Unknown organism type: #{type}"
end
end
end
pond = Pond.new(3, Duck, 2, WaterLily)
pond.simulate_one_day
=begin
スイレン 植物0は浮きながら日光を浴びて育ちます
スイレン 植物1は浮きながら日光を浴びて育ちます
アヒル動物0がガーガー鳴いています
アヒル動物1がガーガー鳴いています
アヒル動物2がガーガー鳴いています
アヒル動物0は食事中です
アヒル動物1は食事中です
アヒル動物2は食事中です
アヒル動物0は静かに眠っています
アヒル動物1は静かに眠っています
アヒル動物2は静かに眠っています
=end
Duck
とアヒルの生活様式、WaterLily
はスイレンについての生活様式について述べるクラスです。それぞれ動物と植物なので、当然起こす行動も違います。
アヒルは寝たり食べたりしますが、スイレンは日光を浴びて成長するだけです。
どちらもPond(池)に生息する生き物なので、Pond
クラスを作ってその二つを管理します。
simulate_one_day
はそれぞれの1日の生活を表現します。
作りたいオブジェクトのクラスをインスタンス変数に格納することで、非常に簡潔に書かれています。
こういった構成にすることで、池に住む動・植物だったら特になにも考えることなくどんどん追加できるでしょう。
実行時も、オブジェクトの数と種類を書くだけ。非常に簡潔です。
2. Abstract Factory
ここから機能を拡張する要望がきたとしましょう。池に住むオブジェクトだけではなく、ジャングル全体を扱うプログラムをつくる要望がきてしまいました。
まず私たちが取る行動は、先ほどのクラスの名前を変更して、とても単純な形で要望に答える短絡的な方法でやってみましょう。
Pond
クラスの名前をHabitat
(生息地)として、そのままそのクラスにトラや、ラフレシア、リスザルなどを入れることです。
それでもHabitat
クラスはちゃんと動いてくれます。柔軟な設計していたおかげでしょう。
jungle = Habitat.new(1, Tiger, 4, Tree)
jungle.simulate_one_day
しかし、今の設計だと、一つだけ問題点があります。
以下のようなことができてしまうからです。
jungle = Habitat.new(1, Duck, 3, Rafflesia)
アヒルとラフレシアの組み合わせなども作れてしまいます。アヒルとラフレシアが共生している場面はあまり考えられませんよね。
こういったジャングルのように、想定されるオブジェクトが増えるにつれて、AbstractFactoryパターンの真価が発揮できます。
同じグループのオブジェクトをくくってしまいましょう
class PondOrganismFactory
def new_animal(name)
Frog.new(name)
end
def new_plant(name)
Algae.new(name)
end
end
class JungleOrganismFactory
def new_animal(name)
Tiger.new(name)
end
def new_plant(name)
Tree.new(name)
end
end
池に所属するものはPondOrganismFactory
、ジャングルに所属するものはJungleOrganismFactory
にくくってまとめてしまいます。
これで、トラ(Tiger)と藻(Algae)のような、組み合わせたくないもの同士が実現しないような構成にすることができました。
ちなみに使いかたは下記のようになります。
class Habitat
def initialize(number_animals, number_plants, organism_factory)
@organism_factory = organism_factory
@animals = []
number_animals.times do |i|
animal = @organism_factory.new_animal("動物#{i}")
@animals << animal
end
@plants = []
number_plants.times do |i|
plant = @organism_factory.new_plant("植物#{i}")
@plants << plant
end
end
def simulate_one_day
@plants.each {|plant| plant.grow }
@animals.each {|animal| animal.speak }
@animals.each {|animal| animal.eat }
@animals.each {|animal| animal.sleep }
end
end
class JungleOrganismFactory
def new_animal(name)
Tiger.new(name)
end
def new_plant(name)
Tree.new(name)
end
end
jungle = Habitat.new(1,4, JungleOrganismFactory.new)
jungle.simulate_one_day
これで柔軟性とともに、オブジェクトが増えても正しいクラスが選択できるような構成になってきました。
もう一つ改善するとすれば、Rubyの力でPondOrganismFactory
、JungleOrganismFactory
などのFactoryを一般化できないかということです。
Rubyらしく...
class OrganismFactory
def initialize(plant_class, animal_class)
@plant_class = plant_class
@animal_class = animal_class
end
def new_animal(name)
@animal_class.new(name)
end
def new_plant(name)
@plant_class.new(name)
end
end
クラスもオブジェクトであるという考え方から、クラスをさらに一般化できました。
実行時にクラスオブジェクトを格納することで、カプセル化に成功しています。
jungle_organism_factory = OrganismFactory.new(Tree, Tiger)
jungle = Habitat.new(1, 4, jungle_organism_factory)
jungle.simulate_one_day
まとめ
Factoryパターンは作ったクラスの中から、適したオブジェクトを選んできて、束ねる役割を果たします。そうすることで、オブジェクト生成を動作部分と分離することもできました。
また、Rubyのダックタイピングによってオブジェクト生成を管理するクラスを一般化し、柔軟性をあげることができました。
それと同時に、対象のオブジェクトをカプセル化することにも成功しました。