This post is part of a series of reviews on the book Design Patterns in Ruby. Check out the Introduction post for a full table of contents along with some generic principles regarding Design Patterns.
While the Factory Pattern is worried about which object to create, the Builder pattern is worried how to create it. Some objects require a load of work to be built and configured, and that’s what the Builder pattern eases out for us.
To illustrate the Builder pattern we’ll be using a simple Computer builder class:
class Computer attr_accessor :motherboard attr_accessor :display attr_accessor :drives def initialize(display=:crt, motherboard=Motherboard.new, drives=) @motherboard = motherboard @drives = drives @display = display end end
As you can see, the Computer object has a few pieces that need to be built, so let’s code them:
class CPU def process puts "I'm processing" end end class Motherboard attr_accessor :cpu attr_accessor :memory_size def initialize(cpu=CPU.new, memory_size=1000) @cpu = cpu @memory_size = memory_size end end class Drive attr_reader :type attr_reader :size attr_reader :writable def initialize(type, size, writable) @type = type @size = size @writable = writable end end
As you can see, even with a simple layout like this one, creating a Computer object is still some cumbersome code:
motherboard = Motherboard.new(CPU.new, 2000) 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) puts computer.inspect
To make this process simpler and more intuitive, we could use a builder:
class ComputerBuilder attr_reader :computer def initialize @computer = Computer.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
Now all we need to do is create an instance of builder and call a few methods on it:
builder = ComputerBuilder.new builder.add_cd(true) builder.add_dvd builder.add_hard_disk(10000) puts builder.computer.inspect
In terms of lines of code we didn’t gain much (in this simple example), but in terms of code clarity we gained alot, and the fact that we could add much more complex creation code inside our builder and configure it with a single line of code is a big win.
Turns out we can also shorten the lines of code by using a technique called magic methods, which uses the same method_missing method we saw on the Proxy Pattern:
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' end end
By adding this to the ComputerBuilder method we can use methods such as this one:
builder = ComputerBuilder.new builder.add_cd_and_dvd_and_harddisk
Whoever is familiar with Ruby on Rails should have noticed that this is also used by find methods such as find_by_name_and_email.