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'.

6 Comments

Agree on most except that synonyms are a bad idea. User interfaces almost always have several synonymous ways of doing one thing. For example, in Mac OS X there's at least three ways to make text bold (this is of the top of my head as unfortunately I'm working on a Windows machine at work, but I think you get the point): You select the text and then you can either use the menu to make it bold, control/right click it and select bold, or press Command-B to make it bold. The power-user would use keyboard shortcuts, someone with a right mouse button would use the context menu and my grandma would use the menu. Very good user interface design!

I agree that taking this to it's extreme could be bad but come on! We're just talking about "size" versus "length" here. The whole point of that alias in Ruby is that first impressions last. The first time you use Ruby and think "oh, crap, whatever is it in this bloody language, size or length?", you try one and it just works! This gives you that warm and fuzzy feeling to keep on learning. Ruby (and in extension Rails) has that indescribable feeling where you just try something and it works the first time. I don't need to include something, I don't need to mess around, it just works. Humane interfaces for sure.

If I saw only one of "size" or "length", I'd assume that the method referred to the number of elements in the array. If I saw _both_ size and length, I'd find it reasonable to assume that one referred to the number of elements in the array, the other to its capacity. But I wouldn't know which.

Similarly, while "collect" and "map" seem to do the same thing, maybe they're subtly different, to reflect differences of practice between the Smalltalk (collect) and Lisp (map) traditions?

Yes, GUIs often provide different ways to do things - but those ways are different modes of operation: a menu item, a keyboard shortcut, a context menu or a toolbar button. They don't get in the way of each other because you automatically pick the one based on what "mode" your brain and hands are when you need the functionality.

On the other hand, imagine if iTunes had _two_ buttons, equally prominent, with different icons both of which suggest "play". Your acquisition time for either of those buttons would be significantly less than if there had been only one or the other. Even if you were consciously aware they did the same thing, you'd still have to waste the cognitive effort to decide between them.

Humans deal with user interfaces in different ways depending on their familiarity with the environment. Initially, we navigate through an environment using recognition. Then as we get used to the environment we transition to recall.

Using java programming as the example, we initially work from javadocs, recognising that java.util.List is probably what we want to store stuff, and then recognising that the add is the method we probably want to add stuff to said list. And we confirm our suspicions by reading the javadoc details. But as we get more comfortable in our environment, we rely less on recognition, and go list.add(foo) from memory. That is, we utilise recall.

The problem with minimal interfaces is that they help during the recognition phase, by reducing the cognitive overload of dealing with information overload. But it hurts during recall based usage because it is overly exacting for our fuzzy memories to cope with. So we use code completion instead, dropping us back to recognition based usage.

The problem being is that recognition is slower than recall. By several orders of magnitude.

So ruby's humane interface is about being more accepting of human fuzzy memory to allow programmers to operate faster, especially multi lingual programmers like myself. I am current in at least four languages at any point in time, and do you think i can remember which one this language wants me to use to find the size of a list?

Anyway, i'll stop ranting now...

The ruby Array has length and size for reasonably good reasons. All ruby's collections (and a few other things come to that) have a size method. Arrays and Strings also have length methods because 'length' is meaningful in that context. So if you're writing a consumer method that works with an arbitrary collection, you use 'size'. If you're expecting an ordered collection of some type, you should consider using 'length' because it expresses your intent better.

Of course, this argument is undermined somewhat by the existence of Hash#length, but you can't have everything.

This issue has a big effect on readability. I have used many APIs where the "simplicity" adversely affected readability. Take the Java String class as an example. Which better expresses intent:

string.indexOf("searchString") > 0

or

string.contains("searchString")

The second version is much more readable, as it does not require intimate knowledge of the String api. Some might argue that a "contains" method is redundant, as the same functionality can be obtained by calling indexOf. However, to be usable an API sometimes requires these seemingly redundant methods.

The Java String class is one example of an admirable attempt to maintain simplicity that went too far. Interestingly, Sun have eventually added missing functionality anyway; adding 'split' in 1.4 and I notice Java 1.5 now has a 'contains' method.

Another factor worth considering is that you can always add missing methods to an API in a later release, but you can't remove redundant methods without breaking existing code. So perhaps the designers of the String class erred on the side of caution, figuring they could add methods later if required.

As in most things, it requires the right balance between both ends of the spectrum. "As simple as possible but no simpler".

Design. It's hard!

Actually, a bug in my previous reply just reinforced my point. It should be

string.indexOf("searchString")>-1;

or perhaps

string.indexOf("searchString")>=0

not

string.indexOf("searchString")>0;

This bug would not have occured if I could just write

string.contains("searchString")

This is a central advantage of Test Driven Development: you tend to avoid bad API design like this example.

Previously: Good Java Developer vs Excellent Perl Hacker?

Next: Atlassian Christmas Party