Edited at

Rubyデザインパターン 10日目 : Factory

More than 3 years have passed since last update.

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)

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


 10日目 Factory

Factoryは、オブジェクトの生成とその動作部分を分けた上で、生成すべきオブジェクトを正しく選択するのためのパターンです。

Factoryの中でも二通りの表現方法があります。

1. Factory Method

2. 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の力でPondOrganismFactoryJungleOrganismFactoryなどの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のダックタイピングによってオブジェクト生成を管理するクラスを一般化し、柔軟性をあげることができました。

それと同時に、対象のオブジェクトをカプセル化することにも成功しました。