Refactoring mailer class

MaJeD BoJaN
4 min readJan 20, 2019

--

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')
end
def send_forgot_password_ar(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_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Confirmation email link')
end
def send_confirm_email_ar(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_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Provider password info')
end
def send_provider_password_ar(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_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Welcome mail')
end
def 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')
end
def registration_status_changeable_en(user_id)
@user = User.find_by(id: user_id)
return unless @user
mail(to: @user.email, subject: 'Changing account status')
end
def 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

--

--

MaJeD BoJaN

Self-Taught Full-Stack ROR Developer || Tech enthusiast.