Extreme Advice Considered Harmful
From APIDesign
Contents |
Have You Ever Wondered...?
I've just finished reading the theoretical and practical parts of your book, are you saying me that I have to apply all your advices to be successful? No, not at all. One of the the worst things to do is to follow advice completely, to the extreme. The chapter 13 builds on the Considered harmful phrase and expands it meaning into the world of API Design.
There are useful advices that can help with proper API design. However, as TheAPIBook mentions in chapter 13, sometimes following an advice to the last letter is not desirable. One of the advices that can often cause more harm than be useful, is the attempt to make an API fully correct. Well, nothing about correctness, correct version of an API is better than incorrect one, in case all other features of those two versions are on par. But often, in the search for correctness one sacrifices simplicity.
Complex but Correct
An example of this can be found in Java's java.io.File. Have you ever tried to read a content of a file in Java? Painful, isn't it? Instead of having a single method to load file's content into a String, one needs to allocate an array, write a loop to populate it and only than convert it into so desired String. I wrote that code many times, especially as a utility method in a unit test, so I at least know how to do it. However imagine a newcomer, a refugee from Perl, trying to rewrite a single line of such I/O Perl code to Java. How can such trivial task be so complex!? Why we cannot have simple methods like:
File f = new File("config.txt"); String text = f.asText(); byte[] arr = f.asBytes(); for (String line : f.asLines()) { ... }
I explain this unnecessary complexity as a result of an extreme search for correctness. Plus a little bit of Copy Based Design. Reading done in the Java way almost completely mimics the C style. E.g. malloc a bit of memory, read data into it, process them, read additional data, etc. This C way is known to be correct, never exhaust available memory and be robust even for use in operating systems. As such the designers of the Java I/O libraries just copied this correct and well-working principle.
Prehistoric I/O
Nobody of its designers considered it too complicated, as they were familiar with it from their C days. In fact it is not that complex compared to other C constructs. Just try to concatenate two C strings and you'll find out how verbose that code is. The C libraries leave the memory manipulation up to the programmer. However Java is different, concatenating strings is a matter of single +. Memory is allocated and freed automatically as needed. API users usually do not need to care. And they like this kind of simplicity. As such the Java I/O API for reading from a file seems like a relict of stone age. So complex and hard to use (but correct).
Fix it Yourself!
For a while I was advocating the Java I/O library to be enhanced with simple reading utility methods. However that takes time. We are not going to have such methods sooner then JDK 7 is out. But few weeks ago I realized that we do not need to wait! The NetBeans project has own virtual filesystem library and we can add the methods there! Today I started an API review to provide simple I/O reading API (see issue 157362) in the FileObject class.
Users of NetBeans 7.0 APIs, say farewell to painful reading of files! And please accept my apology for using Copy Based Design and realizing only recently that our users could benefit from our own simple asText(), asBytes(), and asLines() utility methods.
<comments/>
Mercurial vs. Subversion
One side note in the chapter 13 compares behaviour of Mercurial and Subversion. A curious reader sent a comment:
- Page 241: In the scenario described Subversion behaves the same way as Mercurial,
- so it would not be possible to checkin changes without first synchronizing against
- the HEAD revision (assuming checking goes to HEAD). See, for instance,
- Subversion FAQ, question
- I'm trying to commit, but Subversion says my working copy is out of date?, answer 3.
Looks like sometimes the svn up is needed. However not in the regular workflow. I've just tried:
# create a new file as user A: svn co file:///tmp/svn/r/ echo Hi. >readme.txt svn add readme.txt svn ci -m "adding readme.txt by user A" # create another new file as user B: svn co file:///tmp/svn/r/ echo >b.txt svn add b.txt svn ci -m "Adding file b.txt from user B" # now, as user A change the readme file, without updating # e.g. without knowing that there is the b.txt file: echo Hello >readme.txt svn diff svn ci -m "Changing content of file readme.txt as A, without updating to B's latest version"
The significant difference between Subversion and Mercurial is really there. Subversion lets the user A integrate changes without knowing what the final state after the integration will be. Seen from rationalistic point of view, this is not correct at all.
Mercurial would refuse such integration asking the user A to svn update to latest version of the repository first. This is the correct behaviour. However it is not really clueless.
Enormous majority of developers I know is complaining about such Mercurial restrictions. This is another example of correct but complex behaviour. Such correctness is reached at the expense of usability. Nice illustration of what can happen when one strives to make an API correct while ignoring other aspects of Good API.
Implicit Merges
As parren wrote the cause why Mercurial work flow is more complex lies in the concept of explicit merges.
In case of parallel integrations Mercurial would record the two changes as separate heads (implicit branches). And it would not allow pushing both to a central repo without a prior merge (by default).
What typically bothers people is they need to merge the two heads to combine their effects. Which is, again, correct, but may seem to clutter history. So you can rebase, if you prefer, explicitly emulating what Subversion implicitly does.