How to use the Java API design checklist

This before-and-after example illustrates how to use the Java API design checklist. We borrowed the original API from the free online eBook “OSGi in Practice” by Neil Bartlett. The author uses the API through the book to illustrate various OSGi features and considers it a “reasonable attempt” at defining an API.

Here is the description of the API strait from the book:

“Thanks to the internet, we are today bombarded by messages of many kinds. Email is an obvious example, but also there are the blogs that we subscribe to through RSS or ATOM feeds; SMS text messages; “microblogging” sites such as Twitter[?] or Jaiku[?]; IM systems such as AOL, MSN or IRC; and perhaps for professionals in certain fields, Reuters or Bloomberg newswire feeds and market updates. Sadly we still need to flip between several different applications to view and respond to these messages. Also there is no coherent way to apply rules and automated processing to all of our inbound messages. For example, I would like a way to apply my spam filters, which do a reasonable job for my email, to my SMS text messages, as spam is increasingly a problem in that medium.”

“To support multiple kinds of message sources, we need to build an abstraction over messages and mailboxes, so a good place to start is to think about what those abstractions should look like. In Java we would represent them as interfaces. Listing 1 contains a reasonable attempt to define a message in the most general way.”

“Objects implementing this interface are really just message headers. The body of the message could be of any type: text, image, video, etc. We need the header object to tell us what type the body data is, and how to access it.”

“Next we need a way to retrieve messages. The interface for a mailbox could look like Listing 2. We need a unique identifier to refer to each message, so we assume that an ID of type long can be generated or assigned by the mailbox implementation. We also assume that the mailbox maintains some temporal ordering of messages, and is capable of telling us about all the new messages available given the ID of the most recent message known about by the reader tool. In this way the tool can notify us only of new messages, rather than ones that we have already read.”

“Many back-end message sources, such as the IMAP protocol for retrieving email, support storing the read/unread state of a message on the server, allowing that state to be synchronized across multiple clients. So, our reader tool needs to notify the mailbox when a message has been read. In other protocols where the back-end message source does not support the read/unread status, this notification can be simply ignored.”

“Finally we need the code for the exceptions that might be thrown. These are shown in Listing 3 and Listing 4.”

Design review

The API design shown below is fairly typical for a first draft. All major functional requirements are met, but there are some remaining design issues. We will do a design review using the Java API design checklist to remember overlooked design requirements, spot mistakes, identify less-than-optimal design choices and opportunities for improvements. We marked the identified issues with “//see …” comments in the listings below. The hyperlinks point to the relevant checklist items in the list.

Listing 1: The original Message (see after)

1   package org.osgi.book.reader.api;  //see 1.2.7, 1.3.1

3   import java.io.InputStream;

5   public interface Message{ //see 2.1.8, 2.2.18, 2.4.1, 2.7.1

7   /**
8   * @return The unique (within this message’s mailbox) message ID. //see 3.9.3
9   */
10  public long getId();

12  /**
13  * @return A human-readable text summary of the message. In some
14  * messaging systems this would map to the "subject" field. //see 3.9.3
15  */
16  public String getSummary();

18  /**
19  * @return The Internet MIME type of the message content. //see 3.9.3
20  */
21  public String getMIMEType(); //see 2.2.3

23  /**
24  * Access the content of the message.
25  *
26  * @throws MessageReaderException
27  */
28  public InputStream getContent() throws MessageReaderException; //see 2.1.3, 3.2.3, 3.4.2

30  }

Listing 2: The original Mailbox (see after)

1   package org.osgi.book.reader.api;

3   public interface Mailbox{ //see 2.1.8, 2.4.1, 2.7.1

5   public static final String NAME_PROPERTY = "mailboxName";

7   /**
8   * Retrieve all messages available in the mailbox.
9   *
10  * @return An array of message IDs.
11  * @throws MailboxException
12  */
13  public long[] getAllMessages() throws MailboxException; //see 3.1.3, 3.2.3, 3.3.8, 3.4.2

15  /**
16  * Retrieve all messages received after the specified message.
17  *
18  * @param id The message ID.
19  * @return An array of message IDs.
20  * @throws MailboxException
21  */
22  public long[] getMessagesSince(long id) throws MailboxException; //see 3.1.3, 3.2.3, 3.3.1, 3.3.8, 3.4.2

24  /**
25  * Mark the specified messages as read/unread on the back-end
26  * messagesource, where supported,e.g.IMAP supports this
27  * feature.
28  *
29  * @param read Whether the specified messages have been read.
30  * @param ids An array of messageIDs.
31  * @throwsMailboxException
32  */
33  public void markRead(boolean read, long[] ids) throws MailboxException; //see 3.1.14, 3.3.25

35  /**
36  * Retrieve the specified messages.
37  *
38  * @param ids The IDs of the messages to be retrieved.
39  * @return Anarray of Messages.
40  * @throws MailboxException
41  */
42  public Message[] getMessages(long[] ids) throws MailboxException; //see 3.1.3, 3.2.3, 3.3.1, 3.3.8, 3.4.2

44  }

