Improve Access to Externalized Text
Apply rigorous controls not present in String-based APIs to access externalized text for more efficient code and easier maintenance
by Charles L. Owen

October 18, 2005

Software applications are usually full of text that is meant to inform and guide users. Error messages, widget labels, and help dialogs are a few common examples. The decision to separate text from source code is often simple; doing so enables localization and eases the effort of keeping text crisp, consistent, and correct. However, without proper controls on how text is accessed it may be difficult to guarantee that source code is correct, particularly in large applications where loose APIs readily lead to chaos.

Consider a specific example. Suppose we are building a Web application. Our design problem is to aggregate validation errors during request processing, and then send a complete collection of helpful messages to the user in his or her preferred language. This seems simple enough. We will specify an Errors object with an addError() method. Every anomaly found in the request data results in a call to addError() with information characterizing the problem. The Errors instance will be sent off for display if any issues are found. We have to decide how to handle localization, but so far so good. Let's examine a few of the many possible input parameters of addError().

Our first thought might be to provide the message itself as input to addError(). Sensing that this might be a bit too loose, a rule or pattern might be agreed upon that governs the use of the API. In this case, we use a String key to get the localized message before calling addError(). I call this idiom Control By Agreement, and it should be avoided in all but the smallest applications:

// method in Errors class
public void addError(
  String message) {…}

// client code 
if (name==null || 
  name.length()==0) {
    bdl = ResourceBundle.
      getBundle(…);
    msg = bdl.getString(
      NAME_REQD); 
    errors.addError(msg);
}

Because this wide-open version of addError() accepts any String as input, there is no control over what messages can be sent. In particular, the compiler cannot prevent delivery of hard-coded messages. While marginally helpful, the Control By Agreement idiom can be enforced only by code inspection. Moreover, there is no guarantee that the specified key represents a valid message. If the source base is large it may be unnecessarily complicated to make such guarantees.

Getting There
Forcing the client to supply the key rather than the message itself can solve some of these problems. Thankfully, Control By Agreement is no longer necessary, but ambiguities remain:

// method in Errors class
public void addError(
  String key) {…}

// client code
if (name==null || 
  name.length()==0) {
    errors.addError(NAME_REQD); 
}

This approach is an improvement: the client code does not rely on a weak idiom, and only messages associated with keys can be displayed. The details of finding the specific localized message are left to the Errors implementation. Still there is no automatic check that the specified key refers to a message. The compiler, for example, cannot distinguish misspellings.

Specifying a String argument, no matter what it signifies, leads to lack of rigor. To prove conclusively that every call to addError() results in a valid message delivered to the user, while perhaps possible, requires too much creativity. Specifying a String argument also requires explanation. Developers just beginning to use the API will have to read documentation to know if the argument should represent an error message or a key or some other thing. A better, stronger design would make such documentation unnecessary.

A substantial solution involves more specific types. The approach presented here will take advantage of existing APIs when possible, and will behave much like a typesafe constant:

// method in Errors class
public void addError(Message msg) {…}

// client code
if (name==null || name.length()==0) {
    errors.addError(
      Message.NAME_REQD);
}

Valid instances of Message must be used; the compiler will balk otherwise. Careful design of the Message class can eliminate any remaining doubt that the specified error message exists. (Issuing the wrong message is another problem, one that we'll leave for another day.) What's the best way to parameterize each Message instance with localized text?

The text messages in our hypothetical application will be externalized in properties files named according to the conventions specified by the ResourceBundle API. ResourceBundle will also be used to initially retrieve the text. In addition, we will require that names of Message instances be identical to the keys specified in the locale-specific properties files. Our design will enforce this simple, but obviously quite useful constraint.

Examine first the source for Message (see Listing 1). It looks like an ordinary typesafe constant with a twist. The constructor is private; the only instances of Message are the static final members. The class knows which base bundle name to use, and this is made explicit with a static final constant. Message extends Externalized; the base class handles the heavier work of retrieving text from its external location and parameterizing its subclasses. After creating each Message instance the static method init, inherited from Externalized, is invoked. This is where the interesting work is done.

The implementation of Externalized is also straightforward (see Listing 2). Each instance has a map associating a Locale with a String. Access to localized text is provided through the toString() method. Importantly, if one of the static Externalized fields (like one of our static final Message fields, for example) has a name that is not associated with a message, an exception is thrown while the class is being initialized. Successfully loading and initializing Message is enough to prove that every declared Message instance is in fact associated with some text and should be part of a system's unit tests, and could be done at system startup. Contrast this with the problem noted earlier: if messages are found in the conventional manner by using a String key, then it may be very complicated to attain a similar level of confidence in the correctness of the system.

Accessing text in the way described here is significantly more efficient than relying on ResourceBundle alone. I’ve included a simple test to quantify this increased efficiency (see Listing 3). On my system, I got these results:

init:
build:
build.tests:
perf:
  [java] Beginning test...
  [java] Iterations: 3276700
  [java] Typesafe: 471
  [java] Standard: 16634
Total time: 20 seconds

This approach provides rigorous controls not present in String-based APIs. The compiler provides most of the control by guaranteeing that APIs designed to deliver specific types of externalized text will do exactly that: if the user is supposed to see an error message, a widget label, or any other sort of externalized text, then application developers must use a Message or a WidgetLabel or some other specific type. Hard-coded text mixed with source code becomes impossible. Using this approach also automates the task of verifying that every attempt to access externalized text will meet with success, which can be very helpful in large applications. Another advantage is that all of the guesswork and reliance on documentation that often characterizes looser APIs become unnecessary. The result will be better code and easier maintenance.

About the Author
Charles L. Owen is with Quest Diagnositcs. Contact Charles at .