11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyデザインパターン 11日目 : Builder

Posted at

Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第11日目はBuilderです。(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

 11日目 Builder

Builderは、オブジェクトを組み立てていくためのパターンです。
前回に学習したFactoryとBuilderは、オブジェクトを構築するという点で似通ったところが多いです。
大きな違いは、

  • Factory:正しいオブジェクトを選択・生成する
  • Builder:複雑なオブジェクトを生成・構築する

という点です。

また拡張することで、BuilderにFactoryのようなオブジェクトを選択する機能を持たせることができます。

普通のコード

まずは何も考えずに、一つずつPCのパーツを追加して構築するコードを書いてみましょう!


class Computer
  attr_accessor :display
  attr_accessor :motherboard
  attr_accessor :drives

  def initialize(display = :crt, motherboard = Motherboard.new, drives = [])
    @motherboard = motherboard
    @drives = drives
    @display = display
  end
end

class CPU
  # CPU共通のコード
end

class BasicCPU < CPU
  # あまり高速でないCPUに関するコード
end

class TurboCPU < CPU
  # 高速なCPUに関するコード
end

class Motherboard
  attr_accessor :cpu
  attr_accessor :memory_size

  def initialize(cpu = BasicCPU.new, memory_size = 1000)
    @cpu = cpu
    @memory_size = memory_size
  end
end

class Drive
  attr_accessor :type
  attr_accessor :size
  attr_accessor :writable

  def initialize(type, size, writable)
    @type = type
    @size = size
    @writable = writable
  end
end

motherboard = Motherboard.new(TurboCPU.new, 4000)

drives = []
drives << Drive.new(:hard_drive, 200000, true)
drives << Drive.new(:cd, 760, true)
drives << Drive.new(:dvd, 4700, false)

computer = Computer.new(:lcd, motherboard, drives)

以上のコードが、何も考えずにPCを組み立てるコードを書いた結果です。
それぞれの部品ごとのクラスはいいのですが、それを組み立てるインターフェイスが非常に煩雑で、可読性もよろしくないので、一目で何をやっているかがわかりません!

なにかいい手はないのでしょうか?

 Builderでオブジェクトを組み立てよう!

class ComputerBuilder
  attr_reader :computer # builder.computerでアクセスしてcomputerオブジェクトを得るため

  def initialize
    @computer = Computer.new # Computerオブジェクトを作成
  end

  def turbo(has_turbo_cpu = true)
    @computer.motherboard.cpu = TurboCPU.new
  end

  def display=(display)
    @computer.display = display
  end

  def memory_size=(size_in_mb)
    @computer.motherboard.memory_size = size_in_mb
  end

  def add_cd(writer = false)
    @computer.drives << Drive.new(:cd, 760, writer)
  end

  def add_dvd(writer = false)
    @computer.drives << Drive.new(:dvd, 4000, writer)
  end

  def add_hard_disk(size_in_mb)
    @computer.drives << Drive.new(:hard_disk, size_in_mb, true)
  end
end

builder = ComputerBuilder.new
builder.turbo
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(100000)

computer = builder.computer # 最終的に求めていたComputerオブジェクトが得られます

GoFはBuilderオブジェクトを使うクライアントをディレクタと呼んでいます。
ディレクタは新しいオブジェクトの組み立てをビルダ(ここではComputerBuilder)に指示します。
作られたオブジェクトはプロダクト(computer)と呼ばれます。

こういった形で、**邪魔なオブジェクト生成の部分を全てビルダに分離 **することができました。

冒頭で紹介したように、オブジェクトを組み立てるだけではなく、ビルダを抽象化することによってオブジェクトを選択する機能もつけることができます。

class ComputerBuilder
  attr_reader :computer

  def turbo(has_turbo_cpu = true)
    @computer.motherboard.cpu = TurboCPU.new
  end

  def memory_size=(size_in_mb)
    @computer.motherboard.memory_size = size_in_mb
  end
end

class DesktopBuilder < ComputerBuilder
  def initialize
    @computer = DesktopComputer.new
  end

  def display=(display)
    @display = display
  end

  def add_cd(writer = false)
    @computer.drives << Drive.new(:cd, 760, writer)
  end

  def add_dvd(writer = false)
    @computer.drives << Drive.new(:dvd, 40000, writer)
  end

  def add_hard_disk(size_in_mb)
    @computer.drives << Drive.new(:hard, size_in_mb, true)
  end
end

class LaptopBuilder < ComputerBuilder
  def initialize
    @computer = LaptopComputer.new
  end

  def display=(display)
    raise "Laptop display must be lcd" unless display == :lcd
  end

  def add_cd(writer = false)
    @computer.drives << LaptopDrive.new(:cd, 760, writer)
  end

  def add_dvd(writer = false)
    @computer.drives << LaptopDrive.new(:dvd, 4000, writer)
  end

  def add_hard_disk(size_in_mb)
    @computer.drives << LaptopDrive.new(:hard_disk, size_in_mb, true)
  end
end

laptop_builder = LaptopBuilder.new
laptop_builder.display = :lcd
# etc
# etc
# etc
laptop_builder.computer

オブジェクト生成を安全にする

def computer
  raise "Not enough memory" if @computer.motherboard.memory_size < 250
  raise "Too many drives" if @computer.drives.size > 4
  hard_disk = @computer.drives.find{ |drive| drive.type == :hard_disk }
  raise "No hard disk." unless hard_disk
  @computer
end

最後の.computerでコンピュータオブジェクトを作成する時点の話です。
ここで、基底クラスの.computerをオーバーライドしておくことにより、属性チェックをしてから、それを通過した規格を満たすものだけをオブジェクトとして世に送り出すことができます。

attr_accessorもメソッドなのでオーバーライドできることもワンポイントです

Let's Ruby !!!

これまでのコードでBuilderパターンは完成したのですが、依然として組み立てのときにたくさんのメソッドを呼ぶ必要があります。
これを解決する方法はマジックメソッドです。

それはなんだというと、Builderのクライアントが特定のパターンに従ったメソッド名を組み立て、それをビルダに適用するというものです。

def method_missing(name, *args)
  words = name.to_s.split('_')
  return super(name, *args) unless words.shift == 'add'
  words.each do |word|
    next if word == 'and'
    add_cd if word == 'cd'
    add_dvd if word == 'dvd'
    add_hard_disk(100000) if word == 'harddisk'
    turbo if word == 'turbo'
  end
end

手順的には、予期せぬメソッドを必ず受け取るというのを前提にしておいて、method_missingで補足する、というパターンです。
これによって、

builder.display
builder.motherboard
builder.drive
builder.accessories
##### so many...

のように面倒な繰り返しをしなくてよくなりますね。

rails generate migration add_post_id_to_users
Railsコマンドでも内部はこういった形の実装になっているのかもしれませんね!(詳しくは知りません!)

 まとめ

Builderパターンはオブジェクトをだんだんと組み立てていくパターンです。
組み立てたいオブジェクトのクラスが巨大になるほど、Builderが威力を発揮することは多いようです。
オブジェクトを組み立てていった際に、「めんどくさいな...」と思ったら導入するメリットがあるかもしれません。

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?