Using ActiveSupport to deprecate gems code

Alberto Vena

23 Nov 2017 Development, Ruby On Rails

Alberto Vena

6 mins
Using ActiveSupport to deprecate gems code

Often, when writing Ruby gems, there's the common problem of changing things that our project's users could be using in their own code. When they will update our gem, they could find issues since interfaces provided are no more there. This blog post explains a way to handle this situation.

Code becomes obsolete

In order to be improved, gems need to change their code. That's how software development works and there's nothing we can do about it. Every single line of code can change in a gem, from a method signature to a class name.

To better explain how deprecation works I've crafted an example gem. This is a class of that gem and its code has became obsolete:

module LazySunday
  class Evening
    attr_reader :blockbuster
    def initialize
      @blockbuster = Blockbuster.new
      3.times { watch(random_movie) }
    end
    def random_movie
      blockbuster_movies.sample
    end
    def blockbuster_movies
      blockbuster.movies
    end
    def watch(movie)
      # Snooze in front of the movie
    end
  end
end

LazySunday::Evening is a straightforward class that describes our busy and full of social interactions sunday evening routine in 90's.

Well, it's 2017 and it turns out that this routine is obsolete now. We really need to update our code like this:

module LazySunday
  class Evening
    attr_reader :netflix
    def initialize
      @netflix = Netflix.new
      3.times { watch(random_movie) }
    end
    def random_movie
      netflix_movies.sample
    end
    def netflix_movies
      netflix.movies
    end
    def watch(movie)
      # Snooze in front of the movie
    end
  end
end

And no more long walks in the cold winter!

Wait but, what if some developers that use this gem were using the blockbuster_movies method in their projects?

By just removing the method we are going to break their code when they'll update this gem. Come on, we are nice and we don't want that to happen. Here it comes the concept of Deprecation:

A Deprecation is the discouragement to use a particular method, class or feature. By "deprecating" something we want to tell to users who is using our gem that a particular thing has been changed and its usage will probably be removed in the next versions. They should take action as soon as possible and adapt their code to reflect the new gem design. In other words a Deprecation is a way to provide backward compatibility, giving developers the time to be compliant with the new standard.

How to deprecate code?

A deprecation is just a message, it is usually printed to the stderr stream. The most basic version of a deprecation can be made by using Kernel#warn:

A possible way to deprecate a method, using the above example, could be:

def blockbuster_movies
  warn('DEPRECATION WARNING: blockbuster_movies is deprecated and will be removed from LazySunday 3.0 (use netflix_movies instead)')
  netflix_movies
end

This allows to call the old blockbuster_movies method but it will also:

  • notify the user that we'll remove it in a specific future version
  • suggest users a way to change their code that will be compatible with new versions
  • actually use the new suggested behavior

The interface (in this case the method) is still there, it is callable but under the hood it is already acting like the new one enabling the new shining feature we've built.

Deprecation with ActiveSupport

ActiveSupport provides some useful methods to make this process even easier.

First thing to do is creating our own deprecator class:

# lib/lazy_sunday/deprecations.rb
require 'active_support/deprecation'
module LazySunday
  BlockbusterDeprecation = ActiveSupport::Deprecation.new('3.0', 'LazySunday')
end

The first argument is the deprecation horizon, which represents the first version number that won't have this feature available. The second argument specifies the library name in order to print a message that can help our gem's users to understand where the deprecation comes from.

Let's go ahead and deprecate our obsolete method with ActiveSupport:

def random_movie
  netflix_movies.sample
end
def blockbuster_movies
  BlockbusterDeprecation.deprecation_warning(:blockbuster_movies, 'use netflix_movies instead')
  netflix_movies
end
def netflix_movies
  netflix.movies
end

If someone tries to access the blockbuster_movies method, this message will be printed on stderr:

DEPRECATION WARNING: blockbuster_movies is deprecated and will be removed from LazySunday 3.0 (use netflix_movies instead) (called from random_movie at /Users/kennyadsl/Code/kennyadsl/lazy_sunday/lib/lazy_sunday/evening.rb:12)

