Backwards Compatibility with Ruby

Mike Perham ·

One of my pet projects is my Ruby gem for accessing memcached, Dalli.  Dalli is actually a follow-up to a previous gem, memcache-client, which has been the most popular Ruby library for accessing memcached for several years now.  Since I want people to upgrade from memcache-client to Dalli, I’ve made the API “mostly” compatible but there were minor changes I wanted to make.

I really didn’t want to add a lot of ugly logic to handle API compatibility issues but Jeremy Kemper (of Rails core fame) recently pointed out a nice, clean way to handle the compatibility logic: Object#extend.  Let’s look at an example of an API that has changed:

   # memcache-client
   def set(key, value, ttl=0, raw=false)
   # dalli
   def set(key, value, ttl=nil, options={})

In this case, I’ve changed the last parameter from a very specific boolean to a more generic (and idiomatic) hash of options. I don’t want to put in ugly logic to check for this case in the main codebase so I added a backwards-compatibility layer:

  require 'dalli/memcache-client'

That enables this code:

class Dalli::Client
  module MemcacheClientCompatibility
    
    def initialize(*args)
      Dalli.logger.error("Starting Dalli in memcache-client compatibility mode")
      super(*args)
    end

    def set(key, value, ttl = nil, options = nil)
      if options == true || options == false
        Dalli.logger.error("Dalli: please use set(key, value, ttl, :raw => boolean): #{caller[0]}")
        options = { :raw => options }
      end
      super(key, value, ttl, options) ? "STOREDrn" : "NOT_STOREDrn"
    end
  end
end

Finally, in Dalli::Client:

    def initialize(servers=nil, options={})
      ...
      self.extend(Dalli::Client::MemcacheClientCompatibility) if Dalli::Client.compatibility_mode
    end

We’re doing several things here:

  • Extending the Dalli::Client instance with our new compatibility module. Because of the way inheritance works in Ruby, anyone who calls set on a Dalli::Client instance will have our set method called instead of the original Dalli::Client#set. We can hide our compatibility checks here; we just need to invoke super to call the original set method.
  • Override the initialize method to log a warning to the developer that we are using this mode. It’s just supposed to be a temporary measure, not permanent!
  • Use our set method to check for a boolean value. Modify the parameter and print a warning as necessary. The returned value is also different in memcache-client so we adjust that too.
  • Note the use of Kernel#caller in the error message. This tells the developer the line of code that called the API incorrectly.

Backwards compatibility is a pain. As a Rubyist, I find myself concerned with code aesthetics and this trick allows me to keep my code clean while also providing a nice feature for people upgrading to Dalli for the first time.