Most developers aren’t ever completely happy with their code. I’m no exception to this stereotype — I almost always know that I could probably write a cleaner, more concise method, controller, or class. Usually, it’s a matter of not know the best tool to reach for to refactor my code; eventually, I learn a new pattern or form of encapuslating logic that I later use to make my old code a lot better.
But a few weeks ago, I wrote 100 lines of beautiful code. I’m talking about a goregous, straightforward, no-nonsense class that did a lot in a relatively few lines of Ruby. I still feel pretty proud of it (can you tell?), and part of the reason for this is because I also learned a new pattern while writing this class. I was actually pairing with another developer, and we wanted to try using a rather common Rails pattern to solve a problem we were running into again and again: messy queries in our controllers and models.
We both were familiar with the concept of “skinny controllers” and “fat models”, or the idea that your controllers shouldn’t be responsible for containing the logic specific to a model. However, we also didn’t want our models to get out of control in size, which is exactly what was starting to happen. So, we searched for a workaround, and found our answer in one of the most elegant patterns I’ve seen in awhile: finder objects. Finder objects are simple Ruby classes that encapuslate the logic behind querying the database, and they are hands down, my new favorite kind of Ruby object.
Scopin’ Down The Problem of Scopes
In our bookstore application, we have
Order objects, which represent the orders that a user places in our system. However, these are books that we’re dealing with, and eventually we need to address the whole fulfillment process, which will need to be represented by a whole other set of models. For now, we’ll try to keep it as simple as possible. Let’s say that an
Order has many
Shipments, and each shipment represents a batch of
Book objects (read: products) that need to be shipped out together.
Right off the bat, we know that our
Shipment model will have some kind of state machine that will need to track the different stages of the shipment phase. Again, to keep it simple, let’s say that there are five different stages or
states of a shipment:
- A shipment starts off as
processingonce an order has been placed.
- Once it has been processed, it needs a label with the shipping address information, so it transitions to the
- After it has a label generated, it’ll need a tracking number, so transitions to the
- Once it has a tracking number, it transitions to being
- Finally, when the shipment is actually sent out of the warehouse and to our shipping service, it should be marked as
To be clear, this is a super simplified version of what would happen in a real-life application! Now, in our admin panel, let’s say that we have a page that will render all of our shipments, which should always ordered by when they were created, so that our admins know which shipments to process, and in which order.
We can take it a step further and say that our main admin panel page — which will correspond to the
index action in our controller, should show our admins the most urgent shipments that need their attention as soon as they log in; in other words, these would be the shipments that were created more than a week ago, but still haven’t been processed.
I think we can all agree that basic Rails best practices should steer us away from doing something like this:
1 2 3 4 5 6 7
We definitely know that the controller really shouldn’t be responsible for querying for the correct
Shipment objects. All our controller should have to do is just render the correct ones, and not go digging for them in the database!
Okay, so we’ll create some scopes for these queries instead. These scopes should live in our model, not in the controller, right? Let’s see what our model might look like if we take that approach:
1 2 3 4 5 6 7 8 9
This cleans things up a decent bit! Scopes are actually part of the Active Record Query Interface, and they allow us to specify commonly-used queries which we can then actually reference and use in the form of method calls on our models themselves. They are really nothing more than defining class methods on a model:
1 2 3 4 5
However, they’re pretty wonderful because you can just chain them as method calls (
Shipment.urgent.shipped), but all they do is execute the correct query for you:
But the good news about scopes is also the bad news: you can just keep adding them and adding them, and chaining them onto everything. Our
Shipment class has only three scopes at the moment, one of them being the
default scope. But, we’ll probably have a page that should only render only the shipments that are in the
needs_label state, or a page that maps to a controller action which should only return shipments that are in the
needs_tracking state. We could keep adding scopes and then have our controller actions call the scopes on the class to return the appropriate shipments from our database.
Or, we could try something a little different.
Our scopes don’t actually add any new behavior to our model. As we saw earlier, they are nothing more than macros for querying for the correct rows from our
shipments table in our database. So, it really doesn’t make sense for our model to contain all this logic that doesn’t add any new behavior to it.
It would be nice, however, if we could apply the rule of “separation of concerns” here, and have an entire class whose sole responsibility would be to dig up the correct objects based on the parameters we were querying by. Really, this class should do nothing more than find the right objects. You might even say that instances of this class are just…finder objects! (Get it? Man, I really hope you got it.)
Anyways, how might this class look? Well, we don’t need any of the functionality from ActiveRecord, so we can create it as just a Plain Old Ruby Class:
Next, we’ll want to tell our finder object which models to query for, so it will know how to construct our queries, and which table to query. Since we never really want to have this method accessible elsewhere, we can make it a private class method that will only be called from the context of another
ShipmentFinder class method:
1 2 3 4 5 6 7 8
Now, we can use Rails’ arel DSL to construct a basic query. To do this, we’ll need a method that returns the table, which can also be a private method since we’ll never want to call it explicitly:
1 2 3 4 5 6 7 8 9 10 11 12 13
Now, we can write as many specific queries as we want. For example, we could write a
urgent_needs_tracking class method that could constructs a query using two private methods,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
Now we can rely on our
ShipmentFinder class to find our shipments that have been created more than 7 days ago, and still don’t have a tracking number. We also can add more functionality that can be used in specific instances, which is exactly what we’ve done with our
is_ready private methods. We can use arel to construct queries using those methods to scope down what objects are actually returned. We don’t have to do anything fancy, if we don’t want to. Take a look at that
shipped method — this is just using a simple
where arel method to construct a query that reads like this:
SELECT "shipments".* FROM "shipments" WHERE "shipments"."state" = "shipped".
Finally, the last step: it’s time for us to actually call on our finder object to do its job!
Cleaner Querying, Cleaner Controllers
Let’s bring it all together by going back to our
ShipmentsController and add our new finder object into it. Our
index action should now be able to account for different types of shipments that our admins might want to query for. For now, we’ll construct our controller to accept a query parameter in our params hash that will be that
status of the types of shipments we want to return. Depending on the frontend framework we’re using, this might be a dropdown or checkbox option that will set a value on the
status key in our params hash.
Our controller action could now be rewritten to look something like this:
1 2 3 4 5 6 7 8 9 10 11 12
Here, we’re accessing the query param from
params[:status], and turning it into a symbol (
status_method). Next, we’re using Ruby’s super handy
respond_to? method, and sending our
status_method symbol to our
ShipmentFinder. So, if our admin selects an option that sends the query param
urgent_needs_tracking, we are telling our finder object to call that method, and execute that query. The return value of the query executed by our
urgent_needs_tracking method is what will be set as the instance variable
@shipments for the duration of this action on the controller.
If no query param is set, or if our
ShipmentFinder doesn’t have a method that maps to a query param, we’re just returning all of our
Shipment objects by default.
This is quite an improvement from our earlier code, which had our controller digging for rows in a database that it really didn’t have anything to do with. Now, we’ve separated our concerns into a new finder object, which exists as its own query interface on its own. It’s also worth noting that sometimes, creating a finder object can be overkill, and sometimes, if we have a lot of finder objects, we’d probably want to abstract a lot of this functionality out into a
BaseFinder class, which our finder objects could inherit from. But this is definitely a great start. No more digging for us!
- The finder object pattern helps keep your model logic strictly related to a class’ behavior, while also keeping your controller’s skinny. Since they are nothing more than plain old Ruby classes, finder objects don’t need to inherit from
ActiveRecord::Base, and should be responsible for nothing more than executing queries. Read more about them on this fantastic blog post.
- Scopes are a great tool to use if a finder object seems like more work than it’s really worth, given the size and context of your application. Read more about scopes in the Rails documentation.
- Want to see more examples of implementing finder objects? Check out this slidedeck series.