Ruby is too dynamic. We can do almost anything in runtime, from creating the classes at runtime to creating methods dynamically.
If you are coming from some other language, it would be shocking for you too know that nothing is private in ruby. You can access private and protected method from anywhere you want.
You can call this a flexibility or a curse. But a smart developer knows when to utilize the power of extreme flexibility that Ruby provides and when to stay away from it.
Is this all Ruby can do in terms of flexibility? Definitely not. Ruby have broken every barrier that traditional language had. It's left to developers discretion, how much of Ruby flexibility they want to utilize.
So what apart from dynamically creating classes and methods, ruby can do?
In Ruby we can access instance variable using the instance from anywhere. You might be thinking yes I know that we can always create a attribute accessor to do so. But no. You don't need attribute accessor to access instance variable.
You can do it by using ruby method instance_variable_get
. Not only you can access instance variable you can also set it. instance_variable_set
allows you to set instance variable to any arbitrary value.
Example:
class A; end
a = A.new
a.instance_variable_get(:@var) #=> nil
a.instance_variable_set(:@var, 'Hi')
a.instance_variable_get(:@var) #=> Hi
Hmm. Looks good for show off. Is there any practical use though? Yes it has a practical usage. If you are using Rails framework then you are using this feature under the hood. But where?
Whenever you create a instance variable in controller, how is it available in views? Yep Rails uses instance_variable_get
and instance_variable_set
to implement this functionality.
To know more about how this functionality is implemented you can refer this link How are Rails instance variables passed to views.
That's nice. But would we be able to utilize this feature anywhere else? Yes you can utilize this feature when you want to refactor the code.
Consider below two new action in different controllers.
# In ReviewsController
def new
if @order.review
flash[:alert] = 'Review already exists'
redirect_to spree.root_path
else
@review = @order.build_review
end
end
# In VideoReviewsController
def new
if @order.video_review
flash[:alert] = 'Review already exists'
redirect_to spree.root_path
else
@video_review = @order.build_video_review
end
end
Above code looks very much the same. So how can we refactor it? Its easy in Ruby. We just have to use method discussed above.
module Reviewable
def new_review(type=:review)
if @order.send(type)
flash[:alert] = 'Review already exists'
redirect_to spree.root_path
else
instance_variable_set("@#{type}", @order.send("build_#{type}"))
end
end
end
class ReviewsController < ApplicationController
include Reviewable
alias_method :new, :new_review
end
class VideoReviewsController < ApplicationController
include Reviewable
def new
new_review(:video_review)
end
end
And it's done. We just added a module which have a new_review
method. We have included Reviewable
module in both ReviewsController
and VideoReviewsController
and called new_review
method from new action . We have used send
to call a method depending on the argument passed. We should prefer public_send
though.
We can also depend on params[:controller]
for getting the type
that we are passing as a argument to new_review
method. But its good to avoid dependency on params[:controller]
for such task. Also it will give you ' Remote Code Execution Vulnerability ' in code climate. Though it seems as false positive as params[:controller]
can't be changed by user
EDIT:
You can use controller_name
method inside controller instead of params[:controller] to avoid ' Remote Code Execution Vulnerability ' in code climate.
Using too much dynamic concept can make code unreadable. So be careful :)
APi doc links: