Factory Method パターンは自分の中では理解しにくく、Ruby の例で説明したものがあまりなかったので今回記事を書くことにしました。
参考にしたのは「Rubyによるデザインパターン」の書籍です。もう販売されていなくて中古でしか買えないですが、Ruby プログラマーがデザインパターンを学ぶのにすごいいい本だと思います。
どうしてもデザインパターンは Java や C++ で説明されているものが多く、これらの言語を触ったことがないと読むのが中々しんどかったです。
何となくは分かるんですが詳細が分からず、この本にたどり着きました。
Factory Method パターンとは
Factory Method パターンとはクラスの選択をサブクラスに押し付けるテクニックのことです。
「オブジェクト指向における再利用のためのデザインパターン」では以下のようにあります。
オブジェクトを生成するときのインターフェースだけを規定して、実際に度のクラスをインスタンス化するかはサブクラスが決めるようにする。Factory Method パターンは、インスタンス化をサブクラスに任せる。
例を見ないと理解できないので、早速実際に例を見ていきます。
Factory Method パターンを使わない例
class Duck
def initialize(name)
@name = name
end
def eat
puts "Duck #{@name} is eating"
end
def speak
puts "Duck #{@name} says Quack!"
end
def sleep
puts "Duck #{@name} sleeps quietly"
end
end
class Pond
def initialize(number_ducks)
@ducks = []
number_ducks.times do |i|
ducks = Duck.new("Duck#{i}")
@ducks << ducks
end
end
def simulate_one_day
@ducks.each { |duck| duck.speak }
@ducks.each { |duck| duck.eat }
@ducks.each { |duck| duck.sleep }
end
end
上記の例では、Pond クラスが直接 Duck オブジェクトの生成を行っており、新しく池にいる生物 の種類を追加したい場合にこれを変更する必要があります。
例えば、Frog (カエル)クラスを追加したい場合を考えます。
class Frog
def initialize(name)
@name = name
end
def eat
puts "Frog #{@name} is eating"
end
def speak
puts "Frog #{@name} says Crooooaaak!"
end
def sleep
puts "Frog #{@name} sleeps quietly"
end
end
しかし、Pond クラスで直接 Dack.new しているので、ここを変更しなければなりません。
この例では変わらないもの(Pond クラスの機能)と変わるもの(生物の種類)が混在しているため、これらを引き剥がすことを考えます。
この引き剥がすときに Factory Method パターンを使っていきます。
Factory Method パターンを使う例
冒頭でも言っているように Factory Method パターンとは、クラスの選択をサブクラスに押し付けるテクニックです。
ここでは、Duck.new の部分をサブクラスに押し付けるようにします。
実際に Factory Method パターンを使ってリファクタリングすると以下のようになります。
class Pond
def initialize(number_animals)
@animals = []
number_animals.times do |i|
animal = new_animal("animal#{i}")
@animals << animal
end
end
def simulate_one_day
@animals.each { |animal| animal.speak }
@animals.each { |animal| animal.eat }
@animals.each { |animal| animal.sleep }
end
end
class DuckPond < Pond
def new_animal(name)
Duck.new(name)
end
end
class FrogPond < Pond
def new_animal(name)
Frog.new(name)
end
end
これにより、コンストラクタではサブクラスが実装する #new_animal
メソッドを呼び出すようになっています。
これにより、正しい池の種類を選ぶだけでよくなりました。
上記をクラス図に表すと以下のようになります。

