On logic in a Rails app published on 24 Dec 2012, 3 minute read
Recently, Grant Ammons wrote a nice article about where logic should lie in a Rails app. I would like to complement his opinion, and probably add some real life arguments to it.
Grant extracted logic from the controller, and put it in a separate object, which I also do most of the time. However, a user named DHH (which probably is DHH) said:
So all you’ve done here is extract the work of the controller action into a separate object. This adds needless indirection, makes the code base harder to follow, and is completely unnecessary.
I strongly disagree, and I have good reasons for it.
You know what makes the code base hard to follow? Fat controllers with logic in them. I worked on a Rails app with Controllers of 800 lines of code, it. is. a. freaking. nightmare.
My approach
I look at Rails as web presentation layer, it makes my app work in the browser. This has some consequences to my codebase:
- data-only models (no logic should be in models)
- logicless controllers (no logic should be there either)
- anything else is in single-responsibility objects in
app/domain/
You might think that this is a refactor pattern, and you are right. And you might say that this unnecessary to do from the start, and you may also be right. But the cost of creating a file and moving behaviour there is exactly 0, but the benefits are there from day 1.
Why I do it
Fast tests
The least I am coupled to Rails, the fastest my tests are. Currently, on a medium Rails project my tests run 2-3 seconds, where 90% of the time is wasted to boot rails and test models.
I don’t test my controllers, because they have no logic in them. My methods in controllers don’t have more than 4-5 lines of code.
class RatingsController < ApplicationController
def create
success = RatesBusiness.add_rating(params[:business_id],
params[:score],
current_user)
render json: {success: success}
end
end
RatesBusiness
has some foursquare-related logic besides just creating a entry in the database. Using this approach, I have a very fast TDD flow, because most of the time I don’t need Rails to test my logic. (any dependency to models is stubbed, as any other external dependency).
Your controller only connects your logic to the web interface.
Code and forget
If you have a well tested functionality that works, you don’t need see it in front of you when you code in your application. This is where single-responsibility objects kick in, I TDD them one time, I know about their existence and what their interface is, I will not ever see their code again.
Framework agnostic
When your app logic is not coupled with Rails, you do not depend on it, its version, or interface. Isolating some functionality to a service is a matter of moving some files around.
Reduced stress and debug times
Most of my classes fit in one screen, and have descriptive names as CategorizesCampagins
or FiltersBusinesses
, which makes debugging them very, very easy and pleasant. I don’t need to scroll the hell out of my screen to understand how an isolated piece of functionality works.
Conclusion
Rails is just a framework to make your app work on the web, don’t mix your logic with rails-y stuff. Your app domain should be defined anywhere besides Rails views, controllers, and models.
Inspiration
First, and most influential person for me is Gary Bernhardt and his screencast series destroyallsoftware. If you want to learn isolated testing - watch this guy, he is amazing. (He even test-drives bash scripts :D)
Also, my approach was influenced by Hexagonal Rails concept.
Read more posts
- On logic in a Rails app, revisited 6 years later 18 May 2019
- Fitting the "339 bytes of responsive CSS" in a tweet, with a twist 17 May 2019
- Enforcing that a ruby method is called from a specific location 28 Jun 2017
- Log filename, line and function name in ruby automatically 11 May 2013
- Unity performance tweaks 20 Mar 2013
← back to homepage