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.