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
1 gem ‘interactor’
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.
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
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
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.