Listing 3: The original MessageReaderException (see after)

1   package org.osgi.book.reader.api;

3   public class MessageReaderException extends Exception{ //see 2.1.3, 2.6.2

5   private static final long serialVersionUID = 1L; //see 2.3.2

7   public MessageReaderException(String message) {
8      super(message);
9   }

11  public MessageReaderException(Throwable cause){
12     super(cause);
13  }

15  public MessageReaderException(String message,Throwable cause){
16     super(message,cause);
17  }

19  }

Listing 4: The original MailboxException (see after)

1   package org.osgi.book.reader.api;

3   public class MailboxException extends Exception{ //see 3.4.2, 2.7.1

5   private static final long serialVersionUID = 1L; //see 2.3.2

7   public MailboxException(String message) {
8      super(message);
9   }

11  public MailboxException(Throwable cause){
12     super(cause);
13  }

15  public MailboxException(String message,Throwable cause){
16     super(message,cause);
17  }

19  }

Redesign

Our design review highlighted several omissions, issues, and inconsistencies. Listings 5 6, 7, 8 and 9 show the redesigned API. During the redesign we had many tradeoffs to consider. In addition to the checklist items identified during the design review we considered several others. We show these additional checklist items within square brackets inside comments and using the //also … comments in code. The hyperlinks point to the relevant checklist items in the list.

Listing 5: package overview (package-info.java) for the redesigned API

1   /**
2   * <p>
3   * Provides simple and generic read-only access to messages from a variety
4   * of different sources like email, RSS and Atompub feeds, instant messaging
5   * services, Facebook, SMS and Twitter.</p> [1.3.3]
6   * <p>
7   * All concrete classes implement either {@link org.osgi.book.reader.MessageHeader}
8   * or {@link org.osgi.book.reader.Mailbox}. Together, these two abstract classes define the
9   * generic interface used in the package. All concrete classes are protocol-specific
10  * implementations and extensions like {@link org.osgi.book.reader.EmailHeader} or
11  * {@link org.osgi.book.reader.ImapMailbox}</p> [1.3.5]
12  * <p>
13  * Classes from this package are not intended for direct instantiation.
14  * They have no public constructors. Instead, pre-configured instances of mailboxes
15  * must be retrieved by their name through a JNDI lookup from the naming context
16  * "com/env/mailboxes"</p>
17  * <p>
18  * Classes from this package are not intended for extension. All concrete
19  * classes are final and abstract classes have no public or protected constructors.</p>
20  * <p>
21  * The code sample bellow shows how to read and print out messages
22  * from all configured mailboxes: </p> [1.3.6]
23  *
24  * <pre>
25  *     import org.osgi.book.reader.*;
26  *     import javax.naming.*;
27  *     import java.rmi.RemoteException;
28  *
29  *     try {
30  *         Context initialContext = new InitialContext();
31  *         NamingEnumeration mailboxNames = initialContext.list("com/env/mailboxes");
32  *         while(mailboxNames.hasMore())
33  *         {
34  *             NameClassPair pair = (NameClassPair) mailboxNames.next();
35  *             Mailbox<?> mailbox = (Mailbox<?>) initialContext.lookup(pair.getName());
36  *             for(MessageHeader h : mailbox.readAllMessageHeaders()) {
37  *                  System.out.println(h);
38  *             }
39  *         }
40  *     } catch (NamingException e) {
41  *         e.printStackTrace();
42  *     } catch (RemoteException e) {
43  *         e.printStackTrace();
44  *     }
45  * </pre>
46  *
47  * @version 2.0 [1.3.10]
48  *
49  * <br/>
50  * <p>This sample API is an adaptation of the original published in
51  * <a href="http://njbartlett.name/osgibook.html">"OSGi in Practice"</a> by Neil Bartlett
52  *
53  * <br/>
54  * <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/">
55  * <img alt="Creative Commons License" style="border-width:0"
56  * src="http://i.creativecommons.org/l/by-sa/3.0/88x31.png" /></a>
57  *
58  * <br/>
59  * Sample API by Neil Bartlett and Ferenc Mihaly is licensed under a
60  * <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/">
61  * Creative Commons Attribution-ShareAlike 3.0 Unported License</a>. [1.3.12]
62  */
63
64  package org.osgi.book.reader;

Listing 6: Message(Header) after redesign (see before)

