Air Bags and Other Design Principles

A Conversation with Elliotte Rusty Harold, Part VI

by Bill Venners
August 11, 2003

Summary
Elliotte Rusty Harold talks with Bill Venners about the API design principles that guided the design of the XOM (XML Object Model) API, including enforcement of invariants, information hiding for simplicity, and not using assertions for air bags.

Elliotte Rusty Harold is a prolific author of numerous books about Java and XML, and creator of the popular Java website Cafe au Lait and XML website Cafe con Leche. He contributed to the development of JDOM, a popular XML processing API for Java. His most recent book, Processing XML with Java, shows how to parse, manipulate, and generate XML from Java applications using several XML APIs, including SAX, DOM, and JDOM.

At a meeting of the New York XML SIG in September, 2002, Harold unveiled an XML processing API of his own design: the XOM (XML Object Model) API. On Cafe au Lait and Cafe con Leche, Harold described XOM like this:

Like DOM, JDOM, dom4j, and ElectricXML, XOM is a read/write API that represents XML documents as trees of nodes. Where XOM diverges from these models is that it strives for absolute correctness and maximum simplicity. XOM is based on more than two years' experience with JDOM development, as well as the last year's effort writing Processing XML with Java. While documenting the various APIs I found lots of things to like and not like about all the APIs, and XOM is my effort to synthesize the best features of the existing APIs while eliminating the worst.

In this interview, which is being published in multiple installments, Elliotte Rusty Harold discusses the strengths and weaknesses of the various XML processing APIs for Java, the design problems with existing APIs, and the design philosophy behind XOM.

Enforcing Class Invariants

Bill Venners: You listed several API design principles in your talk at the New York XML SIG. I'd like to walk through them and get your comments. You said, "It's the class's responsibility to enforce its class invariants."

Elliotte Rusty Harold: XML has rules about names, about where certain things can appear, about how constructs are structured. In an XML API, classes should make sure that XML's rules cannot be violated. If an Element class, for example, has a requirement that the element's name not contain the asterisk character, the class should not rely on the client programmer to feed in only names without asterisks. If the class sees the client programmer trying to do that, the class should throw an exception. I think that is a general design principle, but it is one that is not enforced in a lot of XML APIs. They rely on the client programmer knowing what is and is not legal XML, and only giving their classes legal XML.

Bill Venners: Another design principle you listed in your talks was, "Verify pre-conditions."

Elliotte Rusty Harold: Suppose that a String passed to a method you are writing must not be null, that the method fails if it is null. You should check to see if that String is null. If turns out to be null, even though it was required to be non-null, you should immediately throw an exception before doing anything else, before potentially corrupting the object.

Bill Venners: Bad data shouldn't make an object unusable.

Elliotte Rusty Harold: Any data that is so bad as to make an object unusable when passed to a method should not be accepted by that method.

Bill Venners: And after the exception is thrown, the object should be in the same state as it was before?

Elliotte Rusty Harold: Right. The object should not have changed. The badness of the passed-in data should be detected at the first opportunity.

Information Hiding for Simplicity

Bill Venners: Another design principle you listed was, "Hide as much of the implementation as possible." What is that about?

Elliotte Rusty Harold: That's just a good general principle of object-oriented programming. You want to expose a certain API and keep everything else private. You don't want your clients to see how you are doing certain things that they don't need to know.

If you expose too much, there are two costs. One is, it confuses the client programmers. It takes mental effort to learn and understand these things, and hey, they don't need to know. Smaller is better. Smaller is simpler. That's one of the big advantages of data encapsulation that's not often pushed in object-oriented textbooks, though I think it's one of the most important in practice.

The other advantage to implementation hiding is very well known: it allows you to vary the implementation. If later you decide that java.lang.String class isn't fast enough for your needs, you can change the implementation to use arrays of chars. You can get away with that kind of change without breaking client code.

Hiding implementation also means that clients can't sneak behind the public methods and break things by calling methods out of order than the implementation would normally call them, for example. But far and away the most important practical reason for encapsulation is it makes the classes simpler and easier to understand.

Designing for Subclassing

Bill Venners: You also mentioned in your talk, "Design for subclassing or prohibit it."

Elliotte Rusty Harold: That is taken directly from Joshua Bloch's book Effective Java. If you're going to allow subclasses, you must remember that subclasses typically need to have a greater access to the superclass members than the general public. Subclasses also need to access the superclass in different ways from the general public. So designing for subclassing takes extra effort. You really need to think about what a subclass is going to do, what it can do, what it should be able to do. It is much harder to get the design right for a subclass than just for a client object that's merely invoking the methods.

That being said, sometimes subclasses are in fact useful, so you do the work. If you don't do the work, if you don't really think about what subclasses do and don't need and how they'll interact with the superclass, you should make the class final to prevent it from being subclassed. Because often, subclasses sneak behind the normal barriers of access protection, and they can get at pieces of the class in ways you had not intended.

