Local variables are one of the basic building blocks in programming in general. In Ruby, it is the only variable that doesn’t start with special characters like @, @@, or $, which indicates that they should be used often. However, many Rubyists, including me a couple of months ago, tend to avoid them in favor of message chains, passing expressions directly to the caller, or by using the extract_method pattern
Here are some simple examples of avoiding local variables
Message Chains
The Example below is not that bad at the end of the day, however, it’s very common and I believe we can do better
Do you see that Rails.logger...
? That’s what we are talking about
def feed_animal(animal)
case animal
when :dog
feed_the_dog
Rails.logger.info("Dog is full now")
when :cat
feed_the_cat
Rails.logger.info("Cat is full now")
else
Rails.logger.error("The breed does not exist")
end
end
if Rails.env.development? || Rails.env.test?
Directly passing expressions
This scenario is quite bad, the code is much less readable.
Success(DTO::Task.new(
task.attributes.symbolize_keys.merge(
author: task.author.attributes.symbolize_keys
)
))
ProjecMenagment::Issues::DTO::IssueField.new(
filed: { description: "Descirption", title: "Title", user: {
usnermame: "user", email: "user@example.com"
}
},
issue_field_definition: ProjecMenagment::Issues::DTO::IssueFieldDefinition.create(
issue_field_definition_params
)
)
Why local variabels
As we’ve seen, avoiding local variables doesn’t make code more readable, rather opposite… . Especially if we directly pass expression with long message chains to the caller. The second reason, which probably won’t be a problem unless we do something computation heavy… is when we access a local variable, the virtual machine knows the location of the local variable and can more easily access memory.
How to refactor
Well, that should be fairly simple, just extract variable.
So in our first example we could just extract Rails.logger
to variable or inject the logger to the class/method with the assigned default value
def feed_animal(animal, logger = Rails.logger)
case animal
when :dog
feed_the_dog
logger.info("Dog is full now")
when :cat
feed_the_cat
logger.info("Cat is full now")
else
logger.error("The breed does not exist")
end
end
Second code snippet might look like this
task = task.attributes.symbolize_keys
author = task.author.attributes.symbolize_keys
task_with_author = task.merge(author: author)
task_DTO = DTO::Task.new(task_with_author)
Success(task_DTO)
Little Bonus
Here comes a more interesting example. The code we will take a look at is not just fine, it is excellent, but let’s see if we can do even better.
So what gets our attention here… are the private methods(user, topic, video) called directly in send_weekly_iteration_mailer
.
I often have the feeling that extract_method pattern
is pushing us away from using local variables, sometimes it’s good sometimes not
Let’s take a look at the class below, the author decided to abstract calls to AR models to private methods instead of calling them directly inside
send_weekly_iteration_mailer
which is obviously a good thing, but if the class will be a bit bigger and send_weekly_iteration_mailer
will call other methods, that
will compose of other methods it might get a bit confusing, but still a very clean piece of code though
class WeeklyMailer
def initilize(user_id, video_id, topic_id)
@user_id = user_id
@video_id = video_id
@topic_id = topic_id
end
attr_reader :user_id, :video_id,
def send_weekly_iteration_mailer
WeeklyMailer
.update(user: user, video: video, topic: topic)
.deliver_now
end
# Other methods...
private
def user
User.find(user_id)
end
def video
Video.find(video_id)
end
def topic
Topic.find(topic_id)
end
end
But maybe we can combine hiding the calls to AR modes while still using local variables. Personally I find it even more explicit, and that would be my way to go
class WeeklyMailer
def initilize(user_id, video_id, topic_id)
@user_id = user_id
@video_id = video_id
@topic_id = topic_id
end
attr_reader :user_id, :video_id,
def send_weekly_iteration_mailer
user = find_user
topic = find_topic
video = find_video
WeeklyMailer
.update(user: user, topic: topic, video: video)
.deliver_now
end
# Other methods...
private
def find_user
User.find(user_id)
end
def find_video
Video.find(video_id)
end
def find_topic
Topic.find(topic_id)
end
end