その他

【Ruby で解説するデザインパターン】Factory Method パターン

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 パターンをあまり見ないですが、知っているだけでコードの見方が変わるのでデザインパターンを知っておくことはいいコードを書く上でとても大切だと感じています。

ここまで読んでいただきありがとうございました!

ABOUT ME
酒井 駿
名古屋工業大学大学院卒業後、豊田合成(株)で品質管理を経験し、その後スタートアップ・マネーフォワードを経て、2024年11月に株式会社EGGHEAD創業。 製造業とエンジニアリング、両方の現場の知見を活かし、製造業における生成AIを活用した業務改善やシステム開発を支援します。