|
Wow. Elliotte's criticism to the so called "Humane
Interface" API design "school of thought" has spawn an Internet wide
discussion (meme?) about
simplicity in API design.
So I decided to review yesterday's entry, and go take a look at the
Internet, to try to see what makes an API a good API. At least for me.
And this is what I've found (and, as always, all feedback is welcome).
Surprise, surprise...
Yesterday I talked about the importance of following idioms and language
conventions. Today a principle backs up this idea. The so called
Principle of
minimum surprise (or "Principle of least astonishment") states it clearly:
In user interface design, programming language design, and ergonomics, the
principle (or rule) of least astonishment (or surprise) states that, when two
elements of an interface conflict or are ambiguous, the behaviour should be
that which will least surprise the human user or programmer at the time the
conflict arises, because the least surprising behavior will usually be the
correct one.
Principle of
minimum surprise
And the fact is that Ruby's array API is
all but "least astonishing" to a Java programmer. I couldn't stop laughing
while reading Elliotte's
review of Ruby's array api!!. (And, well, all respect due to Ruby programmers,
but please understand that Elliotte has a point here, and he's so funny I couldn't
resist).
Another example is C++ operator overloading. If you wrongly overload the
assignation operator then your implementation fails to follow the principle of
minimum surprise, and you can end up with a class that makes memory leaks. That's
one of the reasons why I wouldn't like to see operator overloading in Java! ;-)
So, to summarize, point one is that an API should follow the minimum
surprise principle.
Minimal interfaces: what is reasonable?
Martin Fowler introduces the
Minimal Interface style of API design. He defines "minimal" as the smallest reasonable
set of methods that will do the job.
And I think that's a nice definition. "smallest" and "reasonable" balance each
other quite well and give the definition the part of ambiguity it needs.
Take, for instance, lists. The list
abstract data type requires just one constructor and four operations. Just
that and you can do whatever operation you want with lists (including "first()" and
"last()"). So the "smallest set of methods that will do the job" for a list is
four (and a constructor).
Well, of course a list interface with just 4+1 methods would result spartan to
everybody. That's where "reasonable" fits in nicely in the definition (making the
definition subjective and, thus, unusable, by the way ;-) ).
So, what is reasonable? Can we improve Martin's definition a little bit? What
are the forces, the non functional requirements a Minimal Interface must meet
so as to be a "Minimal Interface" with a smallest set of "reasonable" operations?
Reasonable means "as usual"
This is, a reasonable interface (to me) is one that follows the minimum
surprise principle. For a Java list a reasonable interface (to me) is one
that uses "size()" to get the number of elements, and that uses indexes
starting from 0. And that uses Iterators to iterate over elements of the list.
That's the principle of minimum surprise applied to (Java) lists. At least for me.
But a reasonable interface is one that keeps backwards compatibility.
What would happen if Sun decided to remove the "Enumeration" interface in Java
API? Wouldn't that break lots of existing programs out there?
Reasonable means easy to implement and extend
I wouldn't like to extend a list interface with 78 methods. That's nightmare.
As Martin states it a minimal interface "reduces the burden on implementers".
(Well, at least for languages such as Java, where non abstract classes implementing
an interface have to implement *all* methods of the interface).
Reasonable means easy to learn
And again Martin introduces the importance of the learning curve in
API design. Note, as well, that the more an API follows the Minimum
Surprise Principle the easier the API is to learn. I don't need to go
take a look at the java.util.Map API to know that I can retrieve the size
of the map by invoking "size()" on it. (and yes, I admit Java API has
evolved a lot and we still suffer from things such as length(),
getLength() and size() burden in Java APIs).
Reasonable means easy to test
That's another important point, of course. APIs are to be maintained, and
the smaller the public API the less things to test, right?
Reasonable means encapsulated, modular, providing information hiding
Which are basic principles to follow, aren't they? As Elliotte points out,
is a Ruby array ...
... Is it a List? Is it a stack? Is it a map? It's three, three, three
data structures in one!
And, please, note that I wouldn't like to make any critics to
Ruby people, but I think the example servers quite well as a
counter-example of the encapsulation and modularity. Wouldn't it
be better to have separate Map and Stack classes? Is that reasonable?
I think so. So, for me, a reasonable minimum interface is one
that follows the principles of encapsulation, modularity and
information hiding. The ones that make an array an array, and not a map,
a ternary search trie or the mother of all data structures ;-)
Reasonable means ... minimal!!
APIs don't usually change. Changing a public API is asking for trouble
to all people using it (remember those deprecated classes we have in APIs?).
It's probably easier to augment a public API than to reduce it.
Adding some new methods to a public API is much easier than removing
methods people is already using. And that's why a reasonable API is a
small one. The smaller the less prone to be reduced in the future, right?
To summarize
So, to summarize, I like minimal interfaces too. "Reasonable" minimal
interfaces. They're easier to use, easier to learn and easier to
maintain. Minimal interfaces can be augmented without
too much hassle. But minimal interfaces are probably harder to
design. You cannot measure the qualities of an API
until you release it out. And then it may be too late to change it to make
it even better. All we can probably do is use some good old
common sense.
Sorry for this long post. And happy API desigining,
Antonio
|
Enviado por Jeffrey Olson en diciembre 08, 2005 a las 09:24 PM CET #
For instance, many classes support iteration methods like .collect, .each, .map, ... These aren't contained in each class, but are instead inserted via a mixin. This mixin only requires that the class it's inserted into supports (from memory) methods like get() and size().
What these methods do is to use the minimal interface because that's all they need... and they don't add anything to the object layout, they simply call the objects get and size methods.
Think what this means...
If you want the same thing in Java, for instance for the new foreach-loop. The for loop will happily loop over every object that implements the Iterable interface. The problem: the person that wrote the class needs to have thought of that, and then needs to implement a way to actually iterate (either return the Iterator from a contained class or write an Iterator).
Sure... you could write a utility method like Collections.makeIterable(List) ... but then you add up with a huge Collections class ... ... wait... aren't big classes a code smell?
Another language that supports mixins is Sun's very own Fortress (I think they're called Traits there).
Enviado por murphee en diciembre 08, 2005 a las 11:57 PM CET #
Enviado por Ramnivas Laddad en diciembre 09, 2005 a las 10:47 PM CET #
"array.at(index) -> obj or nil
As near as I can figure, this is exactly the same as using array brackets except it uses a method call. If there is a difference, it's pretty subtle. DRY, anyone?"
The Ruby API says:
Array#at is slightly faster than Array#[], as it does not accept ranges and so on.
What's so 'subtle' about that? For me, it's pretty clear what the difference is.
Enviado por Vamsee en diciembre 17, 2005 a las 01:53 AM CET #