Dry with Ruby metaprogramming

You’ve probably already heard of Ruby’s metaprogramming capabilities but you may either think it is only suitable for building frameworks and DSLs or you don’t know when to make use of it. Here we’ll show you how little doses of metaprogramming can help to dry up your Rails app. It’s definitely a tool to keep in your toolbelt but it generally raises the overall abstraction of your code. So, watch out!

Include mixins in your modules

Modules in Ruby, besides providing namespacing, implement the mixin facility. However, modules may depend on the object they are included in. You may want to execute funcionality of your brand new module from an ActiveRecord callback. Let’s start with the following model:

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :organization
end

Let's say we want to track down whenever a membership is deleted. Easy enough. We just add the following:

class Membership < ActiveRecord::Base
  include MembershipDestroyLogger
  
  belongs_to :user
  belongs_to :organization
  
  # Warning! this couples Membership with MembershipDestroyLogger
  after_destroy :log_model_deletion
end

module MembershipDestroyLogger
  def log_model_deletion
    Rails.logger.warn("[MembershipDestroyLogger] Deleted Membership #{id}")
  end
end

The `MembershipDestroyLogger` nicely wraps it up. Note that `Membership` must implement the `after_destroy` callback in order for the module to work. Unfortunately, this couples `Membership` with `MembershipDestroyLogger`.

Let's say that now we also want to keep track of the oauth applications that get removed. That'd involve to implement the `OauthApplicationDestroyLogger`. If we keep implementing more destroy loggers we will soon end up having a bunch of modules containing the same code along with the required callbacks. It's time to dry things up.

Included hook

According to the Ruby documentation

`included`: Callback invoked whenever the receiver is included in another module or class

It basically executes whatever code you specified whenever a module is included into a class.

module Foo
  def self.included(base)
    puts 'Foo has been included somewhere'
  end
end

Note the method receives an argument. That represents the class where the module has been included into. That's all we need. We can now abstract our destroy loggers.

module DestroyLogger
  def self.included(base)
    base.after_destroy :log_model_deletion
  end

  private

  def log_model_deletion
    Rails.logger.warn("[DestroyLogger] Deleted #{self.class} #{id}")
  end
end

`base` is the model we include the module into, so the module can add the required `after_destroy` callback itself. Furthermore, as `self` points to model we can retrieve the model's class name.

Voilà! we can remove all the previous destroy loggers and just drop the module in whatever module we want to keep track of. So, finally our models look like this:

class Membership < ActiveRecord::Base
  include DestroyLogger
end

class OauthApplication < ActiveRecord::Base
  include DestroyLogger
end

Conclusions

As you can see, there is no need for digging into complex and abstract metaprogramming stuff to benefit from Ruby's features. Little doses of metaprogramming can already improve your code and lead you to a better world.

The `included` hook is also the underlying implementation of ActiveSupport::Concern's included method. With this and other techniques, ActiveSupport::Concern eases the implementation of the mixin pattern considerably.

References

If you want to expand your Ruby metaprogramming skills further, Metaprogramming Ruby 2: Program Like the Ruby Pros is a recommended reading. Specially its ActiveSupport's Concern Module.

 

Pau Pérez Fabregat

Computer science student currently working on the master thesis. Web addict.

 

Leave a Reply

Your email address will not be published. Required fields are marked *