|
|
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.
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.
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.
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.
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.
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.
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.
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.
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.
Have an opinion about the design principles presented in this article?
Discuss this article in the News & Ideas Forum topic,
Design Principles and XOM.
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
|
Sponsored Links
|