The Builder Pattern in Ruby

Feb 10, 2022 - 7 min read
The Builder Pattern in Ruby
Share

Here's a video version of this article if you prefer to watch instead:

And you can check out the code on Github if you want to try it out.

The Builder pattern allows you to hide the configuration of complex objects in a separate class.

It makes sense to use it when you feel like the creating of an object has become too complex, and you're repeating the process in few different places. Or, when you need to create an object with different possible configuration options.

It's similar to the Abstract Factory pattern, in the sense that it too is a creational pattern, and it separates concerns.

But unlike the Abstract Factory pattern, it doesn't build the entire object in one method call. It's more like a wrapper around the different parameters you might want to pass to the constructor.

I know that might sound a bit too abstract, so let's look at some examples.

# profile_controller.rb
require_relative "./base_controller"

# Director
class ProfileController < BaseController
  def index
    builder = build @params[:format]
    builder.body = @params[:body]
    builder.content_type!
    builder.etag!
    builder.created!
    response = builder.response

    puts response
  end

  def delete
    builder = build @params[:format]
    builder.content_type!
    builder.deleted!
    response = builder.response

    puts response
  end
end

params = { body: "This is some data", format: :html }
ProfileController.new(params: params).index

puts "\n-----\n"

ProfileController.new(params: { format: :html }).delete

One configuration is for a JSON response, and the other is for an HTML response.

So the way I've laid out the structure is, we have a ProfileController class with two methods, where each builds up a response object. And then they print it to the screen.

It's similar to a real controller that handles HTTP requests.

In the Gang of Four book, this is what they would call the director. It's the class that knows how to use the builders to get the desired object back (namely the Response object).

The director class doesn't have to be the actual end client (like it is here). It could be a different class that the client would call, and that provides a simpler interface.

But in our example, this controller class acts like the director.

Ok, so how does it work.

Well... if you look down at the bottom of the controller class, you can see that we're initializing a controller object with some params, and then we're calling the index method on it.

params = { body: "This is some data", format: :html }
ProfileController.new(params: params).index

Then, we're doing the same thing for the delete method.

ProfileController.new(params: { format: :html }).delete

And if we run this code, you'll see it prints the output for both methods, to the screen.

HTTP/1.1 201 Created
Content-Type: text/html
ETag: eixsgntwfhodyrmp

<html><body>This is some data</body></html>

-----
HTTP/1.1 204 No content
Content-Type: text/html

But if we change the html format to json, we get something slightly different back.

params = { body: "This is some data", format: :json }
ProfileController.new(params: params).index

puts "\n-----\n"

ProfileController.new(params: { format: :json }).delete
HTTP/1.1 201 Created
Content-Type: application/json
ETag: hroiakzquwjxyscf

{"content":"This is some data"}

-----
HTTP/1.1 204 No content
Content-Type: application/json

On the json format, the Content-Type header and the body are different. The body of the delete method doesn't exist so it's just the content type that changes there.

As you can see, the controller is building a response object step-by-step and then it returns the object.

But how does it do that?

Well... let's dissect this code.

We're initializing the controller with a params hash.

Which gets assigned to an instance variable called @params.

Then, we're calling this build method with the format argument, and we're using the format to decide which builder we're going to use to construct the final object.

So if the format is html, we're going to use the HtmlResponse class as the builder.

# html_response.rb
require_relative "./base_response"

class HtmlResponse < BaseResponse
  def content_type!
    @response.headers = @response
      .headers
      .merge("Content-Type" => "text/html")
  end
end

And if the format is json, we're going to use the JsonResponse class as the builder.

# json_response.rb
require_relative "./base_response"

class JsonResponse < BaseResponse
  def content_type!
    @response.headers = @response
      .headers
      .merge("Content-Type" => "application/json")
  end
end

The way the builder classes work, is they inherit some common methods (like the initializer for initializing the response object, the etag! method for setting the ETag header, or the body= method for setting and validating the body) from this BaseResponse class.

