Techniques and Patterns for Writing Once and Running Anywhere

Posted March 31, 1998 by Joshua Kerievsky

Despite the proliferation of drag-and-drop tools for developing Java software for the Web, developers continue to face problems with inconsistent cross-platform look-and-feel, download delays, problems manipulating images, AWT limitations, browser bugs and VM speed. This tutorial discusses how to solve these problems using a small Java engine that avoids use of the AWT, provides image manipulation instructions, implements bug-workarounds and manages thread differences among browsers. The engine was built using a variety of Design Patterns that provide speed, adaptability and interoperability. So as well as describing the engine, this talk will show how to apply Design Patterns to Java.

This paper was delivered in presentation form at Object Expo '98 in New York City.

The Promise of Java

Late in 1995, Sun introduces Java and describes it as:

A simple, object-oriented, distributed, interpreted, robust, secure, architecture-neutral, portable, high-performance, multithreaded, and dynamic language.

Memory management is replaced by garbage collection.

"Write Once, Run Anywhere!" is no longer a dream, and the Java Virtual Machine (JVM) promises to make nearly all platforms potentially "Java-enabled."

Netscape releases Navigator 2.0, furnished with a JVM, opening doors to a whole new way of doing Client/Server programming.

"Just In Time" compilers greatly improve Java's speed.

The Reality Today

All JVMs are not created equal. Nuances in JVM implementation make for unpredictable behavior.

"Write Once, Run Anywhere" is still a dream.

"Write Once, Debug Anywhere" is closer to the truth.

Netscape's and Microsoft's browsers sit on millions of desktops world-wide, containing different and buggy JVMs.

Java's Abstract Windowing Toolkit (AWT) is buggy and produces user interfaces that look and behave differently across platforms.

Major improvements like JavaSoft's Java Foundation Classes (JFC, a.k.a. Swing) are not widely available in browsers.

Bridging Promise with Reality

Sun's description of Java back in 1995 pointed toward an ideal.

Although that ideal is still not completely attainable today, there are ways to get closer to it.

Programming techniques and patterns, combined with building blocks already in Java, get us a lot closer to realizing Sun's ideal.

Today, you'll see commercial, Web-based Java applications and learn about the problems that emerged during design and development, along with the solutions to those problems.

Along the way, you'll learn about a Java Engine that was built to make it a little easier to "Write Once and Run Anywhere".

Overview and Objectives

Overview:

  • We are going to study and discuss four real-world business requirements, problems associated with meeting these requirements, solutions to these problems, and commercial examples of the suggested solutions.

Objectives:

  • Describe design alternatives that overcome weaknesses in Java and in developing Java for the Web.
  • Describe techniques and patterns to help developers write platform-independent Java code.

Images & Code

Requirement: Same Look & Feel Across Browsers

August 1996: Industrial Logic wins a contract to build the Java version of MTV.com.

Despite the gap between Sun's ideal and the reality of Java/Web programming, MTV wants its Java version to:

  • Run within Netscape Navigator 2.01+ on the PC.
  • Run within Internet Explorer 3.0+ on the PC.
  • Run within Netscape Navigator 3.0+ on the Mac.
  • Look and behave exactly the same on all supported browsers.
  • Download quickly for MTV.com's millions of users.

Problem: Java's AWT UIs

Java's Abstract Windowing Toolkit (AWT) UIs don't look the same across platforms or inside browsers.

AWT components are buggy and require a good deal of sub-classing to add the functionality you need.

Sub-classing, or using third-party controls, requires larger, time-intensive downloads, and many third-party controls aren't lightweight.

AWT font support is lacking: It supports only simple fonts, and fonts rarely look the same across platforms and browsers.

AWT layout managers don't produce great-looking UIs.

Drag-n-drop tools don't produce great UI code and suffer from the same AWT cross-platform bugs.

Solution: Images & Code

Use images and write code to remove ties to the AWT.

+ GIFs and JPEGs really and truly look the same across platforms or inside browsers.
+ Using images to represent components frees you up to create stunning, professional-looking UIs.
+ Using images, you can easily separate a UI's "look" from its "behavior."
+ You can use whatever fonts you like, since fonts will ultimately be image-based.
+ You no longer rely on AWT layout managers.
- You must write "behavior" code for your components.

Example: Image Maps

Because MTV wanted a very specific look for its UI, Industrial Logic used MTV's trademark GIFs and created a generic image-map component to control behavior.

Summary

Java's AWT limitations and bugs make it difficult to guarantee that your UI will look and behave the same across platforms and browsers.

GIFs and JPEGs always look the same across platforms and browsers.

Platform-independent UIs can be written by using images and writing your own user-interaction code.

You're not limited by AWT fonts: You can use whatever fonts you or your customer wants.

