Heading image for post: Module.prepend: a super story

Ruby

Module.prepend: a super story

Profile picture of Micah Woods

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.

More posts about Ruby Development

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project