Inside the builder classes we're just setting the Content-Type header to the appropriate value.

Once we have the builder object, we're calling methods on it to configure the response object.

def index
  builder = build @params[:format]
builder.body = @params[:body]
builder.content_type!
builder.etag!
builder.created!
response = builder.response puts response end

In the BaseResponse class we have a setter method for body that also validates the payload.

# base_response.rb
require_relative "./response"
require_relative "./statuses"

class BaseResponse
  include Statuses

  attr_reader :response

  def initialize
    @response = Response.new
  end

  def etag!
    @response.headers = @response
      .headers
      .merge("ETag" => ("a".."z").to_a.sample(16).join)
  end

def body=(body)
validate_body!(body)
@response.body = body
end
def content_type! raise "Not implemented." end private def validate_body!(body) raise("Bad payload.") if body.nil? end end

This is another notable aspect of the builder pattern. It allows you to build correct objects by incorporating some validation logic.

The idea of having "correct objects" is important. If you build your objects such that they are guaranteed to be in the correct state all the time, you can remove a lot of conditionals from your code.

Ok, back to our controller code.

def index
  builder = build @params[:format]
  builder.body = @params[:body]
  builder.content_type!
  builder.etag!
  builder.created!
  response = builder.response

  puts response
end

The create! method, along with a few more related methods are all bundled in the Statuses module, which is included in the BaseResponse class.

# statuses.rb
module Statuses
  def created!
    @response.status = Status.new(code: 201, message: "Created")
  end

  def not_found!
    @response.status = Status.new(code: 404, message: "Not found")
  end

  def deleted!
    @response.status = Status.new(code: 204, message: "No content")
  end
end

It assigns a new Status object to the status field of the Response object.

The Status class is pretty simple. It consists of a code and a message. And it's got this to_s method to return the status line.

class Status
  attr_reader :code, :message

  def initialize(code: 200, message: "OK")
    @code = code
    @message = message
  end

  def to_s
    "HTTP/1.1 #{@code} #{@message}"
  end
end

After configuring the response object via the builder, we're ready to fetch the configured object. We do that by calling the response method on the builder object.

def index
  builder = build @params[:format]
  builder.body = @params[:body]
  builder.content_type!
  builder.etag!
  builder.created!
response = builder.response
puts response end

And it gives us the Response object back.

Finally, we're printing the response to the screen via it's to_s method.

def index
  builder = build @params[:format]
  builder.body = @params[:body]
  builder.content_type!
  builder.etag!
  builder.created!
  response = builder.response

puts response
end

The Response object doesn't do much either. And that is the goal.

# response.rb
require "json"
require_relative "./status"

class Response
  attr_accessor :status, :body, :headers

  def initialize
    @status = Status.new
    @headers = {}
    @body = body
  end

  def to_s
    <<~HTTP
    #{@status}
    #{headers_string}

    #{format_body}
    HTTP
  end

  private

    def headers_string
      @headers.map { |k, v| "#{k}: #{v}" }.join("\n")
    end

    def format_body
      return "" if @body.nil?

      if @headers["Content-Type"] == "text/html"
        html_body
      else
        json_body
      end
    end

    def html_body
      "<html><body>#{@body}</body></html>"
    end

    def json_body
      { content: @body }.to_json
    end

end

We want to factor away the complexity of the building process. Otherwise it would need to live here in the initializer.

But this way, there's not much to building the Response object in it's initializer. It's a simple object that just formats the body and converts it's data to string.

You could design your builders in such a way that they could be reused to create multiple objects using the same builder object. You'd probably need a reset feature for that.

But we're not going to do that here.

So there you have it, that is the Builder pattern in Ruby.

Whenever you find yourself in a situation that feels like you're doing a lot of work to configure an object, and in multiple different places, or if you notice you're creating a lot of invalid objects, the Builder pattern might be just the thing you need.

12 Project Ideas
Cezar Halmagean
Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.