Monkey-patching, subclassing, and accidental overriding
One of the great things about open-source software is the ability to reuse handy classes written by other people. But sometimes, you find yourself using a class that doesn't have quite enough features for what you're trying to do. What's the best way to deal with that sort of situation?
One option would be to monkey-patch new code into the class you're using — just add extra subroutines to the original namespace. But unconstrained monkey-patching has consequences that make it extremely hard to use in practice. So the usual alternative recommendation is to subclass the upstream code, add the new methods in the subclass, and then ensure that the rest of your program always uses the subclass in place of the original. But that approach has two flaws. First, it can be awkward to make sure your subclass is always used in the right places. Second, it doesn't actually fix the problem: you can still experience all the same issues as with monkey-patching!
I gave a talk on this topic at this year's YAPC::EU in Rīga, and it's getting a repeat (and extended!) outing at the inaugural Dynamic Languages Conference today. But if you'd like to read the full details, the corresponding paper is now on my website.
Enjoy!
Your ex::monkeypatched module looks useful. As I've done this a lot myself, I have one feature request: could you add an optional constraint for version numbers? If hte admins update Third::Party::Class and you don't know about this, it could be problematic. Unfortunately, you might have to change your API slightly:
use ex::monkeypatched;
ex::monkeypatched->inject('Third::Party::Class' => ({
methods => {
clunk => sub { ... },
eth => sub { ... },
},
version => 1.21,
});
That constrains you to a particular version of Third::Party::Class and if the version changes, it would warn. That's because the desired behavior may have been added under a different name.
Alternatively, you could have inject() return an ex::monkeypatched object and keep your existing API:
use ex::monkeypatched 'Third::Party::Class' => (
clunk => sub { ... },
eth => sub { ... },
)->version(1.21);
Hi, Ovid. Thanks for the idea.
Is warning (rather than throwing an exception) the right thing in your situation? My inclination is to make it an error, like the error you get when the class being patched contains a method of the appropriate name. But then, you’ve experienced the need for this feature, and I haven’t.
As for the API: I’ll think about that a little further. It may be possible to extend the API to support this without changing the existing stuff; class and method names don’t begin with digits, while version numbers do. On the other hand, the current API is, as you note, unfriendly to being extended, so maybe switching to something more explicit would be useful. And that’s exactly why the current release is called
ex::monkeypatched
rather than justmonkeypatched
.The idea behind the warning is simple: never break production. For a related module, see Devel::Deprecate and how I handled it there. You probably won't kill anything by upgrading a module you've monkey patched, but if the code you've made the monkey patch with is what kills the Web site, people are going to be very unhappy with you.
In short: never, never, never break production by accident. You break it very deliberately when you *know* that you have no choice but to break it.