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.