Learning From the Platypus

  • Patrick Robertson
  • December 9, 2010

Out of the many exciting things I am working on at Velir, perhaps one of my favorites is revamping up our best practices surrounding Ruby coding. As a firm that traditionally has had a lot of strength in the realm of C# coding, one of the most controversial practices of Ruby is  "duck punching".

With all the greatest intentions in the world, a naive young programmer attempting to make a duck sing with some gentle poking and prodding can create a platypus instead. Not only is platypus a slight bit weirder than a duck; it also happens to be poisonous for any support developers that have to handle it later! 

Duck Punching Gone Awry

Let's take a look at how we can create a platypus and then how to avoid it. In this example we're going to change how hashes work in the context of a Rails application. Our fearless developer notices that symbolizing hash keys is taking an awful lot of time in his User model. Using the dynamic capabilities of Ruby, he decides upon a solution to this problem:

  # app/models/user.rb
  class User < ActiveRecord::Base
    has_many :authorizations

    #symbolize_keys is going way too slow.  So I optimized it.
    class Hash
      def symbolize_keys
        #insert totally optimized code here
      end
    end

    def method_that_symbolizes_keys
      #super fast symbolizing code is used here
    end
  end

There are a number of problems introduced by this cavalier approach to improving how hashes are implemented:

  • The "improvement" to the Hash class has been thrown in the middle of another class and is nearly impossible to locate. If this rails application had 50 models, it would probably take days to find where the duck punch happened for a support developer.
  • Not only is it impossible to locate, but it is very difficult to know when this enhancement is made. We know that the methods after it in User will benefit, but what else will? The symbolize_keys method has the potential to behave very differently and without explanation in different areas of the application.
  • When a method begins to misbehave, an experienced Rubyist will pop open irb and call the classes ancestors method. If someone performed a responsible modification of it's behavior, you'll see evidence in there. In this case, you'll see no evidence of our programmers tampering with Hash.
  • Last but not least, the old method has been eradicated at runtime. The old method apparently wasn't good enough, but it didn't need to die!

Responsibly Punching that Duck

This story has a happy ending though! The team lead for this project noticed the commit and suggested a couple of modifications:

  # lib/user_suport.rb
  module UserSupport
    module CoreExtensions
      alias :old_symbolize_keys :symbolize_keys
      def symbolize_keys
        #insert totally optimized code here
      end
    end
  end

  class Hash
    include UserSupport::CoreExtensions
  end

In the end, we've accomplished the same goal as the previous example. This time though, we've made sure that we don't get a poisonous duck/marsupial by accident. Let's list what has changed:

  • Now the new and improved method exists in a location that a support developer would look for when he/she is looking for errant duck punching.
  • The modification now has to be explicitly required in the Rails application. You can control exactly when hashes get a new and improved symbolize_keys
  • Hash.ancestors() will now return UserSupport::CoreExtensions somewhere along the way. This is considerably easier to search for in a large project than Hash or symbolize_keys.
  • By creating an alias of old_symbolize_keys, you've spared the existence of the old method. The alias binds at the time alias is declared, so your redefinition has no affect on the alias itself. This is a useful trick in creating a method that needs the old one to function.

One of the primary reasons that "monkey patching" is generally not nearly as large a problem as it could be, is that the practice of enhancing core libraries in this far safer manner has been in Rails since before 1.0. Still, many developers will tell you of stories of ducks turned platypi. Maybe the next time you see someone beating up a defenseless duck you'll submit a patch and show them how it's done!