Ruby
Module.prepend: a super story
I was super excited when I first heard about the prepend
feature included in the Ruby 2.0 release, but never had a chance to use it. Micah Cooper and I were working in an existing code base recently, and one of the project's requirements was to log API requests that took longer than a given threshold. The project talked to multiple APIs via XML, JSON and SOAP. We didn't want to change the existing code, because it had been tested and was working in production.
Our approach was to make the smallest change possible to the code base, so we came up with a simple DSL that would wrap the current methods without making any modifications. Here is the DSL we wanted.
class Service
include Bubot
watch :request, timeout: 1 do # |instance, time, result|
# execute this block when the request method
# takes more than the timeout (threshold) in
# seconds.
end
def request
# make api request and return results
end
end
Reaching into our Ruby toolbox, we immediately thought of the 'around alias' pattern. If you're not familiar, the around alias pattern requires the developer to define a new method, rename the original method, and then rename the new method to the original method name. Here is an example of how you would implement the pattern.
class Model
def save
puts "saving model"
end
def save_with_validations
puts "validating model"
save_without_validations
end
# around alias pattern
alias_method :save_without_validations, :save
alias_method :save, :save_with_validations
end
__END__
> m = Model.new
=> #<Model:0x007fa3a45e5d90>
> m.save
validating model
saving model
=> nil
At one point, Rails heavily depended on this pattern – in fact ActiveSupport.alias_method_chain
does this exact thing. If you'd like more info, check out the doc's here alias_method_chain. However, the alias_method_chain
method is no longer used in the Rails code base. (In fact, it was deprecated, removed, and then added back.)
Furthermore, the around alias pattern is generally frowned upon – and it should be avoided, because you are redefining the original method and can't call super
anymore. Before using this the around alias pattern, a developer should really ask themselves if inheritance or (better yet) composition could be used. Here's an example with inheritance.
module Validations
def save
puts "validating model"
end
end
class Model
include Validations
def save
super
puts "saving model"
end
end
__END__
> m = Model.new
=> #<Model:0x007fa3a45df468>
> m.save
validating model
saving model
=> nil
Here's an example using composition & dependency injection (this is preferred over the previous example):
class Validator < SimpleDelegator
def save
puts "validating model"
__getobj__.save
end
end
class Model
def save
puts "saving model"
end
end
__END__
> m = Validator.new(Model.new)
=> #<Validator:0x007fa3a45dcbc8>
> m.save
validating model
saving model
=> nil
Given that we did not want to change the existing code base, we implemented the around alias pattern with what we think is a useful and elegant DSL.
Here's the first pass (naive approach) at creating this DSL.
module Bubot
# included is a hook ruby calls when a module is included
def self.included(base_klass)
# the class that is being included to is being passed in
# and we are extending that class with class level methods
base_klass.extend(ClassMethods)
end
module ClassMethods
# this is the method we want on our DSL
def watch(method_name, timeout: nil, &bubot_block)
define_method("#{method_name}_with_bubot".to_sym) do |*args, &block|
start_time = Time.now
method_return_value = save_without_bubot(*args, &block)
if (total_time = Time.now - start_time) >= timeout
bubot_block.call(self, total_time, method_return_value)
end
method_return_value
end
alias_method "#{method_name}_without_bubuot".to_sym, "#{method_name}".to_sym
alias_method "#{method_name}".to_sym, "#{method_name}_with_bubot".to_sym
end
end
Problems with this approach and the DSL we wanted
Generally alias_method_chain
is considered a nasty approach, and I agree. Most ruby dev's would reach for composition or inheritance. However, we wanted this simple DSL, and we did not want to go through every service in the application and modify existing code. We wanted the code that was already in production – the code that was tested and used – to remain unchanged (as far as our commits were concerned). For this particular problem, because we were redefining the method at runtime, around alias seemed like the most elegant solution.
There are problems however, in order for the around alias to work the method has to be already defined. All calls to Bubot.watch
would have to be called after the method definition.
class Service
include Bubot
def response
# make api request and return results
end
watch :response, timeout: 1 do
# do stuff
end
end
And although this is OK, we'd really prefer calls to .watch
to be at the top of the file so they can be easily identified. So now we have to make the code a little uglier:
module ClassMethods
# this is the method we want on our DSL
def watch(method_name, timeout: nil, &bubot_block)
define_method("#{method_name}_with_bubot".to_sym) do |*args, &block|
start_time = Time.now
method_return_value = save_without_bubot(*args, &block)
if (total_time = Time.now - start_time) >= timeout
bubot_block.call(self, total_time, method_return_value)
end
method_return_value
end
alias_method_chain_or_register(method_name)
end
private
# if we already defined the method, alias_method_chain
# otherwise add it to a list that we can evaluate later
def alias_method_chain_or_register(method_name)
if method_defined?(method_name)
alias_method_chain(method_name)
else
(@method_names ||= []).push(method_name)
end
end
def alias_method_chain(method_name)
alias_method "#{method_name}_without_bubot".to_sym, method_name
alias_method method_name, "#{method_name}_with_bubot".to_sym
end
# method_added is a ruby hook that is called anytime a method is defined
def method_added(method_name)
if (@method_names ||= []).delete(method_name)
alias_method_chain(method_name)
end
end
end
More problems
And now we have a working version of bubot, except for when the method being watched makes a call to super
. The request
method is now called request_without_bubot
, and that method doesn't exist on the parent. Luckily our codebase didn't need to call super
, but the fact that this could break production code in the future made me very uncomfortable.
So what to do?
Luckily, I was able to spend a day pair programing with Paolo Perrotta (if you haven't read his book Metaprogramming Ruby, stop reading right now and get a copy, you will thank me). One of the things I wanted to ask him about is how I could get rid of the around alias pattern we were using. His solution was simple: "Use prepend", he said.
Prepend is a newer feature to ruby and is only available in ruby >= 2. Here is an example of how it works.
Imagine we have the following code.
module A
def hello
super if defined?(super)
puts "hello from A"
end
end
module B
def hello
super if defined?(super)
puts "hello from B"
end
end
class C
include A
prepend B
def hello
super if defined?(super)
puts "hello from C"
end
end
__END__
> c = C.new
=> #<C:0x007fa3a45d62c8>
> c.hello
hello from A
hello from C
hello from B
=> nil
> c.class.ancestors
=> [B, C, A, Object, Kernel, BasicObject]
So even though we initialized a C, when we send messages to an instance of C, Ruby looks first to B to see which methods to call. This was a revelation to me about how Ruby works. Ruby's class ancestors are nothing more than a lookup chain for method calls – this is much different than other languages. We created a new C, but Ruby is looking to B first, then to C, then to A and then up the rest of the chain.
Armed with this knowledge, we could now extend Bubot
onto class, which will set Bubot
as a parent so .watch
can be called anywhere in the child (our objects) code. But more importantly, we prepended a module after the class so that any message sent to the class will look at the prepended module first. It's that simple: we create a method with the same name and call super
.
module Bubot
def self.included(base_klass)
base_klass.extend(ClassMethods)
interceptor = const_set("#{base_klass.name}Interceptor", Module.new)
base_klass.prepend interceptor
end
module ClassMethods
def watch(method_name, timeout: nil, &bubot_block)
interceptor = const_get "#{self.name}Interceptor"
interceptor.class_eval do
define_method(method_name) do |*args, &block|
start_time = Time.now
method_return_value = super(*args, &block)
if (total_time = Time.now - start_time) >= timeout
bubot_block.call(self, total_time, method_return_value)
end
method_return_value
end
end
end
end
end
And if we look at our original desired DSL, here is what the ancestors look like.
class Service
include Bubot
watch :response, timeout: 1 do
# execute this block when the response method
# takes more than the timeout (threshold) in
# seconds.
end
def response
# make api request and return results
end
end
__END__
> s = Service.new
#<Service:0x007fa3a45cf9a0>
> s.class.ancestors
=> [ServiceInterceptor, Service, Bubot, Object, Kernel, BasicObject]
Now we have a working DSL, we can still make calls to super
from our original method and we don't have to worry about when the call to watch
happens. If you'd like to see the full source or contribute, the repo is here bubot.