BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Ruby's Open Classes - Or: How Not To Patch Like A Monkey

Ruby's Open Classes - Or: How Not To Patch Like A Monkey

This item in japanese

Bookmarks

Rails developers who watched the recent Ruby 1.8.7 preview releases, soon noticed something about the 1.8.7 Preview 1: it broke Rails. The reason was the addition of a method Symbol#to_proc, which was backported from Ruby 1.9. Adding this method allows to write certain code in a more compact way (see details about Symbol#to_proc).

So what happened? Rails had already added the to_proc method to Symbol. However... the method Ruby 1.8.7 Preview 1 added had a slightly different behavior than the one Rails added.
Fortunately, Rails has quite a few users, so the problem was quickly reported, and the final version of Ruby 1.8.7 has a version of Symbol#to_proc that works.

The Problem

Ruby's Open Classes are  a useful feature that allows to add methods to an already loaded class, as simple as
class String
 def foo
 "foo"
 end
end
puts "".foo # prints "foo"

The problem of Open Classes is quite easy to see, we can use an old and undisputed principle of software design: Modularity. Over the years, an enormous amount of concepts has been developed to gain modularity in light of growing code bases. From local variables (vs. global variables), lexical scoping (vs. dynamic scoping), numerous namespace systems, etc. This is a process that is still ongoing - just consider the ongoing idea of doing "component oriented" development and the idea that software should be composable in the same way as physical components are assembled into products. Modularity, as we see, is an important property of software.

So it is this property that speaks out against Open Classes and freewheeling Monkeypatching (as this feature is also known, particularly in the Python community). Any library developer who opens an existing class must answer this: is this added method really so absolutely necessary in this class that I must break modularity. Let's reiterate the issues we get:
  • Potential name clashes and interaction with 3rd party libraries
    A client program usually doesn't just rely on a single library - any additional library increases the probability that another library also adds something to an already modified class and causes a name clash to happen. Even if that doesn't seem likely for some reason - opening very basic classes of the Ruby standard library definitely poses a problem. Some solutions require opening Object - in that case, every subclass of it has the added method(s). This is a bigger problem than clashing with another monkeypatching library. Why? Because every class in the system is derived from Object, thus the added method is now in the name space of every class. So... unless the added method has a name that includes, say, a SHA-1 hash value, there's a chance it'll clash with another method.
  • Compatibility with future Ruby versions and stdlibs
    A recent and high profile example is the Symbol#to_proc method - libraries added this to allow for a special, terse syntax for certain operations. In Ruby 1.9, this was added to the standard Ruby stdlib's Symbol class. This shows another source for name clashes: if a name is general enough, a future Ruby version might include the method too. While it might be fine if the method does the exact same thing - it's a problem if it doesn't. In that case, redefining the method can break the system, i.e. the Ruby stdlib and all it's clients that rely on the behavior of the standard Ruby library.

How to avoid Open Classes by design

One reason to open a class is to make objects of a class support a certain protocol or interface, i.e. a set of messages/methods (read a longer explanation of the term protocol in the context). Here are alternative solutions to achieve this.

Adapters

The Adapter pattern basically allows - given some object X - to look up another object which supports a certain protocol which can act on behalf of object X.

An example of widespread application of the Adapter pattern can be found in Eclipse, where it helps to keep the platform extensible and modular. An example of the use of Adapters: getting an Outline GUI for an Editor:
OutlinePage p = editor.getAdapter(OutlinePage.class); 
The class of the editor object can either directly return an OutlinePage object that knows how to display an outline for the editor's content. If this particular editor doesn't implement Outline functionality, the protocol of the getAdapter method suggests to forward the call to a central lookup system. Which brings us to the extensibility/modularity part: even if the creator of the vendor hasn't supplied an Outline GUI, another Eclipse plugin can provide one. Advantage of the Adapter pattern: no need to modify domain classes to add functionality  - the adapter logic contains all the logic to adapt from the desired interface to the original object. Allows orthogonal changes to the interface. No global changess necessary. For more information about Eclipse version of the Adapter pattern, read "What is IAdaptable?" by Alex Blewitt.

