Refactoring your code is no less important than writing the code itself. And writing on Ruby on Rails is no exception here, though you can face some additional challenges here. Today we will cover how to refactor it right, if you use Ruby on Rails for your project.
So what about Ruby on Rails refactoring challenges?
Refactoring is important both for business development and your personal comfort as a developer. We have already talked about all the reasons why refactoring is essential, but now it’s time to get down to work.
Ruby on Rails is known as not the easiest choice for refactoring. There are few reasons for that.
Problem 1. Low scalability
Problem one is that Ruby on Rails application is not so good at scaling up. They suit best for CRUD type of applications or apps with simple business logic. If you ever heard of a huge application written with Ruby on Rails, the highest probability is that its logic is pretty simple. Other odds are that the developers have put enormous effort in order to make everything right.
Problem 2. Refactoring Ruby on Rails requires a grounded plan
Another problem is that though there are a bunch of gems that will help you to automate refactoring a little, all these small changes are till not enough. And keeping in mind that Ruby on Rails code is not so easy to refactor, before you start you need to develop a good plan of all changes to be introduced.
To stop you from panicking and cheer you up a little, we have gathered up a bunch of tips, how to achieve success in refactoring Ruby on Rails code.
Control your controllers
Controllers are a God’s blessing for a Ruby on Rails developer: they orchestrate your models, trace HTTP request parameters and do plenty other useful stuff. It’s quite easy to fall into temptation here and orchestrate all your business logic within them.
The best thing you can do here is to get rid of this coupling. Have your business logic separated in order keep the application balanced. Extract supportive pieces into a gem o make things work. In order to achieve this form and service objects need to be introduced.
How to use Form objects
This type of objects is used to make the life of a controller a bit easier and take params processing responsibility out of it. Creating form object basically means making proper type coercions and introducing simple validations.
To use a form object simply wrap params object from controller with it:
[ruby]
class SubmitArticleForm
include ActiveRecord::Validations
include Virtus.model
attribute :title, String
attribute :content, String
validates :title, :content, presence: true
validates :title, length: { minimum: 5 }
def persisted?
false
end
def validate!
raise ValidationError.new(errors: errors) unless valid?
end
end
[/ruby]
After that you can use it in the controller:
[ruby]
class ArticlesController < ApplicationController
def create
form = SubmitArticleForm.new(params[:article])
form.validate! # your logic here.
rescue ValidationError => err
# …
end
end
[/ruby]
Looks quite useful, right? But you can make another step forward and refactor your code even better using Service objects.
How to use Service objects
Service objects only use is in extracting the business logic from controllers. No additional technologies required here – they are old Ruby objects.
[ruby]
class SubmitArticle
def initialize(article_mailer)
@article_mailer = article_mailer
end
def call(form)
Article.create!(form.attributes).tap do |article|
article_mailer.published_article_mail(article).deliver_later
end
end
private
attr_reader :article_mailer
end
[/ruby]
After you have them, you can replace in controller behavior like this:
[ruby]
class ArticlesController < ApplicationController
def create
form = SubmitArticleForm.new(params[:article])
form.validate!
@article = Article.create!(form.attributes)
ArticleMailer.published_article_mail(article).deliver_later
end
end
[/ruby]
With this:
[ruby]
class ArticlesController < ApplicationController
def create
form = SubmitArticleForm.new(params[:article])
submit_article = SubmitArticle.new(ArticleMailer)
@article = submit_article.(form)
end
end
[/ruby]
Since SubmitArticle is a plain object, you can easily add dependencies, which is great for testing.
Following these two patterns described above, you can go miles with refactoring you Ruby on Rails code. And patterns and hacks are you using? Share with us in the comments!
3 Comments
Nice article!
I maybe would add a line in SubmitArticle, `form.validate!` before get its attributes.
Refactoring Rails code is a continuous product improvement construct – it never ends. Some of your points are very well taken. I don’t accept that Rails has low scalability – Problem 1. This statement is too generalized. It’s true that Facebook added a Scala backend in order to scale users and services because Rails broke at over 200M subscribers! This is a nice problem to have!
Nice article if your determined to refactor Rails code without adding value or simplicity. The default Rails Way implies MVC. which is why I suppose your refactored code still lives in the controller and model; that’s the only place to put it when your following a MVC strategy.
My advice would be to consider separating your code from the Rails Framework to the largest extent possible. The Rails Framework itself is an excellent Web Interface/library; but it is not an application server. I use a services strategy to compose applications which use Rails, and have found it very productive, easy to maintain, and performant at scale. The strategy is not fully documented, but I have a working code example here: https://github.com/skoona/SknServices focused on the core security aspects of a much larger application.
I have used Rails since V1, and worked through several upgrades with large and small applications. MVC has always felt wrong and fat controller / skinny model, vice versa, is the wrong conversation to be having. Think about it, how would your code be structured differently if you were not embedding it in the MVC nonsense, but instead using Rails as a pure Web Library.
1. True, the Rails Controller has the url-based entry points into your applications, and Rails Views render the results of that entry. That does not mean all your code has to live in the Controller!
2. How is it that an ActiveModel/ActiveRecord class which define a simple or complex data record, should also have business logic bolted on! Data is simply data, don’t muck it up with non-relevant code. Organize your business logic in a Domain Object (or simply a separate class).
3. Views do not benefit greatly from their knowledge of the underlying DB model, and helpers like #form_for don’t do the whole job. Abandon the helpers which are ActiveRecord Aware, and use the basic #form_for helpers. Views should format and display results, not exercise knowledge the db model. Keep it simple, paint the page — period.
4. The code you might otherwise see in a controller, would be in a service object. The code you might otherwise see in a ActiveRecord Model would be in a Domain object or more tightly package in a DBProvider object. Views would expect and use a data bean like object (ResultBean) https://github.com/skoona/skn_utils . Views are handed results and boolean control flags which they render pages or UI from. Views should have no awareness of the DB, it models, etc.
Let me say I agree that you have made some wise recommendations if your follow the Rails Way. But, I should also say — there is another way, closer to the more traditional Object Oriented or Domain Driven Design methodologies. If adapted you will find Rails upgrades to be almost effortless, easier on boarding of new developers, faster test suites and deeper test coverage without worthless test that test Rails Code (i.e. controller test).