
One useful pattern I've found when writing Sinatra applications is the command pattern. For those that are not familiar with it, here is a simple example to start with:
class MyCommand
def initialize
# initialize some stuff
end
def call
# do some stuff
end
end
The class is pretty straightforward, just an #initialize
and a #call
method, that's all you need. Now, why is this useful? Let's take a look at an example request handler in Sinatra for a photo uploading API:
post "/photos" do
if params[:image] and param[:title]
# store the photo
uploader = CarrierWave::Uploader::Base.new
uploader.store!(params[:image])
# store photo metadata
DB[:photos].insert({
title: params[:title],
content_type: params[:image][:type],
file_size: File.size(params[:image][:tempfile]),
url: uploader.url
})
201
else
400, JSON.generate({
status: 401,
message: "Photos require a title and an image"
})
end
end
While it's not too bad, there are a few code smells in this request handler. The most obvious smell is the comments. In order to upload a photo, our handler needs to do two things: send the photo somewhere (in this case an S3 bucket using the CarrierWave gem), and store some meta-data about the photo to our database. Another smell is the conditional that checks to make sure there is an image and a title sent with the request. One way we could clean that up is to use a guard clause that immediately halts the request like this:
post "/photos" do
unless params[:image] and params[:title]
halt 400, JSON.generate({
status: 401,
message: "Photos require a title and an image"
})
end
# store the photo
# ...
# store photo metadata
# ...
end
I think this makes things better, but we could do more. Imagine that we need to store more information about the photo, like longitude and latitude, or maybe the validation logic needs to handle more scenarios. Our request handler has the potential to get quite messy. Let's see how we can clean it up using the command pattern.
class UploadPhoto
attr_reader :album, :title, :photo
def self.call(album, title, photo)
new(album, title, photo).call
end
def initialize(album, title, photo)
@album = album
@title = title
@photo = photo
end
def call
uploader.store!(params[:image])
album.insert({
title: title,
content_type: photo[:type],
file_size: File.size(photo[:tempfile]),
url: uploader.url
})
end
private
def uploader
@uploader ||= CarrierWave::Uploader::Base.new
end
end
post "/photos" do
unless params[:image] and params[:title]
halt 400, JSON.generate({
status: 401,
message: "Photos require a title and an image"
})
end
UploadPhoto.call(DB[:photos], params[:title], params[:photo])
201
end
Creating the UploadPhoto
command class buys us a few things. For one, it encapsulates our entire use case for uploading a photo. Our handler becomes very simple. It also allows us to create a private helper method. We could do this where our Sinatra handlers are located as well, but that would quickly get out of hand if each request handler defined several private methods. Finally, it also makes testing easier. We could easily create a fake album
pass it in to this class. Notice that I created a convenience class method for call
that I think makes the request handler code read a bit nicer.
There's one more refactoring I'd like to mention. In this particular example, I'm on the fence as to whether what I'm about to show you makes sense. I think the idiom is helpful, however, so I'll show it to you and then write about my reservations below.
class UploadPhoto
# ...
def call
unless params[:image] and params[:title]
# THIS IS A BAD IDEA!!!
halt 400, JSON.generate({
status: 401,
message: "Photos require a title and an image"
})
end
# upload photo and store meta-data...
end
# ...
end
post "/photos" do
UploadPhoto.call(DB[:photos], params[:title], params[:photo])
201
end
What I've done here is to move the data validation into the command class. However, this poses a problem. the halt
method is a part of Sinatra, our web library. UploadPhoto
is a part of our domain. Mixing the two together is a bad idea. To solve this problem we can use a technique I first read about in Avdi Grimm's Exceptional Ruby (he's also done a Ruby Tapas episode on it). He refers to the technique as the "Caller-supplied fallback strategy". It uses the power of Ruby's blocks to allow us to keep the web specific call to halt
in our web module, yet still allow us to perform the validation inside the command class. It looks like this:
class UploadPhoto
attr_reader :album, :title, :photo, :error_block
def self.call(album, title, photo, &block)
new(album, title, photo, &block).call
end
def initialize(album, title, photo, &block)
# ...
@error_block = block
end
def call
error_block.call unless params[:image] and params[:title]
# upload photo and store meta-data...
end
# ...
end
post "/photos" do
UploadPhoto.call(DB[:photos], params[:title], params[:photo]) do
halt 400, JSON.generate({ status: 401, message: "Photos require a title and an image" })
end
201
end
By passing a block to our command, we can have it handle the validation and then call the block if it fails. If for some reason we needed to have the request handler redirect instead of halt, for example, the UploadPhoto
class doesn't care. We only need to make the change in our request handler.
I mentioned earlier that I had reservations about using this technique in this specific example. I'm still mulling it over, but I've been doing some reading about various web architectures and design patterns recently. One of the concepts that has come up a few times is that validations should happen at the boundary of the system, not inside the domain. In other words, we should validate the photo image and title in the request handler before we hand it off to UploadPhoto
for processing. I'll try to follow up with another post about this after I do some more research and experimenting.
The last thing I'd like to mention is that there is nothing about using the command pattern that is specific to Sinatra handlers. I've used this same technique to clean up bloated Rails controllers.