GoFのDesign PatternのBuilder Pattern in Ruby

photo credit: Andrew* via photopin cc

Builder パターン(ビルダー・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。
Builder パターン – Wikipedia

Builder Patternとは作成過程が同じで異なる表現形式の結果を得るためのパターンである。

上記Wikipediaの利用例である家を建てることを例にして考えてみよう。完成する家がどのような家になるかというのは「家の作成過程」と「素材」の2つの要素で決定される。「作成過程」は「どのような順番でどこに何を配置するか」であり、「素材」は「土台には何を使って、壁には何を使って…」ということである。

このとき、「作成過程」は「日本式建築の作成過程やかまくらの作成過程…」など様々なものが考えられる。同様に「素材」も「土台はコンクリートで、壁は雪…」など様々なものが考えられる。

「作成過程」と「素材」をそれぞれ用意しておくことで、「“日本式建築”で“土台はコンクリートで壁や柱も雪”の家を建てる」という要望に柔軟に応えることができるようになる。

Builder Patternとは、このように「作成過程」を決定するDirector と呼ばれるものと「表現形式(=上記説明で言えば素材)」を決定する Builderと呼ばれるものを組み合わせることで、オブジェクトの生成をより柔軟にし、そのオブジェクトの「作成過程」をもコントロールすることができるようにするためのPatternである。

Builder Patternを構成する要素は以下の3つである。

  • Director:Builderで提供されているインタフェースを使用して処理を行う
  • Builder:各メソッドのインタフェースを定義する
  • ConcreteBuilder:Builderが定めたインタフェースの実装部分

サンプルソース

サンプルとして、理科の実験で、食塩水や砂糖水を作成について考えてみよう。

Q.1
最初に100gの水に40gの食塩を溶かして食塩水を作成する。
その食塩水の70gを捨てた後、水を100g追加し、最後に15gの食塩を溶かすと、
この食塩水の濃度は何%になるだろうか?

この問題文をDirectorとして実装すると、以下のようになる。add_solventは溶媒(=問題文では水)の追加、add_soluteは溶質(=問題文では食塩)の追加、abandon_solutionは生成物(=問題文では食塩水)の破棄を意味している。

class Director
  def initialize(builder)
    @builder = builder
  end

  def constract
    @builder.add_solvent(100)
    @builder.add_solute(40)
    @builder.abandon_solution(70)
    @builder.add_solvent(100)
    @builder.add_solute(15)
  end
end

次に、各メソッドのインターフェースとなるBuilderを作成する。

class Builder
  def initialize(class_name)
    @builder = class_name.new(0,0)
  end

  def add_solute(solute_amount)
    @builder.add_solute(solute_amount)
  end

  def add_solvent(solvent_amount)
    @builder.add_solvent(solvent_amount)
  end

  def abandon_solution(solution_amount)
    @builder.abandon_solution(solution_amount)
  end

  def result
    @builder
  end
end

最後に、Builderが定めたインタフェースの実装部分となるConcreteBuilderを作成する。

require_relative "./builder"

class SaltWaterBuilder < Builder
  attr_accessor :salt, :water
  def initialize(salt, water)
    @salt = salt.to_f
    @water = water.to_f
  end

  # Add salt
  def add_solute(salt_amount)
    @salt += salt_amount
  end

  # Add water
  def add_solvent(water_amount)
    @water += water_amount
  end

  def abandon_solution(solution_amount)
    salt_delta = solution_amount * (@salt / (@salt + @water))
    water_delta = solution_amount * (@water / (@salt + @water))
    @salt -= salt_delta
    @water -= water_delta
  end
end

最後にこのプログラムを呼び出し、実際に食塩水を生成し、食塩の量を導き出してみよう。

require_relative "./director"
require_relative "./salt_water_builder"

builder = Builder.new(SaltWaterBuilder)
director = Director.new(builder)
director.constract
p builder.result

このプログラムを実行すると、次の結果を得られる。

#

このようにBuilderに作業を担当させ、Directorは作業過程を担当することで、オブジェクトの生成が柔軟にできることが分かると思う。

続いて、同じ要領で砂糖水の作成をしてみよう。手順が変わらないので、DirectorBuilderに変更は不要である。以下の砂糖水のConcreteBuilderを追加するだけで良い。

require_relative "./builder"

class SugarWaterBuilder < Builder
  attr_accessor :sugar, :water
  def initialize(sugar, water)
    @sugar = sugar.to_f
    @water = water.to_f
  end

  # Add sugar
  def add_solute(sugar_amount)
    @sugar += sugar_amount
  end

  # Add water
  def add_solvent(water_amount)
    @water += water_amount
  end

  def abandon_solution(solution_amount)
    sugar_delta = solution_amount * (@sugar / (@sugar + @water))
    water_delta = solution_amount * (@water / (@sugar + @water))
    @sugar -= sugar_delta
    @water -= water_delta
  end
end

実際に食塩水や砂糖水を生成し、食塩の量、砂糖の量を導き出すプログラムは以下のように変更する。

require_relative "./director"
require_relative "./salt_water_builder"
require_relative "./sugar_water_builder"

builder = Builder.new(SaltWaterBuilder)
director = Director.new(builder)
director.constract
p builder.result

builder = Builder.new(SugarWaterBuilder)
director = Director.new(builder)
director.constract
p builder.result

このプログラムを実行すると、次の結果を得られる。

#<SaltWaterBuilder:0x007fe2011f2fd0 @salt=35.0, @water=150.0>
#<SugarWaterBuilder:0x007fe2011f2df0 @sugar=35.0, @water=150.0>

今回作ったサンプルソースはGitHubにも公開しているので、参考にしてみて欲しい。

Builder Pattern in Ruby

参考情報