So one principle I follow in design is that all classes start their life as final. If at some point I discover a reasonable need to subclass, either my own or somebody else's, then I will remove the final modifier. I take this approach because once a class is published as non-final, it becomes very hard to make the class final later on.

Ensuring Well-Formed XML

Bill Venners: What XML principles did you adhere to in your XOM API design? You listed a few in your talk, for example, "All objects can be written as well-formed XML text." All objects?

Elliotte Rusty Harold: All Node objects— all objects that represent part of an XML document, so the Element object, the Attribute object, the Text object, etc.—can be written as well-formed XML.

Bill Venners: Is that not true of JDOM?

Elliotte Rusty Harold: No, it's not true of JDOM. It's not true of DOM. It's not true of most other XML processing APIs.

Bill Venners: How is it not true of JDOM?

Elliotte Rusty Harold: As I said earlier, you can use Strings in JDOM that contain control characters that cannot be serialized. JDOM doesn't make the checks it needs to guarantee well-formedness in several areas.

Bill Venners: I see. So because the API doesn't ensure well-formedness when data is passed to the Node objects, well-formed XML is not guaranteed to come out when those objects are serialized. In your talk you also said, "Validity can be enforced by subclasses." What did you mean by that?

Elliotte Rusty Harold: Going back to the principle, "Design for subclassing or prohibit it," in most of the XOM classes, I designed for subclassing. Let's say you're developing an XHTML package, a set of subclasses of the standard XOM classes. You have classes for each of the specific element types in XHTML, such as PElement, TableElement, and BodyElement, all of which extend Element. Each of these classes could add additional constraints to those usually enforced by XOM. So the BodyElement subclass could require that the element name be "body" and the namespace URI be the XHTML namespace.

Or, in an RSS package with a LinkElement class that extends XOM's Element class, you could verify that the text of every LinkElement is actually a URI. None of these additional checks are required by XML, but specific XML applications might have such further requirements. You can enforce those in subclasses. On the other hand, you can't remove checks. A subclass is not allowed to decide, for example, that it going to allow white space in Element names.

Designing the XOM classes for subclassing took a lot of effort. It would have been much easier in a language like Eiffel, that has real assertions that are inherited by subclasses.

Serialization with XML

Bill Venners: You said in your talk, "Classes do not implement Serializable, use XML." That would probably be the main question I would have in a XOM design review. By simply marking the classes Serializable, you give clients a choice to serialize via XML or Java object serialization without adding much clutter to the public API. Why did you choose not to?

Elliotte Rusty Harold: XML is a good serialization format. It's often smaller, more compact, and faster than Java's binary object serialization. If you have a Document or an Element object, and you want to blast it across the network to somebody else, XML is much more portable. It's much more efficient to send it as XML, as text on the wire, than it is to serialize this object into Java binary serialization format. The only case where you might perhaps want to use object serialization is if you're doing remote method invocation. But my response to that is, well this is XML. We probably ought to be doing SOAP or XML-RPC, or some REST-ful thing instead.

Using Custom Lists

Bill Venners: Another comment you made in your talk was, "Lack of generics really hurts the Collections API, hence don't use it." Explain your reasoning.

Elliotte Rusty Harold: Essentially, there's no way currently in Java to say, "This is not just a generic list. This is a list of Nodes, Elements, or Attributes" Anytime you put an object into a java.util.List, you lose some type information. That results in a lot of casting, a lot of instanceof checks, and it's just plain ugly. There's probably a little performance cost, but I don't care about that. I do care that it's ugly.

It's not that hard to implement your own lists, something we all learned about in Data Structures 201. It was still too hard for me to do, though. So internally in XOM, I used a java.util.List. I used the facade design pattern to provide type safe list operations in the public API. All the casting and instanceof checks and everything else that's necessary with java.util.Lists are done in the private parts of the classes.

Interestingly, this is the exact reverse of how JDOM does it. In its private parts, JDOM uses its own FilterList class that was written by the JDOM developers. FilterList is a very sophisticated list with a lot of power. It knows a lot of details about the specific JDOM objects. In the API JDOM exposes to the world, however, it might as well be any other java.util.List that contains objects. None of that power, knowledge, or sophistication is seen. Behind the scenes in XOM, I'm just using the standard java.util.List, but out front it looks a lot nicer.

Bill Venners: I think the tradeoff there is that one of the advantages of using the Java Collections API in your public interface is that everyone already knows what they are.

Elliotte Rusty Harold: Right, that's certainly an advantage, but I don't think the XOM lists are so challenging that anybody is going to have excessive trouble learning them. The NodeList interface, for example, has two methods, one to return the size and another to get the item at a particular index. It's a read-only list, not a read-write list. If you want to write Elements into the List, you use the Element's own insertChild or appendChild methods. You can't change the lists that are exposed to you.

Assertions Are Not For Air Bags

