Humane Interfaces

December 9, 2005 12:56 AM

A bit of a war of words has broken out over Martin Fowler's post about Humane Interface Design, in which he proposes that the interface of a class be designed to maximise its usefulness, rather than to minimise its complexity. As his example, he contrasts the Ruby Array class with its Java equivalent: java.util.List.

(Fowler has been linking to the various sides of the ensuing debate at the bottom of his post, which saves me having to do it here.)

I'm not going to come down on either side of the debate, because I haven't really formed a clear opinion either way. Instead, I'll just throw a little more kerosene on the fire. :)

1. An interface isn't an Interface

In Java, List is an Interface. In Ruby, Array is a class. The distinction may seem to be hair-splitting, but it's important.

A Java interface defines a type, a series of messages to which any object wishing to be called a List must respond correctly, but it can provide no implementation for those methods. As such for every method that List defines, anyone who wants to provide their own List implementation must implement that method. Sure, there's an AbstractList that can take some of the weight off you, but extending AbstractList locks you into a particular line of superclass inheritance, which might not be helpful.

Java utility classes like <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/Collections.html">java.util.Collections</a> exist just so Java can provide static implementations of functions that can be applied to all implementors of an interface, without any mucking about with inheritance.

Ruby's Array doesn't have this problem. Partly this is because Ruby doesn't have the equivalent of a Java Interface, and partly this is because Ruby allows for mixins as an alternative to multiple inheritance. 21 of Array's methods are, in fact, mixed-in functionality from the Enumerable module.

Java's design affords small interfaces, and utility functions provided as static methods on helper clases. Ruby's design affords larger classes with mixed-in utility methods.

2. Array is a really bad example

Part of the reason this argument could go on forever is that Ruby's Array is both an example of arguments for Humane design, and arguments against it. Nobody could really dispute the usefulness of last(), join(), sort() or map(). Similarly, Ruby's convention of having two methods for many operations -- foo to return a new object, and foo! performing the same operation but modifying the existing object in place -- is a useful one.

On the other hand, many of Array's methods are harder to defend. Methods like rassoc() fetch() or pack() bear the strong smell of being Perl or Lisp refugees that don't really belong.

3. List is a really bad example

java.util.List isn't really a shining example of good interface design either.

Take the fact that List defines an add() method. Implementing add() is, according to the Javadoc, optional. This completely defeats the purpose of having an Interface in the first place. Instead of being able to rely on the object's type to determine its capabilities, the only way to find out if you can, in fact, add something to a list is to try to add something and hope it doesn't throw an (unchecked) UnsupportedOperationException.

(All mutators on the List interface -- 9 of List's 25 methods -- are optional in this way)

Under some circumstances, for example the custom List implementation returned from java.util.Arrays.asList(), you may or may not be able to call add() on the resulting list, depending on whether your operation will overflow the list's backing array - something else you can't ask the List interface about beforehand.

The penalty for adding something to this kind of list at the wrong time isn't even an UnsupportedOperationException, it's an ArrayIndexOutOfBoundsException -- presumably because there's no such thing as a PartiallySupportedOperationException.

The fact that this sort of thing doesn't trip most Java programmers up more than once or twice a year is a convincing argument in favour of dynamic typing. Programmers just make sure they know which kind of List is being passed around where, without the assistance of extra type information.

4. Synonyms are a really bad idea

Fowler:

When you want the length of a list, should you use length or size? Some libraries use one, some the other, Ruby's Array has both, marked as aliases so that either name calls the same code. The rubyist view being that it's easier for the library to have both than to ask the users of a library to remember which one it is.

This is where I have to disagree vehemently.

Having two otherwise equivalent ways to perform the same operation is bad user-interface design, and it's bad library interface design, because the existence of the synonyms actually adds to your cognitive load by making you choose between them.

Say you're scanning a class, trying to work out what method to call. You find the class has two methods with synonymous names. Do they do exactly the same thing, or are they subtly different? Well, now you have to go to the documentation to find out (or the code, if the documentation doesn't explicitly say "these methods are 100% identical"). If there was just the one method, you'd probably have just used it without a second thought.

5. Ruby could be both humane and minimal

In Ruby, libraries can add methods to existing classes. As such, a lot of the less core methods on Array could be harmlessly moved into the standard library, to be re-applied if needed by include 'pack', include 'synonyms' or include 'obscure-lisp-stuff'.

Previously: Good Java Developer vs Excellent Perl Hacker?

Next: Atlassian Christmas Party