Telephone +44(0)1524 64544

Backwards Compatibility and Migration Paths

Easing users' pain so they get the new shiny sooner

Sat Aug 22 20:15:00 2009

Digg! submit to reddit Delicious RSS Feed Readers

People who've been using Catalyst and DBIx::Class for a while will have noticed we have a strong commitment to backwards compatibility - DBIx::Class has been doing its best to avoid breaking running code since version 0.03 and Catalyst's commitment to compatibility has been in place since around version 5.50, also from the end of 2005.

This commitment means that I'm extremely confident (having a very good idea which features will be deprecated eventually and making sure we don't cover them) that the Definitive Guide to Catalyst will continue to lead people to writing working code for at least two years and hopefully longer - even if it won't always teach people how to write the best code of the day (unless we get a second edition, of course :)

There's a lot of debate about how much we've slowed ourselves down by doing this, how much perl itself has slowed itself down, and whether this has been a worthwile decision overall. Obviously I'm on the "worthwhile" side of the argument, but that doesn't mean I believe in maintaining compatibility forever. Across major releases features move from "normal" to "deprecated" to "warning loudly" to "gone entirely", as we refactor underneath and find clearly better ways to provide features.

However, the change to these new features is always provided as evolution, not revolution - you can keep your old code and implement newer code using the new shiny just fine, and if older features aren't any trouble to maintain then we may not even bother removing them - just undocument them so that people stop using them for new code without causing any trouble to people with perfectly working code that just happens to use them. Of course, then when they need to modify that code they'll find there's no docs anymore and that may make them think about moving just as well.

The most important thing though is to provide a migration path - a way for users to get from the old to the new without wanting to shoot themselves in the face with a shotgun. Deprecation warnings should reference the documentation for this, and it should be as extensive as possible.

So when Sebastian Riedel, the original founder of the Catalyst project ( who left not long after 5.50 due to technical differences between him and the rest of the core team), posted about changes to his new Mojo framework, I had to have a look. The Mojo team has "prepared a detailed changelog with instructions, and I'm going to reproduce the relevant parts here, with notes and my thoughts about how things could easily have been done a little differently.

0.991250 2009-08-18 00:00:00
        - This release contains many substantial changes that are not
          backwards compatible, but good news is that it's also the last
          major feature breaking release before 1.0. ;)

0.991246 -> 0.991250 ... what? How do you change huge amounts of code at that version? At the very least call it 0.999000 or similar. And of course the changelog indicates there were no dev releases, which would have given people a chance to test their apps beforehand.

          Older releases of Mojo did contain additional Mojo::Script::* and
          Mojolicious::Script::* modules that are obsolete now and might
          break this version if they are still present on your system.
          Because of this we highly suggest that you
          DELETE ALL MODULES IN THE "Mojo", "MojoX" AND "Mojolicious"

Well, why not just do a version check of old modules and ignore them? Or at least make sure that 'make install UNINST=1' does the right thing - or provide a script to check for such old modules and provide 'rm' invocations to remove them. Having an upgrade make even new applications written to the new standards potentially not work seems insane to me.

        - Mojo::Script has been renamed to Mojo::Command, this change is not
          backwards compatible!
          You will have to regenerate application scripts or replace
          "Mojo(licious)::Script" with "Mojo(licious)::Command" manually.

Or you could just have shipped Mojo::Script:: empty modules that loaded the real ones while emitting a warning that the application scripts needed to be regenerated or modified - Catalyst has long used a script versioning system so we can warn the user if they're running an old one (I'm fairly sure in fact that Sebastian wrote it - so I can't see any reason at all for not doing something similar again).

        - Removed unused features from Mojo::Base and simplified API, this
          change is not backwards compatible!
              __PACKAGE__->attr('foo', default => 'bar');
              __PACKAGE__->attr(foo => 'bar');

So check if @_ has the extra member and emit a warning telling the developer to change - it's a couple lines of code to do that and prevents every single class file in their project potentially breaking at once.

        - Merged eplite and epl, this change is not backwards compatible, you
          will have to rename all your eplite templates to epl.

I'm sure there's a reason for this. I'm sure there's a perfectly good reason they can't simply pick up two file extensions - or even *shock* make the extensions configurable so somebody can use .html as the extension if they want to. But I cannot for the life of me work out what it is.

        - Simplified MojoX::Renderer, this change is not backwards
          Handler can no longer be detected, that means "default_handler" or
          the "handler" argument are required.
          The template argument can no longer contain format or handler.
              $self->render(template => 'foo.html.epl')
              $self->render('foo', format => 'html', handler => 'epl')

Because clearly one regexp, a warning, and an option to turn it off would be too hard. Though I guess this explains why you couldn't use .html before ... still doesn't give me any idea why it can't be now.

And it goes on - more renaming of things without stubs for the old names (and since every class should pass the empty subclass test, if that doesn't trivially work there's something else you need to fix anyway). For added bonus fun, they appear to have removed their per-request context object entirely and switched to using controller base classes for customisation purposes instead. I'm not going to criticise this last one since it seems like a change that would be extremely confusing to try and keep compatibility across, but it's buried half way through the Changes and seems like it could do with a write up on how to switch.

I just don't understand this. I think everything I've described here would be maybe a few dozen lines of extra code - but a huge decrease in the amount of pain inflicted on the user base. To quote a couple of people who were around when I first went "what the ..." at this -

"couldn't he have done that at more like 0.3?"


"I understand there are reasons to rewrite your entire framework and fuck over your users that you sold down the river, but that is pretty extensive"

"That is Crazy Eddy insane."

Guys. You're selling yourself as being a good way to get beginners into perl. How many beginners are going to handle trawling through @INC and deleting .pm files manually? How many beginners are going to read a goddamn Changes file? We can do better than this, and it isn't hard. All you have to do is think a little about how your users are going to get from A to B, and get your code to guide them gently in that direction. Those few dozen lines of migration path code will save you a lot of support questions and you can use the time saved to do something more useful - and you can take them out after a few releases once the majority of people have upgraded anyway.

Old features don't need to be forever, but if your users are scared to upgrade then they'll never see the new ones. Backwards compatibility as far as you can is good, but migration paths are way more important. And that way you get more people testing and patching the latest release rather than the legacy code - and that's important too.

-- mst, out