This month we continue the process begun in the January Java Toolbox by completing the RPN (Reverse Polish Notation) calculator application. The RPN calculator is a small but nontrivial application that demonstrates some of the important object-oriented UI principles we've looked at in previous Java Toolbox articles, specifically the visual-proxy design pattern. This application isn't just a toy; I use it virtually every day -- every time I need a calculator, in fact.
A word of warning: the following discussion will be incomprehensible if you haven't read the earlier installments of this series.
TEXTBOX: TEXTBOX_HEAD: Build user interfaces for object-oriented systems: Read the whole series!
- Part 1. What is an object? The theory behind building object-oriented user interfaces
- Part 2. The visual-proxy architecture
- Part 3. The incredible transmogrifying widget
- Part 4. Menu negotiation
- Part 5. Build an application that puts user-interface principles into practice
Reverse Polish Notation was championed for years by Hewlett-Packard, though I've been told that their most recent calculators don't support it anymore (a pity). It is one of those things that are difficult to learn but wonderful to use once you understand them. The RPN's basic notion is built around an arithmetic stack. Numbers, when entered, are pushed on the stack, and all operations use stack items as operands.
For example, when you press the add (+) key, the two items closest to the top of the stack are popped and added together, and the resulting sum is pushed, effectively replacing the original operands. Although that might seem like a strange way to do things, you never need to use parentheses, and once you get used to it, you'll probably rather like it. I've been using my PalmPilot as a handheld calculator, using Russ Webb's great RPN calculator (see Resources), but I wanted one for my computer too. Being a programmer, I thought building one was the easiest way to get exactly what I wanted.
The analysis model
Since the structure of the calculator's UI is closely related to the object model, let's start by looking at the analysis-level static model, shown in Figure 1.
As is the case with all object-oriented designs, the analysis-level classes are found in the implementation as well. Figure 1, therefore, is really an implementation-level diagram of the original analysis diagram. In object-oriented systems, the design-level model is nothing but the analysis-level model with implementation-level detail added.
The first analysis-level class of interest --
Rpn -- contains the
main() method and little else. Though they aren't shown in the diagram, the class also contains a constructor and implementations of all the methods required by the interfaces. In an attempt to reduce the clutter a bit, I usually don't show these in a Unified Model Language (UML) diagram. As is the case with many object-oriented programs,
main() (and the
Rpn class, for that matter) aren't much to look at. Object-oriented systems tend to be networks of cooperating objects, with no central God class that controls everything from above. There's no spider sitting in the middle of the web pulling strands. An object-oriented system's
main() method, as a consequence, typically creates a few objects, hooks them up to each other, and then terminates. That's exactly what happens here:
main() creates an instance of the
Math_stack, hooks them up to each other, and terminates.
The main reason that I have an
Rpn class at all is that I need an implementation-level class to take care of creating the main frame.
Rpn is not a God class, however. Though it continues to exist after the program launches, it is not an active participant in the program.
The other two analysis-level classes constitute the actual calculator: the
Parser parses the user's input and passes it to the
Math_stack, which does the actual arithmetic. We'll look in depth at both of those classes in a moment, but for now note that the
Math_stack contains methods like
Parser, which contains no analysis-level methods of interest, receives the user's input and sends appropriate requests to the
Math_stack. Therefore, no analysis-level messages are sent to it.
Visual proxies in action
The calculator's UI follows the visual-proxy architecture that I discussed in the first couple of installments of this series. Using the vocabulary of the previous articles, the
Rpn class is a control object, while the
Parser objects make up the abstraction layer.
The control object assembles a presentation by asking the abstraction-layer classes for visual proxies --
JComponents that represent the object's state. Both the
User_interface, so they can produce visual proxies when asked. The main purpose of the
Rpn class, with respect to the UI, is to ask the
Math_stack objects for those visual proxies, which it then lays out and displays. The
Rpn object itself creates the main frame and implements a
Menu_site that manages the menu bar.
Note: The proxies can get a reference to the encapsulating container's
Menu_site component by calling
(Menu_site)( SwingUtilities.getAncestorOfClass( Menu_site.class, myself ) );
which returns a reference to the object above
myself in the runtime window hierarchy that implements
Menu_site. That is, it returns a reference to some container of the
myself object, provided that the container implements or extends the indicated interface or class.
So, when it's created, the
Rpn object asks the
parser for visual proxies and then displays them. The visual proxies don't speak to the
Rpn object as such, but they will interact with it through the
Menu_site interface when they need to customize the menu bar. The proxies think of
Rpn exclusively as a
Menu_site -- they don't know the actual class name.
It's important to note the decoupling of these UI elements. The
Math_stack's proxy doesn't know that the
Parser's proxy exists, much less what it looks like -- and vice versa. Similarly, the
Rpn object treats the proxies as simple
JComponents (it positions them within the frame but does absolutely nothing else with them), while the proxies treat the
Rpn object as a
Moreover, the main frame is simply a passive vehicle for holding visual proxies. The proxies communicate directly with the analysis-level object that creates them -- the frame isn't involved -- and these analysis-level objects can communicate with each other. If the state of an analysis-level class changes as a result of some user input, it sends a message to another of the abstraction-level classes, which may or may not choose to update its own UI (its proxy) as a consequence.
Figure 2 shows the resulting UI. The
Rpn class owns (creates and manages) the main frame and menu bar. The top, gray window, which displays the current contents of the
Math_stack object's arithmetic stack, is the
Math_stack's visual proxy. The yellow and white windows are the
Parser object's UI. (The
Parser's visual proxy is a single
JPanel that contains the other widgets.) When you type into the bottom input window, the
Parser parses what you type, displays a record of your requests in the yellow "tape" window (which acts more or less like the paper tape on a financial calculator), and sends appropriate arithmetic requests (push, pop, add, and so on) to the
Math_stack object. The
Math_stack does the work, then updates its proxy to display the new state of the stack. There's no UI-related messaging between analysis-level objects. The
Parser sends domain messages (push, pop, and so on) to the
Math_stack. The fact that the
Math_stack updates its UI as a consequence of receiving these messages is irrelevant.
As it turns out, the
Parser's visual proxy can display itself in one of two ways: the Good way, which uses the computer's keypad, and the Bad way, which simulates a keyboard on the screen. When the proxy is created, it negotiates with the control object and adds the Interface item to the main menu bar. That is, the
Rpn object adds the File and Help menus, but the visual proxy adds the Interface menu for the
Parser. Clicking on this menu shows you the two choices, as seen in Figure 3.
The Good interface is the initial choice, while selecting Bad causes the
Parser object's proxy to change its appearance to that shown in Figure 4.
I think of the second choice as the Bad interface because, from the perspective of usability, the notion that a virtual calculator should look like a physical one is an absurdity. Computer keyboards have perfectly good numeric keypads on them, and there's no reason to simulate that keypad onscreen. I read a paper a while back that claimed that a significant number of people who used the Windows-provided calculator didn't know that they could use the keyboard to control it; they assumed that since the UI displayed buttons, that they had to use those buttons. I've seen the notion of duplicating physical interfaces taken to absurd places. For example, one HP calculator emulator was so true to the original that each button had three purposes. You had to select the secondary and tertiary purposes by clicking on a blue or yellow function key.
This slavish mimicking of the real world does make the program easy to learn, but don't confuse ease of learning with ease of use. I suppose the one saving grace of the Bad interface is that you can use it when no keyboard is available as in a PDA. But that's a special case; the UI should normally be hidden.
You'll note that when the
Parser's proxy changed its appearance, it also added an Advanced menu to the menu bar, as seen in Figure 5. This menu gives you access to the calculator functionality for which there's no keyboard button.
By the same token, you'll notice that the Advanced menu goes away if you switch back to the Good interface and that the Help menu now includes a user-interface item. (I haven't shown it in a figure, so you'll have to use your imagination.) This menu pops up the window in Figure 6, which shows you what you can type from the keyboard. Again, the proxies do this work. The
Rpn object hosts the menu site, but it's a passive participant in the menu negotiation. The proxy for the
Parser object effectively communicates directly to the
Menu_site.Implementation and adds whatever menu items that it needs.
To summarize, the menu bar in the visual-proxy architecture is a composite of menu items created by the individual proxies. When those menu items are selected, the resulting notification goes directly to the proxy that created the menu item. The
Menu_site implementation has no involvement.
The Rpn class implementation
Since we're here, we may as well look at
Rpn's implementation (in Listing 1) before going on to the detailed implementation model. As you saw in Figure 1 above, the
Rpn class has four fields of interest. The
math_engine (Listing 1, line 45) and the
parser (Listing 1, line 46) reference the
Parser objects that compose the logical model. The
parser_viewer field (Listing 1, line 47: a
JComponent reference) points at the parser's visual proxy. The
stack_viewer reference to the
Math_stack's visual proxy (shown as a dashed line in Figure 1) is a local variable of the constructor. (That's why the line is dashed:
stack_viewer isn't a field, but there is a relationship between the two classes.)
As I mentioned earlier,
main(...) (Listing 1, line 76) does little or nothing. It instantiates an instance of
Rpn() and prints a help message if someone launches the program with the wrong number of arguments. Most of the work is done in the constructor,
Rpn() (Listing 1, line 102). The constructor creates the system-level menus (File and Help) and installs them on the
Menu_site.Implementation object. It then gets the two visual proxies from the abstraction layer (Listing 1, line 142) and defines the component-level interaction between the proxies. (When the
Math_stack proxy gets focus, the focus is transferred to the
Parser proxy.) The
Rpn object then creates a splitter frame and installs the proxies into it. Finally, the
Rpn object installs the splitter into main frame. Once the UI is set up, the constructor -- and
main() -- terminate.
From this point on, the program is really a small network of the two objects that make up the abstraction layer, which communicate with each other.
Rpn has no involvement beyond providing a host site for the menu bar. Put another way, the UI and the logical model -- the abstraction layer -- are quite decoupled. You can radically change the organization of the UI without changing the abstraction-layer classes at all, and you can radically change the abstraction-layer classes without affecting the UI-layout code. Moreover, if you do change an abstraction-layer class, all the UI changes (the parts of the UI that expose the state of the class) are concentrated in one place. This UI change will affect the appearance of the whole program, however.