1   package org.osgi.book.reader;  

3   import java.io.InputStream;
3a  import java.io.Serializable;
3b  import java.rmi.RemoteException;

4a  /**
4b  * Contains descriptive (structured) information about a message and
4c  * defines methods for retrieving the (unstructured) message body. [2.7.3]
4d  * Read-only (immutable) instances of this abstract type are
4e  * read from a Mailbox implementation. [2.7.5]
4f  * Concrete derived types may offer additional information or functionality, like {@link EmailHeader}. [2.3.4]
4g  * The natural ordering is the order of reception.
4h  * For a code sample, see {@link Mailbox}.. [2.7.6, 2.7.9]
4i  */
5   public abstract class MessageHeader implements Comparable, Serializable { //also 2.2.4, 2.2.8, 2.2.10, 2.3.11, 2.3.12, 2.3.17

6a  MessageHeader() {} //also 2.3.9, 2.3.21

6b  @Override
6c  public boolean equals(Object obj) {...}
6d  @Override
6e  public int hashCode() {...}
6f  @Override
6g  public String toString() {...}
6h  @Override
6i  public int compareTo(Object o) {...} //also 2.3.10, 2.3.11

7   /**
7a  * Returns the unique (within this message’s mailbox) message ID.
8   * @return The message ID.
9   */
10  public long getId() {...}

12  /**
12a * Returns a human-readable text summary of the message.
13  * @return A human-readable text summary of the message. In some
14  * messaging systems this would map to the "subject" field. Not null.
15  */
16  public String getSummary() {...}

18  /**
18a * Returns the Internet MIME type of the message content.
19  * @return The Internet MIME type of the message content. Not null.
20  */
21  public String getMimeType() {...}

23  /**
24  * Access the content of the message from the remote mailbox. [item 3.9.14]
25  *
25a * @return An input stream for reading the message content. Not null.
26  * @throws RemoteException In case of a communication error with a remote mailbox
26  * @throws MailboxException Unrecoverable internal mailbox error
27  */
28  public abstract InputStream streamContent() throws RemoteException, MailboxException;

30  }

Listing 7: Mailbox after redesign (see before)

1   package org.osgi.book.reader;
1a
1b  import java.rmi.RemoteException;
1c  import java.util.Collection;
1c  import java.util.SortedSet;