An example of the use of Adapters in a dynamic language comes from ZOPE. In his presentation "Using Grok to Walk Like a Duck", Brandon Craig Rhodes describes the experience of building ZOPE over the years, and explores the pros/cons of different approaches how to "make an object which is not a duck behave like a duck". The solution describes several ways of defining and providing Adapters.

These Adapter implementations might seem like overkill in small applications, they do allow to keep an application modular. There's a difference to Open Classes, because the returned Adapter is not necessarily the same object as the adapted one - with Open Classes (or Singleton classes - see next section) it's possible to add behavior to all objects of a particular type. Whether that's a crucial feature or not depends on your application.

Singleton Classes

Ruby allows to modify the class of one particular object. It does so by creating a new class, a Singleton class, from the object's original class. Here how to do this:
a = "Hello"
def a.foo
 "foo"
end
a.foo # returns "foo"
The effects of these changes are kept local to the object - no other classes or objects are affected. For more information and examples on when to use Singleton classes, see InfoQ's article "Using singleton classes for object metadata".

How to safely use Open Classes

If you really need to open a class, here some tips to reduce the risk. Jay Fields lists different ways of adding methods to classes. The solutions are
  • alias
  • alias_method_chain
  • Close on an unbound method
  • Extend a module that redefines the method and uses super
Read Jay's article to get the details for each of the solutions.

Finally, collect class extensions in one location, eg. putting them all in a file extensions.rb. By sticking to this convention, all extensions are instantly visible to anyone reading the code without requiring any special IDEs or class browsers that show where methods come from. It also acts as documentation of what classes are affected.

Safe approaches to Open Classes in Ruby and other languages

The idea of extending existing classes isn't unique to Ruby. Other languages have support for similar features, and some have found solutions that don't pollute the global namespace.

One concept is called Classboxes. Implementations are available for Squeak Smalltalk, but also Java or .NET. The basic idea:
Classical modules systems support well the modular development of applications but lack the ability to add or replace a method in a class that is not defined in that module. But languages that support method addition and replacement do not provide a modular view of applications, and their changes have a global impact. The result is a gap between module systems for object-oriented languages on one hand, and the very desirable feature of method addition and replacement on the other hand.

To solve these problems we present classboxes, a module system for object-oriented languages that allows method addition and replacement. Moreover, the changes made by a classbox are only visible to that classbox (or classboxes that import it), a feature we call local rebinding.

C#'s extension methods provide another way to approach the problem. Unlike Open Classes in Ruby, extension methods don't change the actual classes. Instead, they're only visible for the source that defines the extension methods - in short: it's really all implemented in the compiler. An example (from the linked article):
public static int WordCount(this String str) 
As you can see, the method gets the this pointer to the object it's working on. To make the extension visible:
using ExtensionMethods; 
And done - you can now use the new method:
string s = "Hello Extension Methods";
int i = s.WordCount();
The benefit of this approach: the extension method is only visible in code that explicitly imports it.

The debate about Monkeypatching/Open Classes has already caused experiments with workarounds. This workaround by coderr allows to wrap code in a context which keeps the extensions local. One issue with this solution is that it requires use of a Ruby native extension to hook into the Ruby interpreter (it uses RubyInline, look for inline method calls to find the C code that does the work).

A different approach is Reginald Braithwaite's Rewrite gem. It uses ParseTree to get the AST for Ruby code and uses this to make added methods visible in certain contexts. InfoQ discussed the Rewrite gem in more detail before. The Rewrite gem also relies on native extensions (in this case ParseTree) to work.

Conclusion

We saw the Open Class feature can cause problems when used carelessly - a fate it shares with for-loops, dynamic memory allocation and many more language features. Of course the emphasis is on carelessly. We saw what issues like name clashes really do occur in the real world. With this in mind, we looked at alternative solutions to modifying existing classes (using Adapters) - and if that's not an option, how to use Open Classes as safely as possible. Finally, less intrusive solutions to Open Classes have been considered for future versions of Ruby - for an idea of how they might look we looked at solutions in other languages (Classboxes, C# extension methods, etc).

Rate this Article

Adoption
Style

BT