24 July 2006

OSCON06: 7 Principles of Better API Design

Damian Conway gave a good tutorial on 7 Principles of Better API Design at OSCON 2006. One of the things that struck me during the tutorial is how much the principles parallel human interface design; which really shouldn't have been too surprising since it's people who use an API. The overall message was keep it simple -- easy to use and easy to understand.

The first principle was 'Do One Thing Really Well'. His example of this was the Perl6::Say module with the 'say' function that automatically appends a new line to the item printed. I had gotten into the habit of declaring 'our($el)="\n"' in a module and using 'print .....,$el;'. Obviously 'say' is a much better way of handling this. Another example was Perl6::Slurp to read in the entire contents of a file. Some bads ways to do this are '$text=join("", <$fh>);' and worse this '$text.=$_ for (<$fh>)'. This can all be done better with '$text=slurp $f" where $f is either a file handle or the path to a file. A much more elegant solution.

The second principle is 'Design by Coding' -- he is not implying extreme programming here. A more descriptive title might be 'Design by Sample Re-coding'. Write some code that would use the modules and write the interfaces as you would like them. Now, iterate this process with the third principle, "Evolve by Subtraction". You will almost always find that your initial interfaces to the API are too clunky. Prune the interface down to its core purpose. The example he used was Contextual::Return. Context detection code in Perl is quite inelegant. You usually have something like the following:


if (wantarray())
{ return @list; }
elsif (defined wantarray ())
{ return $scalar; }
elsif (!defined wantarray ())
{ warn "why are you calling me"; return; }
else
{ die "impossible, but expect the unexpected"; }


An interface that produces more maintainable and readable code would be:

use Contextual::Return;

if (LIST) { return @list; }
if (SCALAR) { return $scalar; }
if (VOID) { warn "why are you calling me"; return; }
else { die "impossible, but expect the unexpected"; }

He went even farther and moved it all into a single return statement like:

use Contextual::Return;

return (
LIST { return @list; }
SCALAR { return $scalar; }
VOID { warn "why are you calling me"; return; }
DEFAULT { die "impossible, but expect the unexpected"; }
);

Next he explained in some detail how he implemented this using Want.pm and 'package DB' (as a proxy) and it went completely over my head, i.e., he lost me. So it goes.

Another good way to 'Evolve By Subtraction' is to let the users (really testers) help. The module IO::Prompt evolved this way. Users will quickly tell you what they don't like about an interface. If they report about having to repeatedly do something whenever they use the it -- like turning on an option -- this is a strong signal that the option should be on by default.

The fourth principle is 'Declarative Beats Imperative', by this he means enable the user to set options that describe what is to be done, don't make them do it. For example, the Exporter module, used to set how routines in a package are exported requires a lot of coding from the developer. Usually the code looks something like:

use base 'Exporter';
sub import { .... }
our @EXPORT = qw ( routine1 );
our @EXPORT_OK = qw ( routine2 );
our @EXPORT_TAGS = qw (
ALL => [qw (routine1 routine2)],
SET => [qw (routine2)],
DEFAULT => [qw (routine1)] );

Wouldn't it be nicer to just mark the subroutines in the package that are to be exported like this:

use Perl6::Export::Attrs;

sub IMPORT { my $pkg = shift @_; .... }
sub routine1 :Export ( :DEFAULT :ALL ) { .... }
sub routine2 :Export ( :ALL :SET ) { ... }

Now it is easy to see how, if at all, a routine is exported, without having to check any of the EXPORT lists. The module Perl6::Export::Attrs enables just this feature.

The fifth principle is 'Preserve the Metadata', or "Preserve the Users Metadata". Basically don't make the user re-enter data. His example for this principle was Config::Std module for reading and writing simple configuration files. All the previous configuration file modules would read the options in the file into a hash and write the file back out in the hash order of the keys, losing the structure of the file and any comments in the file. Not very nice. Config::Std keeps the comments by caching a copy of the file and unity this copy and any values modified, added, or deleted in the hash before writing the modified copy back to a file.

The sixth principle is 'Build on the Familiar'. For example, consider Log::StdLog. Instead of using an object oriented interface for logging why not just create STDLOG. Most users are already familiar with STDIN, STDOUT, and STDOUT. So STDLOG is just file handle for logging. He did not that this package is not thread safe -- yet.

The seventh principle is 'The Best Code is No Code At All', that is, just by including 'use PackageName;' you start using the API without having to write explicit API calls. 'use strict;' and 'use diagnostics;' are good examples of this. Another is 'Object::Dumper' package. Ever forget and print an object like:

my $obj = myClass->new ();
print $obj;

Unless you have overloaded the stringification function you get something like 'myClass=HASH[0x98392819]'. Wouldn't it be nice if it just automatically did 'print Data::Dumper->dumper($obj);' for you. Well Object::Dumper does just that.

He concluded with some good advice on developing an API -- follow 'The Way of the Camel". Have the API do the hard work for you without getting in your way. Really find the best defaults for the subroutines and work hard to keep the options small. Make the subroutines do one thing really well, maybe even better, overload a built-in function or operator. The API interface should be intuitive to use, if you can't remember how to use it then you need to improve it.

0 comments: