Beware Dependencies
Adding a dependency to your project often adds hidden costs to maintaining your code. Even the best tested, most carefully maintained libraries will have their issues. Some libraries you choose will inevitably be abandoned, leaving you with the equally undesirable options of re-implementing features or taking on maintenance of a community project. I recently began forcing myself away from add-on HTTP libraries in Ruby. I’m making more of an effort now to be productive in the standard library, and I want to share a method for handling HTTP responses in Ruby’s ‘net/http’ library. I know now it can be expressive and flexible simply by following good object-oriented programming practices.
A Sample Request
Consider this call to a fictional API endpoint.
require 'net/http'
Net::HTTP.start 'api.example.com', 80 do |http|
http.get '/endpoint' do |response|
p response
end
end
This works, but it’s hard to test because it doesn’t return anything and our entire implementation is in the Net::HTTP.start block. If we could move the work of evaluating the response outside the block, we might find better ways to handle it. Luckily for us, Net::HTTP.start (as you probably already knew) returns whatever the block returns. Knowing that, we can refactor this slightly.
Think Outside the Block
require 'net/http'
response = Net::HTTP.start 'api.example.com', 80 { |http|
http.get '/endpoint'
}
p response.body
We’re still just printing the response to the console, but having it available outside of the block gives us options we didn’t have before. Consider that different types of responses get different response classes. We can instantiate handlers for those responses by reusing those class names in our own code.
require 'net/http'
response = Net::HTTP.start 'api.example.com', 80 { |http|
http.get '/endpoint'
}
response_base_class = response.class.name.split('::').last
begin
handler_class = Object.const_get("ApiResponse::#{response_base_class}")
handler_class.new(response).handle
rescue NameError => e
ApiResponse::DefaultHandler.new(response).handle
end
module ApiResponse
class DefaultHandler
# ...
end
class HTTPOK
# ...
end
end
This is a pattern popularized most recently by Sandi Metz, and it works in a variety of situations. I encourage you to read the linked post. Understanding the refactoring process to get to this code is more valuable than the implementation itself.
Question Old Habits
In the past, I might have grumbled something about beautiful APIs and flippantly added HTTParty to my Gemfile, probably because I saw it in a project once. That might have been okay as a new developer, but experience requires us to constantly reevaluate the decisions we’re making. Breaking away from those rote decisions is part of what lets us look in horror at the code we wrote six months ago and feel better about what we’re writing today.