The Builder Pattern

This is a post about the Builder Pattern and is from my study of design patterns.

In this example we are writing software for a company that builds custom vintage cars. The company needs a way to describe the details of each order. To construct a car object we could do it by providing all of the parameters to new...

require './tedious_car'
require './parts'

color = '#FF0000'
engine = Parts::Engine.new(8)
transmission = Parts::Engine.new(:automatic)
sound_system = Parts::SoundSystem.new('48D3T8')
warranty_numbers = ['G42WV7', 'E4CDWQ']
car1 = TediousCar.new(color, engine, transmission, sound_system, warranty_numbers)

As my class name implies, constructing this object is a bit tedious. We have to pass in a bunch of arguments, including instantiating three other objects first. A constructor with so many required parameters is a code smell. A new team member reading this code months or years later could have a really tough time teasing apart how this code actually works. And s the codebase grows things could only get worse!

The builder pattern proposes a different way. When constructing an object becomes less simple, let's separate the responsibility of constructing an object away from an object itself. Under this pattern a Car object will encapsulate the car itself but the CarBuilder object just represents how the Car is created. This is aligned with the Single Responsibility Principal. When constructing an object becomes complex enough to be considered itself a concern, we should extract that responsibility to a new class.

Here's our new car. Notice that we are providing a way to construct a car without a builder class, if one really wanted to, but the class is designed with the intention of using a builder class.

require './car_builder'

class Car
  attr_accessor :color, :engine, :transmission, :sound_system
  attr_reader :warranty_numbers
  # Car may be inited with starting values but it doesn't have to be.
  # The intended way to construct this class is by using the builder class.
  def initialize(**attrs)
    @warranty_numbers = []
    attrs.each do |attr, val|
      instance_variable_set "@#{attr}", val
    end
  end
end

and the builder class:

require './parts'
require './car_new'

class CarBuilder
  attr_reader :car
  def initialize(&block)
    # raise 'Car needs a block to be built' unless block_given?
    @car = Car.new
    self.instance_eval(&block) if block_given?
  end

  def add_color(c)
    @car.color = c
  end

  def add_engine(cylinders)
    @car.engine = Parts::Engine.new cylinders
  end

  def add_transmission(type)
    @car.transmission = Parts::Transmission.new type
  end

  def add_sound_system(serial_number)
    @car.sound_system = Parts::SoundSystem.new serial_number
  end

  def add_warranty(number)
    @car.warranty_numbers << number
  end
end

The CarBuilder creates a simple ruby DSL that let's us build up a car class by sending a CarBuilder class messages about what the car should look like.

car_builder = CarBuilder.new do
  add_color '#FF0000'
  add_engine 8
  add_transmission :manual
  add_sound_system :G5T6U8
  add_warranty :G5T3E5
  add_warranty :HY6D45
end

car3 = car_builder.car
p car3

Much cleaner, right? Someone coming back to this code later on can tell what I am up to more easily. Also, in creating the car, I am only concerned with the actual data I need to pass in. When I'm creating a car, I don't really car about what a Parts::Transmission class is. I only care if its a manual or automatic. Let the builder handle the rest of the details! The result is cleaner, more readable, more testable code.