Buttons, radio/check boxes, image maps, tabbed-panels, and the like are relatively easy to write. Grids and drop-down list boxes require a little more work.


Use A Language

Requirement: Support Change

MTV and Industrial Logic hammered out a clear, functional specification, describing the exact behavior MTV wanted for the Java version of its site.

For example, if a visitor to MTV were to click on "MTV Sports," MTV wanted the clicked UI component to change color and a specificied HTML page to load inside an HTML frame.

MTV also wanted a rotating list of news tickers and promotional graphics, plus the ability to specify the contents and order of that rotating list.

Many more such requirements were part of the MTV functional specification.

Problem: Too Many Parameters

Given the requirements, Industrial Logic began thinking of simple ways to let MTV parameterize its site.

One way would be to let MTV's staff update applet parameter tags. But the list of these tags could easily exceed 100, creating an unwieldy, complex mess.

Next, we considered writing off-line tools, to manage and write out the applet parameter tags. But the resulting HTML page would be too large and slow to download.

A third approach involved writing out and reading, at runtime, a flat file containing parameter information. But then our "thin" Java client would have to be very intelligent about how to parameterize UI components.

Solution: Use A Language

Place parameters in a language that clients can easily interpret.

+ A language can be composed of sentences that contain parameters, along with additional elements that signify behavior.
+ A language can include information about how a program will actually run.
+ A language can be created from friendly GUIs, text editors, HTML pages, or whatever your users like.
+ A language can be read and written, tokenized and detokenized, compressed and decompressed.
+ A client program can interpret a language in a predefined and consistent way, keeping the client fairly "dumb," and thus "lightweight."

Example: MTV's Language

A sample of a language used on the MTV project:

Each sentence in this language is composed of:

  • A sentence ID.
  • A command name.
  • Information such as parameters and variable names.

Summary

Businesses want software to support change easily.

Parameterizing programs may support changes, but users may have difficulty managing highly parameterized systems.

Using a language simplifies programs that are highly parameterized.

Client programs can implement simple interpretation strategies to turn languages into useful runtime information.

Languages can be stored and read back in many ways.

Languages can include sophisticated behavioral information that may exceed what parameters convey.


Name and Execute Behavior

Requirement: Support New Behavior

Good software must support the addition of new behavior with a minimum of fuss.

The notion of Object Linking and Embedding is based on the idea of supporting new, unforeseen behavior.

After the first release of the Java version of MTV.com, MTV asked Industrial Logic to make its news tickers and promotions clickable. MTV wanted certain user clicks to trigger a number of UI events.

For future versions of its site, MTV expressed an interest in adding sophisticated animations, user-click tracking, sound effects, user recognition, legacy-system integration, and so on.

Problem: Add New Behavior

Given a language (and some client-side support), users can parameterize existing application behavior however they like.

But we still haven't provided a way for them to add new behavior and configure UI components to use it.

With a language, users can easily specify that an image map should load HTML page "X" rather than "Y," but they can't tell the image map to play a sound, show an animation, and then load an HTML page.

There's still no simple way to configure our system with new behavior.

Java's Class.forName() could help but still doesn't associate our UI components with particular classes.

Solution: Name & Execute Behavior

Name behavior and execute it using a standard protocol.

+ Create a simple Java interface containing a method called execute().
+ Make all your behavior classes implement this interface.
+ Assign names to your behavior classes: e.g., a class that loads images might be dubbed “LoadImage” or “LI”.
+ Let your language include named behavior
+ Let your behavior classes interpret language-based parameters.
+ Let one or many behavior classes be executed by UI elements.

Example: LoadCommand

Example usage of behavior class, "LoadCommand": GetImageMan^LoadCommand^ili_ImageMan^ImageMan

Sample implementation:

public class ili_LoadCommand extends Object implements ili_Command {
  ...
  public void execute(String id) {
    String _className = null;
    ...
    _className = _parms.nextElement().toStr();
    ili_Command _cmd = (ili_Command) (Class.forName(_className).newInstance());
    _command.init(_context);
    _context.getCmdStore().putCmd( _parms.nextElement().toStr(), _cmd);
    ...
  }
}

Summary

Good software must accommodate the addition of new functionality.

Adding new functionality should not necessarily involve changing a system's "guts".

By "naming" behavior, and using a standard protocol and language, users can make client programs execute requests, without letting the client program know what they are executing.

The levels of indirection in this solution allow client programs to remain "lightweight" while being capable of executing complex requests.

Behavior can easily be "batched" to support the execution of chained (or macro) behaviors.


Use A Microkernal

Requirement: Download & Start Intelligently

Example Usage: IBM's e-Commerce Advisor

