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.
