This is the beginning of a series of posts on sustainable development in Ruby. No, I’m not talking about writing code on wind-powered laptops while sipping fair-trade coffee. But the sustainable development movement has a fairly direct analog in software development. As programmers, we work within a code ecosystem. The ease with which we write new programs is impacted by the choices of other coders before us, and likewise the decisions we make while coding affect other programmers down the line.
As with industrial development, in the early years of a particular software ecosystem it’s not always obvious that the choices we are making might be detrimental to our successors.
The first generation of programmers is usually enthusiastic; any failure is a personal failure, so you can gloss over those things. It’s the second generation that’s going to be less enthused, that’s going to stare in bafflement at these classes that mysteriously spawn methods, and trying to figure out what’s going when there’s an exception in dynamically generated code.
When a programming language is relatively young, unsustainable practices often go unnoticed, or are dismissed as easily avoidable.
A mere flesh wound, says our programming primate: I usually don’t get conflicts, so I’ll pretend they won’t happen. The thing is, as thing scale up, rare occurrences get more frequent, and the costs can be very high.
When a language is only a few years old, the amount of code written in it is necessarily small. Legacy code is measured in the thousands of lines, rather than hundreds of thousands. If a section of code, or a third-party library, is causing problems–well, you can always rewrite it. As codebases grow, however, the rewrite option becomes less and less viable. Coping with legacy code is an everyday fact of life for most professional programmers.
If we take it to mean any and all techniques for making software more robust and easy to maintain, sustainable software development is a very broad subject. Indeed, one could argue that most of the major advances in the software field in the last 30+ years have been made with sustainability in mind – OO, refactoring, TDD, to name a few.
In this and the following essays I’m only going to be addressing one specific aspect of sustainability in the Ruby language. I’m going to be talking about the practice of dynamic class modification, colloquially “monkeypatching”. I’m addressing this subject because I believe injudicious use of dynamic class modification to be one of the greatest threats to long-term sustainability currently facing Ruby.
What are we talking about when we say monkeypatching? Some divide dynamic class modification into two categories:
- Runtime addition of methods
- Runtime redefinition (overwriting) of methods
Some maintain that only the latter definition is true monkeypatching. The line is blurrier than it might first appear, however. If two separate libraries both add a #to_xml method to the Object class, then individually they are only adding a method, but when both libraries are required by the same program one will be overwriting the other–whichever one is loaded last. For this reason I will use “monkeypatching” to mean both the dynamic addition and redefinition of methods, albeit with an emphasis on redefinition.
Specifically, I will be referring to dynamic non-local addition and redefinition of methods. By non-local, I mean that the dynamic modification occurs outside of the original class definition. The following is not a monkeypatch:
class Foo attr_accessor :bar # defines bar, bar= # redefines bar def bar # ... end end
Whereas this version demonstrates monkeypatch:
class Foo attr_accessor :bar # defines bar, bar= end # re-open the class class Foo # redefines bar def bar # ... end end
No easy answers
this “monkey patching” thing is seriously powerful
languages—like Ruby—that include dangerous features give the fringe a broader latitude to invent new things. Of course, they also break things and they invent stupid things and they get excited and write entire applications by patching core classes instead of writing new classes and commit all sorts of sin.
One of the things that’s really great about agile languages is they give you the power to do anything. One of the most horrible things about agile languages is they give every other idiot the same power to stab you in the back with a rusty pitchfork.
— Zed Shaw
This series will not tell you when to monkeypatch and when not to. It is not my intent to set myself up as the arbiter of when monkeypatching is justified. These posts make the assumption that you already understand that Ruby dynamism is both tremendously powerful and potentially dangerous. I want to present some alternatives to monkeypatching, so that you can make an informed decision when deciding whether to use monkeypatching to solve a particular problem.
In order to help you make that judgement, I’ll be characterizing the techniques presented by their applicability. Some of the aspects affecting applicability are introduced below. In this context, the term “vendor” is used to refer to any code you don’t have the ability to change upstream – whether from a third-party library, or written by another team down the hall. The term “target” is used to mean a vendor-defined object whose methods you wish to modify.
- Who controls creation of the target? Does your code call the class constructor, or is the object given to you already created by vendor code?
- Are you the only client? Is the target used by third-party code, or is your code the only code that touches it after it is created?
- Can you intercept the target? If the target is produced and consumed by vendor code, is there still a point at which your code has access to it? Or is it’s use completely internal to vendor code?
These factors and others will affect which techniques are right for any given case.