2a  /**
2b  * Represents a generic and abstract mailbox interface for accessing incoming messages. [2.7.3]
2c  * Several implementations based on various messaging protocols (POP3, IMAP, RSS, etc.) are available.
2d  * Configured instances of mailboxes are retrieved by performing a JNDI lookup. [2.7.5]
2e  * The code sample below prints out the summary of all messages from the default mailbox. [2.7.6]
2f  * <pre>
2g  *     Context context = new InitialContext();
2h  *     Mailbox<?> mb = (Mailbox<?>) context.lookup("com/env/mailbox/default");
2i  *     for(MessageHeader h : mb.readAllMessageHeaders()) {
2j  *         System.out.println(h); //uses toString()
2k  *     }
2l  * </pre>
2m  */
3   public abstract class Mailbox<T extends MessageHeader> implements Comparable {//also 2.1.10, 2.3.11

5   public static final String NAME_PROPERTY = "mailboxName";

6a  Mailbox() {...} //also 2.3.9, 2.3.21

6b  @Override
6c  public boolean equals(Object obj) {...}
6d  @Override
6e  public int hashCode() {...}
6f  @Override
6g  public String toString() {...}
6h  @Override
6i  public int compareTo(Object o) {...} //also 2.3.10, 2.3.11

7   /**
8   * Retrieve all messages available in the mailbox.
9   *
10  * @return The ordered set of all available message headers. Not null.
10a * @throws RemoteException In case of a communication error with a remote mailbox
11  * @throws MailboxException Unrecoverable internal mailbox error
12  */
13  public abstract SortedSet<T> readAllMessageHeaders()
13a                              throws RemoteException, MailboxException; //also 3.1.3, 3.1.9, 3.3.9

15  /**
16  * Retrieve all messages received after the specified message.
17  *
18  * @param last The last read message header; not null.
19  * @return The ordered set of message headers since the lst read message header. Not null.
19a * @throws NullPointerException If parameter is null
19b * @throws RemoteException In case of a communication error with a remote mailbox
20  * @throws MailboxException Unrecoverable internal mailbox error
21  */
22  public abstract SortedSet<T> readMessageHeadersSince(T last)
22a     throws NullPointerException, RemoteException, MailboxException; //also 3.1.3, 3.1.9, 3.3.9, 3.4.6, 3.4.12

24  /**
25  * Mark the specified messages as read on the back-end
26  * messagesource, where supported,e.g.IMAP supports this
27  * feature.
28  *
29  * @param headers The list of message headers to be marked; not null.
29a * @throws NullPointerException If parameter is null
29b * @throws RemoteException In case of a communication error with a remote mailbox
31  * @throws MailboxException Unrecoverable internal mailbox error
32  */
33  public abstract void markRead(Collection<T> headers)
33a     throws NullPointerException, RemoteException, MailboxException; //also 3.1.3, 3.1.9, 3.1.10, 3.3.9, 3.4.6, 3.4.12
33b public abstract void markRead(T header)
33c     throws NullPointerException, RemoteException, MailboxException; //also 3.1.3

34a /**
34b * Mark the specified messages as unread on the back-end
34c * messagesource, where supported,e.g.IMAP supports this
34d * feature.
34e *
34f * @param headers The list of message headers to be marked
29g * @throws NullPointerException If parameter is null
34h * @throws RemoteException In case of a communication error with a remote mailbox
34i * @throws MailboxException Unrecoverable internal mailbox error
34j */
34k public abstract void markUnread(Collection<T> headers)
34l     throws NullPointerException, RemoteException, MailboxException; //also 3.1.3, 3.1.9, 3.3.9, 3.1.10, 3.4.6, 3.4.12
34m public abstract void markUnread(T header)
34n     throws NullPointerException, RemoteException, MailboxException; //also 3.1.3

Listing 8: MessageReaderException after redesign (see before)

(no longer needed)

1   package org.osgi.book.reader;

3   public class MessageReaderException extends Exception{

5   private static final long serialVersionUID = 1L;

7   public MessageReaderException(String message) {
8      super(message);
9   }

11  public MessageReaderException(Throwable cause){
12     super(cause);
13  }

15  public MessageReaderException(String message,Throwable cause){
16     super(message,cause);
17  }

19  }

Listing 9: MailboxException after redesign (see before)

1   package org.osgi.book.reader;

2a  /**
2b  * Signals an unrecoverable internal mailbox error.
2c  */
3   public class MailboxException extends RuntimeException{

5    

7   public MailboxException(String message) {
8      super(message);
9   }

11  public MailboxException(Throwable cause){
12    super(cause);
13  }

15  public MailboxException(String message,Throwable cause){
16     super(message,cause);
17  }

18a /* IMPLEMENTATION STUFF */
18b private static final long serialVersionUID = 1L;
18c
19  }

Discussion

Is the redesigned version better than the original? This is a tricky question, a bit like asking which car is better or which city is more pleasant to live in. Your answer will depend on what aspects you consider important. There is no doubt that we managed to improve many aspects of the API. The new version is noticeably more consistent, safer to use, and better documented. Listing 10 shows the code we wrote to aggregate massages from several mailboxes before and after the redesign. Regardless of your stance on the arrays versus collections debate (some developers dislike generic Java collections, finding them too verbose), you’ll probably agree that the second version is safer to use.

Listing 10: Code for aggregating messages before and after the redesign

1  /**
2  * Before
3  */
4  public static Message[]
5  getAllMessages(Mailbox[] mailboxes) throws MailboxException {
6  	Message[] result = new Message[0];
7  	for (Mailbox mailbox : mailboxes) {
8  		Message[] messages = mailbox.getMessages(mailbox.getAllMessages());
9  		result = Arrays.copyOf(result, result.length + messages.length);
10 		System.arraycopy(messages, 0, result, result.length, messages.length);
11 	}
12 	return result;
13 }

1  /**
2  * After
3  */
4  public static SortedSet<MessageHeader>
5  readAllMessageHeaders(Set<Mailbox<?>> mailboxes) throws RemoteException {
6  	SortedSet<MessageHeader> result = new TreeSet<MessageHeader>();
7  	for(Mailbox<?> mailbox : mailboxes) {
8  		result.addAll(mailbox.readAllMessageHeaders());
9  	}
10 	return result;
11 }

This being said, some of our design choices are not the best. For example, we choose JNDI lookup over public constructors to illustrate the importance of proper documentation, not because we don’t recommend constructors. We used generic abstract classes to illustrate how to improve API safety with strong compile time type checks, intentionally disregarding that some developers may be uncomfortable with Java generics.

This brings us to an important point: the API design checklist is just one of the tools we use for API design and checklist items should not be applied mechanically, without thinking. This is especially true for checklist items introduced with the words Favor, Consider, and Avoid. Considering the perspective of the caller and focusing on improving the main use cases should help decide what design trade-offs to make.

Creative Commons Licence
This work is licensed under a Creative Commons Attribution-ShareAlike 2.5 Canada License.

Advertisement