ActiveSupport also provides a shorthand syntax to accomplish the same goal:

def blockbuster_movies
  netflix_movies
end
deprecate blockbuster_movies: :netflix_movies, deprecator: BlockbusterDeprecation

These two kinds of deprecation live inside or near the deprecated method, so it's easy to spot if you are looking at the source code. If you prefer to group all deprecation in a single place you can even use deprecate_methods which allows to define multiple deprecations with a single statement

# lib/lazy_sunday.rb
BlockbusterDeprecation.deprecate_methods(LazySunday::Evening,
  blockbuster_movies: :netflix_movies,
  another_obsolete_method: 'Use the new method instead'
)

This works! When our users will update the gem, if they used the blockbuster_movies method, they will see the deprecation wanrning!

Deprecating classes

Some reckless developer could have even extended their code using the Blockbuster class directly in unexpected ways.

At the current state, this wouldn't print any deprecation message and, since our goal is to root all Blockbuster's code out from the project, by just deleting the Blockbuster class would raise bad errors for them.

ActiveSupport helps us with some easy way to deprecate classes.

# lib/blockbuster.rb
Blockbuster = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Blockbuster', 'Netflix', LazySunday::BlockbusterDeprecation)

By using DeprecatedConstantProxy, every time the Blockbuster class is called a deprecation warning will be printed out and the method called will be proxied to our new Netflix class.

> Blockbuster.new
DEPRECATION WARNING: Blockbuster is deprecated! Use Netflix instead. (called from irb_binding at (irb):2)
=> #<Netflix:0x007fa0b7a9c8d8>

As you can see, a Netflix instance has been returned instead.

Deprecating instance variables

Do you remember our first version of the Evening class?

module LazySunday
  class Evening
    attr_reader :blockbuster
    def initialize
      @blockbuster = Blockbuster.new
      # other stuff
    end
  # ...
  end
end

Since we have the @netflix instance variable now we could be tempted to remove the @blockbuster one. But some developer could have extended this class relying on that instance variable. We should deprecate that as well:

module LazySunday
  class Evening
    attr_reader :blockbuster, :netflix
    def initialize
      @blockbuster = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(
        self,                               # This class instance
        :netflix,                           # The method that will be called instead, can be another instance variable
        :@blockbuster,                      # Deprecated instance variable, will be used in the message
        LazySunday::BlockbusterDeprecation  # Our deprecator
      )
      @netflix = Netflix.new
      # other stuff
    end
  # ...
  end
end
> LazySunday::Evening.new.blockbuster.to_s
DEPRECATION WARNING: @blockbuster is deprecated! Call netflix.to_s instead of @blockbuster.to_s. Args: [] (called from irb_binding at (irb):1)
=> "#<Netflix:0x007f9fe5af7e68>"

RSpec and Raise on Deprecation Warnings

In our gem, running specs after deprecating code will probably produce a lot of deprecation warning in our output. If we did everything right they'll also probably be green though. This means that we are still using some code that has been deprecated and we don't want that.

It's tipically a good idea to raise an error when we try to access something that we just deprecated. This is quite simple, with ActiveSupport, since we can easily define the behavior of our deprecators.

# spec/spec_helper.rb
LazySunday::BlockbusterDeprecation.behavior = :raise

Mastering Deprecation

I decided to write this post since there's not a lot of documentation about this part of ActiveSupport, probably since it's something intially thought for internal use only. If you want to study this topic in deep you should start from ActiveSupport source code. Also, I found this gist from Rafael Franca very useful, since it pragmatically shows how to use ActiveSupport::Deprecation.

Conclusions

This blog post contains some non sense examples to help (especially me) understand how to deprecate Ruby application components.

In real life having this kind of accuracy is a big signal of a project's maturity, showing how much developers who wrote that code is caring about their users base.

Thanks to open-source we can dig into mainly all gems code and see if it contains deprecation warnings. If it does we'll probably have less troubles when we'll need to update that gem.

You may also like

Let’s redefine
eCommerce together.