02 October 2008

Rails refactoring exercise

I started my blog after reading jay field's post on the underuse of modules and 'design pattern in ruby' book. Mainly because i thought it would be interesting, as a learning process, to explore the silent power of modules.

So knowing Rails was doing a heavy use of alias_method_chain as it's AOP trick, i did a shallow dive in ActiveRecord's code to see how it could be transformed in the MultiInheritance-like-style of modules.

require "rubygems"
require "uninclude"

module Core
def save; "saved" end
end

module Dirty
def save; "clean + " + super end
end

module Transaction
def save; "transaction + " + super end
end

module Validation
def save; "valid + " + super end
end

class Base
include Core
include Dirty
include Transaction
include Validation
end

# PLUGIN adding more validation
module MyValidation
def save
"more validation + " + super
end
end

# PLUGIN overiding Transaction
module MyTransaction
def save
Base.send :uninclude, Transaction
body = "my transaction! + " + super
Base.send :include, Transaction
body
end
end

# Our everyday model
class A < Base
include MyTransaction
end

class B < Base; end

class C < Base
include MyValidation
end

a = A.new
b = B.new
c = C.new
puts a.save #=> my transaction! + valid + clean + saved
puts b.save #=> transaction + valid + clean + saved
puts c.save #=> more validation + transaction + valid + clean + saved



UPDATE
I published the post unfinished not thinking it would attract much attention without annoucement : so sorry for the lack of explanation.
  • the main idea is to move (almost?) all public instance methods of ActiveRecord::Base in a Behavior module called ActiveRecord::Core - which makes sense since they're the methods to be deCORAted.
  • The uninclude gem is here to enable the possibility for a plugin to override an AR "core" behavior in a sub "everyday" model.
  • Another way I thought of was this (below), unfortunately i get : NoMethodError: super: no superclass method ‘save’

module MyTransaction
def save
Transaction.instance_eval do
@@save = instance_method(:save)
remove_method :save
end
body = "my transaction! + " + super
Transaction.module_eval do
define_method :save do
@@save.bind(self).call
end
end
body
end
end

  • another solution would be to use the callstack to intercept the presence of MyTransaction#save and tell Transaction to do a transparent super.

3 comments:

Tyler Tompkins said...

The code you posted is very elegant. It's inspired me to do further investigations into the use of modules in ruby/rails.

Charly said...

Thanks ! I would love it if this avenue was explored wether it leads to a change or not.
Whenever i'll have time i'll put my nose back into it, and try and keep the blog updated. Their are many things to consider :
- include vs extend
- super_as(Validation) in facets lib
- the new inheritance scheme in 1.9

etc etc

Avdi said...

Nice. I am so glad to see people reawakening to the power of Modules in Ruby.