Yesterday, I have been playing with Aquarium, an AOP framework for ruby. I decided hack a little bit. I try to refactor the dynamics finders of ActiveRecord, using an AOP approach.
First lets try to understand what are and how dynamics finders works.
This are queries are equvalent :
User.find(:first, :conditions => ["name = ?", name])
User.find_by_name(name)
User.find(:all, :conditions => ["city = ?", city])
User.find_all_by_city(city)
User.find(:all, :conditions => ["street = ? AND city IN (?)", street, cities])
User.find_all_by_street_and_city(street, cities)
And how this cool methods are generated? They are not generated, ActiveRecords use a bit of the method_missing magic to parse the method_name to find a pattern. A simplify version of the method_missing :
def method_missing(method_id, *arguments)
if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
# find...
elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
# find_or_create...
else
super
end
end
Now its time to introduce the AOP in the recipe. We create two advices, the pointcut is the method missing. This two advices perform as a chain, first the last defined.
class Base
before :method=>:method_missing do |this, execution_point, *args|
if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(args.fir st.to_s)
# find_by_xxx... #do something smart and break the flow
end
end
before :method=>:method_missing do |this, execution_point, *args|
if match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/. match(args.first.to_s)
# find_or_xxx #do something smart and break the flow
end
end
# And so on, until you have a minimum common behaviour or an empty method_missing.
end
We can extract more behaviour from the method missing, We can repeat this process until we have the minimum common behaviour or a empty method_missing. Sometimes we neer to break the chain at some point. In this cases we can uses around.
One of the benefits of this approach is that will be more easy to introduce new finder in rails. Currently to create a new finder we need to create a plugin in the vendor folder. For example we are going to create a find_like finder. We write our code in the vendor/plugins/find_like/lib/find_like.rb :
module ActiveRecord
class Base
class << self
private
alias_method :previous_method_missing, :method_missing
def method_missing(method_id, *arguments)
if match = /find_(all_like|like)_([_a-zA-Z]\wo*)/.match(method_id.to_s)
# do something smart
else
previous_method_missing(method_id, *arguments)
end
end
end
end
end
If we use the AOP approach we need to write, also our code in the vendor/plugins/find_like/lib :
module FinderLike
# A custom finder finder_like_xxx
before :types => :Base, :methods =>:method_missing do |execution_point, *args|
if match = /find_(all_like|like)_([_a-zA-Z]\w*)/.match(args.first.to_s)
# Do something smart and break the flow
end
end
end
I Don’t know if this is a good design in this particular case. But at least was a funny Kata.