More class loading adventures

02 Sep 2016

I was working on upgrading a library in a Rails 4.1 application. It was a big-ish upgrade; 17 version bumps and more than 2 years since that gem had been upgraded. Things were proceeding along well until I ran the test suite and, when instantiating one of the classes from that library:

ArgumentError: wrong number of arguments (given 1, expected 0)

I poked around the gem a bit and that class' constructor also took one argument, so it seemed like that should have been fine. Then I noticed that the application was monkeypatching that class. Looked like a trouble source - and yup, it was.

The interesting bit was that the monkeypatch alone wasn't entirely responsible for the issue. The problem was that the monkeypatch was in a directory tree that mirrored the structure in the gem. So the monkeypatch was for the class Baz in this hierarchy:

lib/foo/bar/baz.rb

And Baz was in the gem in the same directory structure, although in .bundle/gems/the_upgraded_gem/ of course. So that's what was happening. First, the test referenced the class name. On seeing that reference, Ruby tried to find it, and when it loaded the file from the application's lib/ directory, that satisfied the requirement and defined the class, so it didn't look any further and the class declaration in the gem was never executed. Of course the monkeypatch didn't declare a constructor; it only had the one method we wanted to add to that class. So later when the app tried to instantiate the class, boom. This is a situation that Justin Weiss covers nicely in his post on monkeypatching; it's the scenario that he mentions around monkeypatching Date. Our monkeypatch file didn't require the gem's file, so, issues resulted.

One thing I don't understand is that, if I renamed the file containing the monkeypatch to my_baz.rb, the interpreter would load the file from the gem first and all was well. The class defined in the gem would be declared, the monkeypatch would be loaded and define its method, and everything would work as designed. I need to dig around a little more to figure out what's going on there.

I solved the problem temporarily by moving the monkeypatch into config/initializers/. In retrospect that probably further muddies the waters, because it's the directory structure that's the problem, not "initializer vs lib/ file". But really, the better solution will be the one that Justin suggests - define a module, put the monkeypatch in it, and include it into the patched class.

Also, we've since upgraded that app to 4.2. Feels good!