Bill Venners: You also mentioned in your talk that, "Assertions that can be turned off are pointless." Why is that?

Elliotte Rusty Harold: Imagine you're designing a car for General Motors. You put in the most wonderful air bags that have ever been seen. You've got airbags on the side, airbags in front, airbags in back. These airbags don't hurt small babies. They don't break anybody's neck. They only go off when they're supposed to, not when somebody's walking through the parking lot kicking all the cars' bumpers. You test the car, you run it into walls. You put crash test dummies in it. You film it. You drive the car around on test tracks. You drive it up and down mountains. You make nice TV commercials. These are great airbags that make riding in the car much safer. But just as you're getting ready to go into production to send the car out to consumers, you take out all the airbags. That's what Java 1.4 assertions are like.

Java's assertions are only intended for testing. They aren't intended as part of the real class's design. There's a runtime flag that says, ignore all the assertions. Does that make all the problems go away? No. If buggy data is passed in, it is still buggy, and it will still cause problems, but now the problems will be hidden. It is much better to find out about them sooner rather than later.

Bill Venners: I asked both Josh Bloch and James Gosling about the appropriate use for assertions. They helped me understand that I should continue to use exceptions where I used them before assertions came along. I shouldn't just start using assertions in places where I traditionally used exceptions, such as checking for preconditions at the beginning of a method. So I would say that explicit checks for preconditions that result in explicit thrown exceptions are like airbags in cars. You don't take those away at runtime, so you don't implement them with assertions.

Where I find that assertions make sense is when I have something that's kind of complex and I lack confidence. For example, perhaps at one point in a particular method I'm assuming something is true that really needs to be set up by other methods. And I think it's going to work, but I don't feel confident that even if it works now, it will continue to work as the class is changed over time. I would put an assertion in there for two reasons. One is that if the assertion did fail in testing an error message would pop up. It's almost like a little unit test that runs as the application is running. It is not something that is enforcing client constraints such as method preconditions. The other reason I think it's helpful to put an assertion in there is that the assertion actually communicates some intent to other programmers. Without the assertion statement, it might not be immediately obvious to other programmers that as I was programming along here, I was making the assumption that the asserted condition is true.

Elliotte Rusty Harold: That sounds reasonable. It's certainly not how I've heard people pushing assertions over the last year since Java 1.4 came out. If you look at the articles written about assertions at Sun's Java web site, JavaWorld, and elsewhere, what you see is people using them most commonly as a replacement for IllegalArgumentException and similar things. And that to me just seems insane.

Next Week

Come back Monday, August 25 for the next installment of this conversation with Elliotte Rusty Harold. If you'd like to receive a brief weekly email announcing new articles at Artima.com, please subscribe to the Artima Newsletter.

Resources

Elliotte Rusty Harold is author of Processing XML with Java: A Guide to SAX, DOM, JDOM, JAXP, and TrAX, which is available on Amazon.com at:
http://www.amazon.com/exec/obidos/ASIN/020161622X/

XOM, Elliotte Rusty Harold's XML Object Model API:
http://www.cafeconleche.org/XOM/

Cafe au Lait: Elliotte Rusty Harold's site of Java News and Resources:
http://www.cafeaulait.org/

Cafe con Leche: Elliotte Rusty Harold's site of XML News and Resources:
http://www.cafeconleche.org/

JDOM:
http://www.jdom.org/

DOM4J:
http://www.dom4j.org/

SAX, the Simple API for XML Processing:
http://www.saxproject.org/

DOM, the W3C's Document Object Model API:
http://www.w3.org/DOM/

ElectricXML:
http://www.themindelectric.com/exml/

Sparta:
http://sparta-xml.sourceforge.net/

Common API for XML Pull Parsing:
http://www.xmlpull.org/

NekoPull:
http://www.apache.org/~andyc/neko/doc/pull/

Xerces Native Interface (XNI):
http://xml.apache.org/xerces2-j/xni.html

TrAX (Tranformation API for XML):
http://xml.apache.org/xalan-j/trax.html

Jaxen (a Java XPath engine):
http://jaxen.org/

RELAX NG:
http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=relax-ng

Talk back!

Have an opinion? Readers have already posted 3 comments about this article. Why not add yours?

About the author

Bill Venners is president of Artima Software, Inc. and editor-in-chief of Artima.com. He is author of the book, Inside the Java Virtual Machine, a programmer-oriented survey of the Java platform's architecture and internals. His popular columns in JavaWorld magazine covered Java internals, object-oriented design, and Jini. Bill has been active in the Jini Community since its inception. He led the Jini Community's ServiceUI project that produced the ServiceUI API. The ServiceUI became the de facto standard way to associate user interfaces to Jini services, and was the first Jini community standard approved via the Jini Decision Process. Bill also serves as an elected member of the Jini Community's initial Technical Oversight Committee (TOC), and in this role helped to define the governance process for the community. He currently devotes most of his energy to building Artima.com into an ever more useful resource for developers.