Metaprogramming is a powerful capability of Ruby. It describes the ability to treat data as code. So, for example, it is quite possible to evaluate a string at runtime such as “def x(y);puts(y.reverse);end” and end up with a completely new method called x bound into the program that is currently running!
Moreover, such methods can be bound into standard Ruby classes such as Array or even Object - the class from which all other classes descend - so that the entire behaviour of every single Ruby class can be changed on the fly as the program executes. You can, in a similar way, modify the behaviour of existing methods or even create entire new classes. This is great stuff and, when used with care, can be extremely powerful. Metaprogramming is, for example, used all over the place in the well-known Rails web framework for Ruby; it provides Rails with the ability to wire up data and evaluate Ruby code embedded into HTML during runtime.
However, this power comes at a price. One example quoted by Avdi Grimm is of a Rails plugin that dynamically modifies certain Rails classes such as ActiveRecord. He comments:
As a result of this implementation, it is tightly coupled to the inner workings of ActiveRecord. A small change to Rails and it could cease to work...
This rang a bell with me. I make no secret of the fact that I have always had considerable reservations about Ruby’s tolerance of encapsulation-breaking. In most respects, Ruby is as well-encapsulated as that most influential of Object Oriented programming languages, Smalltalk. Unlike many other OOP languages, Ruby and Smalltalk generally enforce data-hiding so that the variables inside an object cannot be accessed from outside unless you use specific methods to do so. But Ruby has exceptions to this rule. There are several ways in which you can bypass its data-hiding - and modifying the behaviour of existing classes ‘from the outside’ is perhaps the most egregious of these.
This, in fact, bypasses the most fundamental principle of object oriented programming: namely that the implementation details of an object are private to itself. The world of the program beyond the confines of that object should, therefore, never be reliant upon the implementation details of that object. The original author of the class defining the object should be confident that he can modify the code inside the class without his modifications having any effect on anyone else’s code. There should, in short, never be an occasion when a small change to a class in Rails, for example, should cause someone else’s code to cease to work - just so long as the Rails classes maintain the same interfaces (methods) through which other programmers can communicate with them.
By providing extreme self-modifying capabilities, Ruby necessarily gives people the ability to break encapsulation in the most profound way possible - by messing around with the internals of the fundamental classes. On the one hand, my instinct tells me this is bad and should not be allowed. On the other hand, I recognise that there are occasions when this is a very powerful capability and the Ruby language would be much diminished without it.
The only real way in which we can have the best of both worlds is, I suppose, by imposing some kind of self discipline. So-called monkeypatching should be regarded as a last resort - reserved only for very, very special circumstances. On the whole, the privacy of a class should be respected. If you need new behaviour you should define a new class as a descendant of an existing class.
But who is going to enforce this rule? In practice, nobody. If something can be done, it will be done - even though, in the long term, this may have a highly undesirable impact on program maintainability. In a commercial environment, where maintainability may be regarded as a primary goal, anything which has such as negative impact may be deliberately avoided as an enforced policy. But many of the programs written for Ruby have no commercial imperative. They are written as free contributions by people who are quite likely to have moved on to unrelated projects long before any maintainability problems come to the surface.
What the solution to this problem is I cannot say. That it is a problem, however, is, in my view, undeniable.
Huw Collingbourne is Technology Director at SapphireSteel Software, makers of the Ruby In Steel IDE for Ruby in Visual Studio.