Tuesday, May 29, 2007

Are Open Classes Evil?

When doing my JRuby talk at No Fluff, Just Stuff, one of the consistent questions I get is the morality question about open classes: are they evil? Open classes in dynamic languages allow you to crack open a class and add your own methods to it. In Groovy, it's done with either a Category or the Expando Meta-class (which I think is a great name). JRuby allows you to do this to Java classes as well. For example, you can add methods to the ArrayList class thusly:
require "java"
include_class "java.util.ArrayList"
list = ArrayList.new
%w(Red Green Blue).each { |color| list.add(color) }

# Add "first" method to proxy of Java ArrayList class.
class ArrayList
def first
size == 0 ? nil : get(0)
puts "first item is #{list.first}"

Here, I just crack open the ArrayList class and add a first method (which probably should have been there anyway, no?). When you define a class, Ruby checks its classpath to see if another class of the same name has already been loaded. If it has, it adds the new behavior to the class.

It it's too frightening to add a method to the entire class, Ruby gives you the option of adding it to an object instance instead. Consider this:
# Add "last" method only to the list object ... a singleton method.
def list.last
size == 0 ? nil : get(size - 1)
puts "last item is #{list.last}"

Here, I add the last method just to the instance of the list class (i.e., this list object). That way, you don't add it to the entire class.

Many Java and C# developers are shuddering in horror right now. The consensus seems to be that this is just too dangerous. And, like all advanced language features, it can be abused. But here is a counter argument. How many Java and C# developers have a StringUtils class in their project? Pretty much everyone. Why? Because the language designers for both languages no only won't allow you to add your own methods to String, they won't even allow you to subclass it to create your own String class. Thus, you are forced by the language design to switch from object-oriented coding to procedural coding, passing Strings around like you are writing in C again.

Open classes allow you to make your code much better, when used responsibly. One common argument against this feature from paranoid developers is that they don't trust "junior" developers with this kind of power. So, tell the junior developers on the project not to do it! And, if you really hate the additions that have been made, you can also reopen a class and remove methods at runtime, using either remove_method (which removes it from a class) or undef_method (which removes it from the entire inheritance tree).

You can add new methods to classes in Java if you want to use Aspects, but:
  • uses an entirely different syntax from Java

  • you give up tool support in your IDE for the new methods (in fact, it probably won't even compile it properly without some plugins and such)

  • it's so much trouble that you just suffer through the ugly, non-OOP StringUtils class

In JRuby, you can add methods to existing classes:
  • using the natural syntax of the language

  • it is supported as much as anything else (we don't have IntelliJ for Ruby...yet), but nothing chokes either

  • it is so easy that it is natural

I think this level of power makes developers used to non-dynamic languages queasy. But in the right hands, it can make you code much more expressive, and not pollute your namespaces with lots of made up class names with Util and Helper tagged on the end. Like all advanced features, when used correctly, it makes your code much better. Are open classes evil? No more evil than any advanced language feature.


pdcawley said...

Open classes may be evil, but they are a lesser evil than structural code.

tug said...

The problem, as I see it, with Open Classes is that they can lead to unintended linkage between isolated code modules. A very ciommon use of Groovy (and, I would imagine, JRuby) is the use as a scripting language fore some large Java program. In this case you get arbitrary chunks of Groovy/JRuby run at arbirary times (potentially in parallel). None of these chunks know or care about each other. If one of these chunks of code adds a method to java.lang.Object, for example, then all of the other chunks immediatly see the new method. Also when the chunk which adds the method terminates the method is still there on java.lang.Object.

Groovy's category mechanism does not suffer from this problem as the method is added only for the current thread and only for the duration of the closure.

Changing the behaviour of an instance is, I have decided, pure evil:) I think you really should be able to produce a clone of the object with different behaviour.

i.e. a = b.become(newBehaviour)

Aaron said...

In C# 3.0, you use Extension methods to achieve the same effect of adding functionality to existing classes (String.MakeCamelCase()? ... why not?). And yes, there are people who think this is a bad thing - me not being one of them.

Unknown said...

The more powerful a language feature is, more is the probability of it being misused by the average programmer. The point is, should we evangelize a language with enough safety net like Java targeting the average developer ? For that mass, open classes are evil - the feature can be ripped apart in big projects with 80% less than average quality developers.

Prasanna L.M said...

Hi Neal, I am from Bangalore (India). I attended your session Polyglot programming in the recently held JAX india 2007 conference. Your session was really good. probably i can say one of the best session in the conference. I read few artciles in your blog and liked a lot.. Looking forward for more technical articles from your side..

Andreas Ronge said...

But you can not use the modified opened java classes in java, only from ruby. (E.g. you override one method in ruby in a java class - that method will not be used from java.)
Please correct me if I'm wrong.
It would be great to have open classes in java from a test perspective.

Neal Ford said...

You are correct. In both JRuby and Groovy, the open classes cannot be consumed from Java. It's really a shame, to, because creating a mocking library in Ruby/JRuby/Groovy is trvial because pf open classes.