/ rails

Dynamic Ruby

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:

http://apidock.com/ruby/Object/instance_variable_set

http://apidock.com/ruby/Object/instance_variable_get