Refactoring mailer class
Refactoring Mailer class using metaprogramming
Refactoring is one of the important thing that you have to do to improve your code, it is the process of restructuring existing code, changing the factoring without changing its external behavior
The main purpose of refactoring is to fight technical debt. It transforms a mess, dirty, and smell code into clean code and simple design.
Refactoring improves non-functional attributes of the software.
The advantages:
- improve code readability
- reduced complexity
these can improve source-code maintainability and create a more expressive internal architecture or object model to improve extensibility
After a period of time i came across UserMailer class that already created months ago i found 5 methods doing almost same thing and sending mail for user with different content with `en` locale.
my task was to make those 5 methods able to send with ar locale as well so i decided to refactor the entire class and use define_method which is ruby metaprogramming method.
METAPROGRAMING: is a technique by which you can write code that writes code by itself dynamically at run time.
This means you can define methods and classes during run time. Crazy, right? In a nutshell, using metaprogramming you can reopen and modify classes, catch methods that don’t exist and create them on the fly, create code that is DRY by avoiding repetitions, and more
define_method(*args): Defines an instance method in the receiver. The method parameter can be a Proc, a Method or an UnboundMethod object. If a block is specified, it is used as the method body. This block is evaluated using instance_eval, a point that is tricky to demonstrate because define_method is private. (This is why we resort to the send hack in this example.)
The UserMailer class was looking like
class UserMailer < ApplicationMailer def send_forgot_password(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Reset password link')
end def send_confirm_email(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Confirmation email link')
end def send_provider_password(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Provider password info')
end def send_welcome_email_to_provider(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Welcome mail')
end def registration_status_changeable(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Changing account status')
end
end
Well!
Now we need to send the same above emails in different locales which are en and ar.
So we have to duplicate the 5 above methods, then our MailerUser class will be huge
instead of 5 methods we got 10 methods very huge class!
class UserMailer < ApplicationMailerdef send_forgot_password_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Reset password link')
enddef send_forgot_password_ar(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Reset password link')
enddef send_confirm_email_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Confirmation email link')
enddef send_confirm_email_ar(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Confirmation email link')
enddef send_provider_password_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Provider password info')
enddef send_provider_password_ar(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Provider password info')
enddef send_welcome_email_to_provider_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Welcome mail')
enddef send_welcome_email_to_provider_ar(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Welcome mail')
enddef registration_status_changeable_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Changing account status')
enddef registration_status_changeable_ar(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Changing account status')
end
end
Now Let’s refactor the UserMailer class and replace all the methods with following ruby metaprograming code.
instead of more then 60 lines of code we got 12 lines of code so cool! right?
class UserMailer < ApplicationMailer['en', 'ar'].each do |lang|
I18n.t('mailer.user', locale: :en).keys.each do |method_name|
define_method("#{method_name.to_s}_#{lang}") { |subject, user_id|
@user = User.find_by(id: user_id)
return if @user.blank?
mail(to: @user.email, subject: subject)
}
end
end
end
Let’s break down our class and explain little things
First we defined an array with ar and en locale so we can loop them and get the 5 methods in both ar and en then we got the names of the methods those we need to create them at the run time from I18n
I18n.t('mailer.user', locale: :en).keys
Now we have got the methods names in array we have to add ar and en in end of each method and defined the methods and pass 2 arguments user_id and subject which will be the mailer subject
define_method("#{method_name.to_s}_#{lang}") { |subject, user_id|
here we go!
now we have the same code that already had in each method which was finding the user id and send the mailer
@user = User.find_by(id: user_id)
return if @user.blank?
mail(to: @user.email, subject: subject)
Cool! now we got very dry code and in future if we needed other method to do same thing we can add the name of the method in I18n and call the method from anywhere
Calling any method from the model will be like
subject = I18n.t('mailer.user.registration_status_changeable.subject')
UserMailer.send("registration_status_changeable_#{I18n.locale}", subject, user.id).deliver_later
instead of
UserMailer.registration_status_changeable(user.id).deliver_later
Consultation
We have covered how to refactor big class using define_method(*args) and we reduced the number of code lines in the class, it’s good to look your code after period it will give you chance to refactor and improve your thinking of writing code