Software development practices change with time and the practices that were used before are being continuously replaced by some new practices that we call “Best Practices”. In this article, we will discuss something similar that is related to refactoring the code in a Rails application following SRP (Single Responsibility Principle).
In this blog, I am taking the liberty of assuming that you know how to code in Ruby and you also have knowledge of Rails and a few of the methods that some gems provide like current_user by devise gem, how mailer works and what are callbacks. So at some points, I may miss the internal details just to explain the actual point. I would not be writing much logic in there, just a comparison of old school practices and some new practices to structure the code.
If you are a Rails developer, you must have heard things like thin controllers, putting the logic in concerns, making use of service object pattern to clean up the controllers and all that. All the good rails applications follow such techniques to clean up the code and making controllers as this as possible. Well, talking about modern rails applications, we have a pattern of interactors that does cleaning and refactoring for us keeping in consideration SRP.
Now assume a use case of a company that registers a user and after successful registration, it gives him a free trial of a subscription and also sends a welcome email.
Following old school practices, we would do something like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RegistrationsController < ApplicationController
def create
user = User.new(user_params)
if user.save!
flash[:success] = "You have successfully registered."
redirect_to dashboard_path
else
flash[:error] = "There was a problem during registration."
render :new
end
end
private
def user_params
params.require(:session).permit(:email, :password)
end
end
Your user model would do something like this.
1
2
3
4
5
6
7
8
9
10
11
class User < ApplicationRecord
after_create :register_free_trial, :send_welcome_email
def register_free_trial
subscription.create(.....)
end
def send_welcome_email
WelcomeMailer.send(self) if subscription.present? && subscription.free_trial == true
end
end
This code needs to be refactored and you might think what is the need for refactoring this code when it already looks so clean? Well, two reasons for that, which I will explain later in this article.
Let’s refactor first!.
For this, we will use interactor
gem. In your Gemfile
, put
1
gem ‘interactor’
Run bundle install
.
Inside app/interactors
create a file called register_user.rb
and put the following code there.
1
2
3
4
5
class RegisterUser
include Interactor::Organizer
organize CreateUser, StartFreeTrial, SendWelcome
end
We could do all these operations in a single interactor
but that would violate SRP, so we would be making use of an organizer that automatically calls the next interactor
if success is returned from the previous one.
The next interactor
would be create_user.rb
in the same directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CreateUser
include Interactor
def call
user = User.create(context.user_params)
if user.persisted?
context.user = user
else
context.fail!
end
end
def rollback
context.user.destroy
end
end
Then we also need to register a free trial for the user after that. Let’s do it.
Create a new interactor
named start_free_trial.rb
in the same directory with content similar to this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class StartFreeTrial
include Interactor
def call
user = context.user
subscription = user.subscription.create(.....)
if subscription.persisted?
context.subscription = subscription
else
context.fail!
end
end
def rollback
context.subscription.destroy
end
end
Let’s welcome our users now. Create a file named send_welcome.rb
in the same directory and put something similar to it.
1
2
3
4
5
6
7
8
9
class SendWelcome
include Interactor
def call
user = context.user
subscription = context.subscription
WelcomeMailer.send(user) if subscription.present? && subscription.free_trial == true
end
end
Time to call our interactor
in registrations_controller.rb
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RegistrationsController < ApplicationController
def create
result = RegisterUser.call(user_params: user_params)
if result.success?
flash[:success] = "You have successfully registered."
redirect_to dashboard_path
else
flash[:error] = "There was a problem during registration."
render :new
end
end
private
def user_params
params.require(:session).permit(:email, :password)
end
end
And yes, we have refactored the code.
Now, what was the motivation of refactoring such a small amount of code?
So let’s assume a use case that is much more complex than what we achieved in this blog. Let’s say, we also want a free trial email be sent after awarding free trial, we might also want(assuming it’s some movies platform like Netflix) to add some recommended movies based on user’s interests after registration and many more things, so the code would no longer be small and clean. This is the first point that I talked about while I was talking about the need to refactor.
The other point that is also mentioned in the gem’s documentation is, at a glance on the interactors directory, we come to know that what is the complete functionality of the application. For example.
So this is all about interactors and structuring the application.