There are many ways to change the behavior of an existing class in Ruby. Every so often you run into a new set of constraints that lead to to a way of doing it that you might normally reject, but is surprisingly sound in context. Here is an example that a couple of colleague’s encountered today and I had the pleasure of talking through with them.

Suppose we’re working on a Rails application with a service like the one below:


  module AirspeedService
    def self.get_velocity(bird_description)
      return 24
    end
  end

Now let’s suppose that in the development environment we want to be able to simulate an error by providing a certain agreed upon input, but otherwise have the same behavior as the normal production service. One way to skin that cat is to simply use a Rails style stub to reopen the class and add the behavior we want, making sure the stub is required in the development environment. In this solution the stub would look something like this:


  module AirspeedService
    class <<self
      alias_method :original_get_velocity, :get_velocity
    end

    def self.get_velocity(bird_description)
      if key == "unladen swallow" 
        raise "What do you mean? An African or European swallow?" 
      end
      original_get_velocity(bird_description)
    end
  end

This is a fine solution, provided we don’t care about testing our stub. If we want to make sure our stub works as we expect, it will have to be loaded in the test environment. This will allow us to test the behavior that the stub adds to the service, but there is a cost. Now the tests written for the unmodified AirspeedService are calling a modified version, running the risk that we are not testing the production code. It avoid this problem we need to extract the stub behavior to a separate module and then inject that behavior into the service only in the environment where it is desired. We can change our stub to look like this:


  module AirspeedServiceStubs
    def self.extended(mod)
      mod.module_eval do
        class <<self
          alias_method :original_get_velocity, :get_velocity
          remove_method :get_velocity
        end
      end
    end

    def get_velocity(bird_description)
      if key == "unladen swallow" 
        raise "What do you mean? An African or European swallow?" 
      end
      original_get_velocity(bird_description)
    end
  end

and then add a line like this to development.rb:


  AirspeedService.extend(AirspeedServiceStubs)

This approach gives us the ability to test AirspeedServiceStubs and AirspeedService separately—a big win. We have, however, sacrificed readability and intentionality. In order for the stubbed version of the method to be called we must now use remove_method to move the original one out of the way (since the module’s methods effectively superclass methods to those in AirspeedService). Furthermore we had to use the extended hook to do the method aliasing, which is more difficult to read and understand than simply reopening the same class. We can, however, simplify our code somewhat while still using a module:


  module AirspeedServiceStubs
    def self.extend_object(mod)
      mod.module_eval do
        class <<self
          alias_method :original_get_velocity, :get_velocity
        end

        def self.get_velocity(bird_description)
          if key == "unladen swallow" 
            raise "What do you mean? An African or European swallow?" 
          end
          original_get_velocity(bird_description)
        end
      end
    end
  end

By moving the definition of the stubbed get_velocity method inside the module_eval, we’ve largely regained the readability of the original implementation. The method aliasing code and new definition of get_velocity are closer to one another, meaning you can concentrate on the implementation without having to process the noise surrounding it (contrast this with the previous approach, where the get_velocity definition and method aliasing were occurring in two different scopes). This implementation also uses extend_object instead of extended. extend_object is the method Ruby invokes on a module to have it add behavior to a object instance. Normally it simply includes the module into the metaclass of the object being extended and then calls the extended hook we used in the previous example. Since AirspeedServiceStubs no longer behaves very much like a module, I’ve changed it to take over the extend_object process completely and do its work. Note that super is never called from extend_object, so if you were to add an instance method to the module it would never get added to AirspeedService.

There’s one more change I would consider making in this case. Changing the extend line in development.rb from


  AirspeedService.extend(AirspeedServiceStubs)

to something like


  AirspeedServiceStubs.add_stub_behaviour(AirspeedService)

might convey more intent to the reader. Rather than tricking the reader into thinking that AirspeedServiceStubs is a module that simply adds methods to the service, using a custom method name calls out that something different is happening that might be worthy of further investigation. The implementation of this is simple, since we have already taken over the job of adding the stub behaviour ourself. Here it is, with the inner code omitted for brevity this time:


  module AirspeedServiceStubs
    def self.add_stub_behaviour(mod)
      mod.module_eval do
        ...
      end
    end
  end

I like this solution for its readability and what it communicates to someone reading the code for the first time. It’s worth noting that we’ve made a tradeoff with the very last change we made, however. Since our stub code previously looked like a normal Ruby module (i.e. it was added using the extend method), we were free to refactor the innards without worrying about how it was called. We’ve now sacrificed that flexibility in favor of communicating more to the first time reader. Personally, I like this trade-off. There are sure to be situations where it doesn’t make sense though, so it’s a good idea to stop and think about which way is better for any particular case.

In the end we’ve added only a small bit a complexity to get a lot more control over where the stub code loaded and where it actually stubs the behavior of the original service. We can now test the stub and the service completely independently and still apply the stub to the service in a specific environment. I think that’s a good thing.

Sorry, comments are closed for this article.