- Learn why service objects are important
- Refactor API calls from controllers to a service object
We'll be continuing with the Foursquare Coffee Shop app. You can build on the one you've been working with, or use the included code.
Congrats on getting your Foursquare Venue Tips to work! While working on these features, you might have started to wonder if everything we're doing: talking to the Foursquare API, parsing Foursquare data, really belongs in our controllers.
If we think back to Single Responsibility Principle, and the purpose of the components of MVC, we can come to the conclusion that we're forcing our controllers to know too much about Foursquare and the business logic of the data we get from the API when controllers are really supposed to be shuffling data back and forth between models and views.
We want to move our business logic out of our controllers, but how? We aren't going to use an ActiveRecord
Model, because we're not dealing with our own database.
We are, however, dealing with data from someone's database, and the business logic of consuming and transforming that data, so we need something else.
Service Objects.
A service object is an object that we can use to encapsulate the inner workings of some business or domain logic that isn't strictly the responsibility of a single ActiveRecord
model.
If you can imagine a complex CRM system, the act of creating a new customer might involve also setting up a sales pipeline, creating tasks and calendar items for a salesperson, and other related stuff. That all doesn't belong in the Customer
model, but it also certainly doesn't belong in the CustomersController
, so we would encapsulate it into a service object.
Similarly, in our system, things like dealing with OAuth, or knowing how to query the Foursquare API don't belong in our controllers or models, so we need service objects.
According to a post by Tom Pewiński in Ruby Weekly:
As I understand it, a Service Object implements the user’s interactions with the application. It contains business logic that coordinates other artefacts. You could say it is the core of the application.
We're going to use service objects to execute our interactions with the external Foursquare API.
Currently, your SessionsController
create
action looks something like this:
# sessions_controller.rb
def create
resp = Faraday.get("https://foursquare.com/oauth2/access_token") do |req|
req.params['client_id'] = ENV['FOURSQUARE_CLIENT_ID']
req.params['client_secret'] = ENV['FOURSQUARE_SECRET']
req.params['grant_type'] = 'authorization_code'
req.params['redirect_uri'] = "http://localhost:3000/auth"
req.params['code'] = params[:code]
end
body = JSON.parse(resp.body)
session[:token] = body["access_token"]
redirect_to root_path
end
Create the folder app/services
and create a file foursquare_service.rb
within that folder. Then define a FoursquareService
class.
# app/services/foursquare_service.rb
class FoursquareService
end
Now, let's move the API interaction from SessionsController
into FoursquareService
. Define a method called #authenticate!
. Our arguments will be the client ID, client secret, and code that we need to authenticate with Foursquare:
def authenticate!(client_id, client_secret, code)
resp = Faraday.get("https://foursquare.com/oauth2/access_token") do |req|
req.params['client_id'] = client_id
req.params['client_secret'] = client_secret
req.params['grant_type'] = 'authorization_code'
req.params['redirect_uri'] = "http://localhost:3000/auth"
req.params['code'] = code
end
body = JSON.parse(resp.body)
body["access_token"]
end
Looks very familiar, right? We've just moved the code from the controller to the service object, almost as-is. The only real difference is we pass in the parameters from the controller.
Advanced: This is an example of an "Extract Method" refactoring. Anywhere you can simply cut code from one place into a new method is a place where you might be violating Single Responsibility Principle.
Now that we've started to set up our FoursquareService
object, what do we need to do in our SessionsController
? Instantiate a new FoursquareService
object and call our #authenticate!
method!
# sessions_controller.rb
def create
foursquare = FoursquareService.new
session[:token] = foursquare.authenticate!(ENV['FOURSQUARE_CLIENT_ID'], ENV['FOURSQUARE_SECRET'], params[:code])
redirect_to root_path
end
Now our controller is much cleaner. All it has to know is that there's a FoursquareService
that can provide an OAuth token.
We pass in the client ID and secret via our .env
environment variables because just as it's not the responsibility of the controller to know the internals of the Foursquare API, it's not the responsibility of the Foursquare service to know where to go to get system data.
If we run our Rails server, everything should still work as expected. To verify, trying accessing /search
in a private tab that won't have your session already stored.
We've cleaned up the OAuth flow for the SessionsController
using a service object. Now let's look at doing the same for our friends
action.
Extract the Foursquare API call from the friends
action in SearchesController
and put it into a new friends
method in the Foursquare service object.
# services/foursquare_service.rb
# ...
def friends(token)
resp = Faraday.get("https://api.foursquare.com/v2/users/self/friends") do |req|
req.params['oauth_token'] = token
req.params['v'] = '20160201'
end
JSON.parse(resp.body)["response"]["friends"]["items"]
end
Here we go as far as to return just the part of the JSON response that we need to build a friends list, rather than forcing the controller or view to know how to pull the right data. Since the method is named friends
, it makes sense that it would just return a representation of the friends.
Then in our controller, we can update the friends
action to use the service object:
# searches_controller.rb
# ...
def friends
foursquare = FoursquareService.new
@friends = foursquare.friends(session[:token])
end
Now if we reload our /friends
page, we should still see our friends!
We've learned that service objects help us encapsulate business logic that doesn't belong in a controller or ActiveRecord
model, allowing us to keep our controllers "skinny" and observe the Single Responsibility Principle.
We've also extracted controller code into our service object and refactored our controllers to use our new service.
For extra practice, finish extracting the rest of the Foursquare API calls in our Coffee Shop application so that all of the Foursquare logic is in our FoursquareService
.
View Service Objects on Learn.co and start learning to code for free.