Animal クラスはありませんが、Factory Method パターンの説明では通常、抽象クラスが登場するので、ここでは書いています。
今回の例でも Duck オブジェクトと Frog オブジェクトが全く同じインターフェースを持っていることを前提にしているので、暗黙的に Animal 抽象クラスが存在すると考えて問題ないです。
パラメータ化された Factory Method パターン
上記の例に植物についても追加したいという要件が来たとします。
class Algae
def initialize(name)
@name = name
end
def grow
puts "The Algae #{@name} soaks up the sun and grows"
end
end
class WaterLily
def initialize(name)
@name = name
end
def grow
puts "The water lily #{@name} floats, soaks up the sun and grows"
end
end
この場合、Pond クラスを以下のように修正する必要があります。
class Pond
def initialize(number_animals, number_plants)
@animals = []
number_animals.times do |i|
animal = new_animal("animal#{i}")
@animals << animal
end
@plants = []
number_plants.times do |i|
plant = new_plant("plant#{i}")
@plants << plant
end
end
def simulate_one_day
@animals.each { |animal| animal.speak }
@animals.each { |animal| animal.eat }
@animals.each { |animal| animal.sleep }
@plants.each { |plant| plant.grow }
end
end
さらにサブクラスも修正が必要になります。
class DuckWaterLilyPond < Pond
def new_animal(name)
Duck.new(name)
end
def new_plant(name)
WaterLily.new(name)
end
end
class FrogAlgaePond < Pond
def new_animal(name)
Frog.new(name)
end
def new_plant(name)
Algae.new(name)
end
end
上記のように new_animal メソッドと new_plant メソッドをそれぞれ作成する必要があります。
これにパラメータ化された Factory Method パターンを適用すると以下のようになります。
class Pond
def initialize(number_animals, number_plants)
@animals = []
number_animals.times do |i|
animal = new_organism(:animal, "animal#{i}")
@animals << animal
end
@plants = []
number_plants.times do |i|
plant = new_organism(:plant, "plant#{i}")
@plants << plant
end
end
def simulate_one_day
@animals.each { |animal| animal.speak }
@animals.each { |animal| animal.eat }
@animals.each { |animal| animal.sleep }
@plants.each { |plant| plant.grow }
end
end
class DuckWaterLilyPond < Pond
def new_organism(type, name)
if type == :animal
Duck.new(name)
elsif type == :plant
WaterLily.new(name)
else
raise "Unknown organism type: #{type}"
end
end
end
これでメソッドが一つになり、他の種類の生物、(例えば魚など)を追加するときにメソッドを丸ごと追加せずに済むようになりました。
この例ではただインスタンス化しているだけなのでメリットは薄いですが、コンストラクタで別の処理等していた場合にコードの重複を無くせるのでこれを利用するメリットが出てきます。
Factory Method の問題点
Factory Method パターンではオブジェクトの型ごとにサブクラスを必要とします。動物や植物の種類が増えていき、されにその組み合わせも複雑したときに必要なサブクラスの量が指数関数的に増えてしまいます。
例えば、魚が増えた場合、FishAlgaePond、FishWaterLilyPond のように増えます。これが5個、10個と増えていった場合、クラスの数が何十個とできることになります。
これではメンテナンスや管理しやすさの点でよくないです。
これを解決するためにクラスを引数にとり、それを使うことでサブクラスを一層することができます。
これは Ruby はクラスもオブジェクトであるという特徴からこのようなことができます。
class Pond
def initialize(number_animals, animal_class, number_plants, plant_class)
@animal_class = animal_class
@plant_class = plant_class
@animals = []
number_animals.times do |i|
animal = new_organism(:animal, "animal#{i}")
@animals << animal
end
@plants = []
number_plants.times do |i|
plant = new_organism(:plant, "plant#{i}")
@plants << plant
end
end
def simulate_one_day
@animals.each { |animal| animal.speak }
@animals.each { |animal| animal.eat }
@animals.each { |animal| animal.sleep }
@plants.each { |plant| plant.grow }
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
クラスの選択をサブクラスに押し付けていないので、これはもう Factory Method パターンではありません。
この新しい Pond クラスを使えば以下のように動物と植物のクラスを渡せばいいだけになります。
pond = Pond.new(3, Duck, 2, WaterLily)
pond.simulate_one_day
このアプローチにより、たくさんあったクラスを Pond クラスのみに押しやることができました。
次に要件として、池以外の生息地も対応してほしいというのが来たとします。
例えばジャングルの動物と植物のクラスが必要になったとします。
class Tree
def initialize(name)
@name = name
end
def grow
puts "The tree #{@name} grows tall"
end
end
class Tiger
def initialize(name)
@name = name
end
def eat
puts "Tiger #{@name} eats anything it wants"
end
def speak
puts "Tiger #{@name} Roars!"
end
def sleep
puts "Tiger #{@name} sleeps anywhere it wants"
end
end
これに対応するには、Pond クラスの名前の改修が必要です。Habitat がよさそうです。
jungle = Habitat.new(1, Tiger, 4, Tree)
jungle.simulate_one_day
問題なく少ない変更で対応することができました。
しかしこの場合の問題点は、あり得ない組み合わせを作れてしまうことです。
例えば、以下のようにトラとスイレンを同じ生態系としてインスタンス化できますが、これがおかしいことを誰も教えてくれていません。
unstable = Habitat.new(2, Tiger, 4, WaterLily)
この問題を解決するために使うのが Abstract Factory パターンです。
このパターンについては次回に回したいと思います。
さいごに
Rails のプロジェクトで自分たちが書くコードでは Factory Method パターンをあまり見ないですが、知っているだけでコードの見方が変わるのでデザインパターンを知っておくことはいいコードを書く上でとても大切だと感じています。
ここまで読んでいただきありがとうございました!