IBM wanted to:

  • Clear the ugly grey HTML applet box ASAP.
  • Display a more attractive download screen ASAP.
  • Describe the e-Commerce Advisor during download.
  • Display a progress bar indicating how much of the applet and its data have downloaded.

Problem: Lengthy Downloads

In a network environment, if your users must wait a long time for a download, they will very likely get impatient.

Java does not come with any tools to help display progress meters while applets download.

Most browsers don't support multple archives (.zips, .jars, .cabs), so developers tend to download all of their .class files within one archive, making users wait and wonder.

Class files that are not used until some user action requires them are often downloaded anyway, regardless of whether they'll be needed.

Without any visual feedback, network bottlenecks appear to be a problem with your program.

Solution: Use A Microkernal

Download a minimal functional Microkernal first.

+ A Microkernal separates a minimal functional core from extended functionality.
+ Using a Microkernal, you can perform initial tasks both before and while your user waits
+ A Microkernal can serve to coordinate your language, your parameters and variables and your behavior classes
+ Mircokernals are typically the key element in adaptable systems.
- If you place your Microkernal code in an archive, it will download quickly, but you may then have to load classes individually (i.e. sloowly).

Example: e-Commerce Advisor

Industrial Logic's implementation of IBM's e-Commerce Advisor displays a descriptive splash screen first, and then makes the IBM logo gradually grow, indicating progress during the download.

Summary

Developing for any network environment requires sensitivity to network speed and a user's time.

Visual feedback goes a long way in helping ease the wait.

A Microkernal can be downloaded quickly to get some informative messages to a user.

You can leverage the great power of the Microkernal to "plug in" new behavior based on need.

The Microkernal can become the coordinator of your language, your parameters and variables, and your behavior classes.


Final Thoughts

Until Java matures (most notably in the major browsers), saying good-bye to the AWT is one of the first steps in beginning to write platform-independent Java code.

Working with behavior classes (Commands), languages, and a microkernal is an equally important step in making your Java programs behave the way you want them to.


Mini-Patterns

Objects, Interfaces, and K Size

Requirement: Very small download files/archives.

Problem: Because you've used so many Design Patterns, you have an explosion of classes and interfaces that make your download k size rather high.

Solution when bandwidth is an issue:

  • Make your objects courser grained.
  • Use only the most essential interfaces.
  • Download finer-grained objects when needed.

Example: On one project, Industrial Logic noticed that it had almost 200K in objects and interfaces. We had an extreemely flexible implementation, but it was too damn big! We later got the total size down to 92K.

Cut, Consolidate, Create, and Tile Images

Requirement: You need to download many images.

Problem: It takes forever to download images.

Solution:

  • Cut: Make your images as small as possible.
  • Consolidate: Place all your images in 1 or 2 images.
  • Create: Construct your images at runtime using consolidated images.
  • Tile: Form larger tiles from smaller ones at runtime.

Example:

Handling Browser Bugs

Requirement: You must install a bug workaround.

Problem: You don't want to modify "guts" of your code.

Solution: Create a behavior class and "plug" it into the Microkernal when appropriate.

Example: a nasty Macintosh Navigator VM bug.

  • Step 1: Write behavior class "FrameRepainter," a subclass of Thread, to call a Frame's repaint() method at designated times.
  • Step 2: Write a lightweight behavior class called "RepaintFix" to download, start (and later stop) "FrameRepainter" when and if needed.
  • Step 3: Write an instruction (sentence) to inform the Microkernal to load and execute "RepaintFix" prior to running the applet.

The Proxy Pattern

Requirement: Download as little as possible.

Problem: Your UI may require a "heavyweight object," but you'd rather not download it until some request requires its presence.

Solution: Use a "lightweight" proxy object in place of the real object. Your code will talk to either the proxy or the real object the same way. But when a call comes in to the proxy object, it will load the real object and delegate the request.

Example: (While Proxy is an incredibly useful pattern, Industrial Logic has not used it in the way described above).


Using Patterns

Adaptable Systems

What are Adaptable Systems?

Systems evolve over time--new functionality is added and existing services are changed. Adaptable Systems must support new versions of operating systems, user-interface platforms, or third-party components and libraries. Adaptation to new standards or hardware platforms may also be necessary. During system design and implementation, customers might request new features, often urgently and at a late stage. You may also need to provide services that differ from customer to customer.
--from Pattern-Oriented Software Architecture (POSA) by Buschmann, Meunier, Rohnert, Sommerlad and Stal.

Design Patterns

Forget the Hype.

The following are two excellent definitions of patterns:

Each pattern is a three-part rule, which expresses relations among a certain context, a problem, and a solution.
—Christopher Alexander, The Timeless Way of Building

