?

Log in

No account? Create an account
 
 
28 March 2006 @ 10:22 am
API Design  

It takes talent to come up with a great idea for a reusable component, or a library, but it takes a particular creativity to design the API to make it usable. A good API is built around a metaphor, or set of metaphors which represent the universe in which your library operates. Good APIs:

  • Make code readable
  • Reduce bugs
  • Reduce testing time

The objects in the API are best represented as nouns and the methods as attributes of the metaphorical object (for properties) and verbs, verb phrases or verb–noun pairs (for actions) which an reasonably be applied to those nouns. For example, from the standard Java API:

List
add(Object)
clear()
contains(Object)
remove(Object)
LayoutManager
addLayoutComponent(String, Component)
removeLayoutComponent(Component)
preferredLayoutSize(Container)
layoutContainer(Container)

These are great examples of sensible metaphor and naming. Just from the name, you can tell that a List represents a list, and a LayoutManager is some kind of automated agent for managing the layout of something. You can also see that layout manager is able to add a named component to itself, and layout a given container. As an aside, note how LayoutManager’s methods are not abbreviated. How much more readable is:

addLayoutComponent

than

addCmp()

? Clue: the correct answer is “a lot”.

A colleague recently pointed me at the latest and greatest innovation in Java XML parsing, VTD-XML. On the face of it, it seems a great idea: don’t build a million objects to hold your XML structure in memory, just keep a flat list of offsets of tokens within the original document, with depths. And for good measure, don’t even create objects for your pointers—use a C-like bit field to keep it all as primitive types. Great! The trouble is that this kind of old-school pack’n’squish mentality which kept us 8-bit programmers semi-sane back in the 1980s has also been applied to the user’s API for the library.

Witness the example in the JavaWorld article (reproduced below):

VTDNav vn = vg.getNav();
            if (vn.matchElement("purchaseOrder")){
                System.out.println(" orderDate==>" 
                    + vn.toString(vn.getAttrVal("orderDate")));
                if (vn.toElement(VTDNav.FIRST_CHILD,"item")){
                    if (vn.toElement(VTDNav.FIRST_CHILD)){
                        do {
                            System.out.print( vn.toString(vn.getCurrentIndex()));
                                System.out.print("==>");

                            System.out.println( vn.toString(vn.getText()));
                        } while(vn.toElement(VTDNav.NEXT_SIBLING));
                    }
                }
            }

Look at the use of abbreviations: “VTDNav” and “getAttrVal”. Makes it less readable. Our (English-speaking) eyes are attuned to reading English, so let’s give them English. How about a “VTDNavigator” and “getAttributeValue”? Those precious few characters are not going to matter in Java bytecode. But the biggest crime here is toElement().

Any sane person would expect toElement(), as a method on an object, to take the object and turn it into an “Element”, whatever one of those is. But this VTDNav, which should be VTDNavigator, is a navigator—something to help you find your way. It doesn’t make sense to turn it into an Element, a component of an XML document. Here’s what the methods of VTDNav are really doing (paraphrasing the JavaDocs):

matchElement(String)
Test if the current element matches the name supplied. The navigator starts at the root of the document.
toElement(int, String)
Navigate through the document in the direction specified by the integer constant, provided the element reached matches the name supplied.
toElement(int)
Navigate through the document in the direction specified.

Take a look at the DOM version of the example in the JavaWorld article:

Element root = d.getDocumentElement();
            if (root.getNodeName().compareTo("purchaseOrder")==0){
                System.out.println(" orderDate==> "
                    + root.getAttribute("orderDate"));

                Node n = root.getFirstChild();
                if (n != null){
                    do {
                        if (n.getNodeType() == Node.ELEMENT_NODE
                            && n.getNodeName().compareTo("item")==0){
                            Node n2 = n.getFirstChild();
                            if (n2!=null){
                                do {
                                    if (n2.getNodeType()
                                        == Node.ELEMENT_NODE){    
                                        System.out.println( 
                                            n2.getNodeName() 
                                            + "==>" +
                                            n2.getFirstChild().getNodeValue()
                                        );
                                    }
                                }while((n2=n2.getNextSibling())!=null);
                            } 
                        }
                    }while ((n=n.getNextSibling()) != null ); 
                } 
            } 

It’s longer, sure, but any fool can see what it is doing (root.getNodeName().compareTo("purchaseOrder") versus vn.matchElement("purchaseOrder") and n.getNodeType() == Node.ELEMENT_NODE && n.getNodeName().compareTo("item")==0 versus vn.toElement(VTDNav.FIRST_CHILD,"item")). That compareTo() == 0 would be better as equals though!

Let’s try reworking the VTD API, without changing the semantics at all, to make more sense of the example:

VTDNavigator navigator = vg.getNavigator();
            if (navigator.currentElementMatches("purchaseOrder")){
                System.out.print(" orderDate==>");
                System.out.println(navigator.valueOf(navigator.getAttributeIndex("orderDate")));
                if (navigator.moveToFirstChild("item")){
                    if (navigator.moveToFirstChild()){
                        do {
                            System.out.print(navigator.valueOf(navigator.getCurrentElementIndex()));
                            System.out.print("==>");
                            System.out.println(navigator.valueOf(navigator.getTextIndex()));
                        } while(navigator.moveToNextSibling());
                    }
                }
            }

Better? I think so.