A pattern is a named nugget of insight that conveys the essence of a proven solution to a recurring problem within a certain context amidst competing concerns.
—Brad Appleton, "Patterns and Software: Essential Concepts and Terminology"

When using patterns, remember:

  • Patterns are suggestions.
  • Patterns point toward solutions.
  • Patterns are implemented differently in programming languages.
  • Patterns combine with other patterns to solve problems.
  • Pattern solutions tend to be generic, and thus promote reuse.

Patterns may make you rich.

artist: Jay Munro, originally printed in DOC Magazine

Using Patterns

Industrial Logic's "Java Engine," the code powering the MTV and IBM applets, has a very high patterns density.

Principal patterns used in the "Java Engine" include:

  • Command
    Intent: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
  • Interpreter
    Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
  • Microkernel —from Pattern-Oriented Software Architecture (POSA) by Buschmann, Meunier, Rohnert, Sommerlad and Stal.
    Intent:

     

    • applies to software systems that must be able to adapt to changing system requirements.
    • separates a minimal functional core from extended functionality and customer-specific parts.
    • serves as a socket for plugging in such extensions and coordinating their collaboration.
  • Composite
    Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
  • Null Object —a pattern written by Bobby Woolf
    Intent: Provide a surrogate for another object that shares the same interface but does nothing. The Null Object encapsulates the implementation decisions of how to "do nothing" and hides those details from its collaborators.

Other patterns and techniques used:

  • Doug Lea's "Concurrent Programming in Java: Design Principles and Patterns"
  • Javascript: for controlling browser windows and parameter passing
  • Factory Method
  • Abstract Factory
  • Decorator

Case Study

MTV & The ILI Java Engine

Problem Pattern
Bandwidth Issues Proxy, Composite, Microkernel
Change Requests Interpreter, Command
Integration Interpreter, Command
New Behavior Command, Composite, Interpreter, Microkernal
Browser Bugs Factory Method, Command
Timing Issues Null Object & Concurrency Principles

The Command Pattern

The Command pattern is one of the most powerful and useful patterns there are.

Problems it helps solve: change requests, integration, new behavior, browser bugs.

Intent: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

This pattern was referred to as "behavior classes" in the main presentation.

ILI Java Engine Implementation:

The Command Interface:

public interface ili_Command {
    public void init(ili_Context context);
    public void execute(String s); }

Sample Usage:

public class MyCommand extends Object implements ili_Command {
    protected ili_Context _context;
    public MyCommand() { }

    public void init(ili_Context context)
    { _context = context; }

    public void execute(String id)
    {
      // do something useful
    }
  }

MacroCommand: Run Multiple Commands

public class MacroCommand
 extends Object implements ili_CommandI
{
  protected ili_ListEnumerator _parms;
  ...
  public void execute(String id)
  {
    ...
    _parms = _context.getListEnumerator(id);
    try
    {
      while ( _parms!=null &&
              _parms.hasMoreElements() )
      {
         _context.execute(
           _parms.nextElement().toStr() );
      }
    }
    catch (Throwable e)
    {
      ... exception handling code
    }
  }
}

The Interpreter Pattern

Problems it helps solve: change requests, integration, new behavior.

Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

  • Context: contains information that's global to the interpreter.
  • Client: builds (or is given) an abstract syntax tree representing a particular sentence in the language that the grammar defines.
  • AbstractExpression: declares an abstract Interpret operation that is common to all nodes in the abstract syntax tree.
  • Terminal Expression: implements an Interpret operation associated with terminal symbols in the grammar.
  • Nonterminal Expression: one such class is required for every rule R := R1 R2 . . . Rn in the grammar.

The ILI Java Engine's Interpreter

We let Commands perform interpretation.

Project goal: let users fully control Commands, thereby controlling client program behavior.

Commands are asked to interpret a language whenever their execute() method is called.

Commands interpret sentences, and do so however they see fit.

Sentences are composed by users.

Users create sentences by working with developer-defined "vocabularies" and "sentence formats."

Users create collections of sentences that combine to form intruction sets.

Ultimately, the Java Engine is controlled, at runtime, by instruction sets.

The POSA Microkernal Pattern

This Pattern

  • helps make systems adaptable.
  • separates a minimal functional core from extended functionality.
  • lets developers easily plug in extensions and coordinate their collaboration.

The ILI Java Engine's Microkernal

Problems it helps solve: new behavior, bandwidth issues.

The Java Engine's Microkernal helps:

  • Store and manage Commands.
  • Store and manage a user's language (instructions).
  • Store and manage system-wide variables.
  • Minimize download times; i.e., some minimally functional Microkernal code downloads first to execute user instructions (using Commands).
  • More Commands are downloaded later (or as needed) and "plugged in" to the Microkernal.