The RPN-calculator application demonstrates object-oriented UI principles
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
Part 6. An RPN-calculator application demonstrates object-oriented UI principles :END_TEXTBOX
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 Parser
and 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 push()
, pop()
, and add()
. The 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 Math_stack
and Parser
objects make up the abstraction layer.
The control object assembles a presentation by asking the abstraction-layer classes for visual proxies — JComponent
s that represent the object’s state. Both the Math_stack
and Parser
implement 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 Parser
and 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 stack
and 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 JComponent
s (it positions them within the frame but does absolutely nothing else with them), while the proxies treat the Rpn
object as a Menu_site
.
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 Math_stack
and 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.
Let’s now move on to the Menu_site
. The Menu_site
interface contains an inner-class implementation called Menu_site.Implementation
. Normally, a class could be a menu site simply by extending this inner class, but that’s not an option here because our one and only extends
relationship is already used up by the JFrame
base class. Consequently, the Rpn
class (Listing 1) implements the Menu_site
interface’s methods as simple pass-throughs, which do nothing but chain through to the equivalently named methods of the Menu_site_support
object: that is, support
; see Listing 1, line 49). These pass-through methods are at the very end of Listing 1.
This way of doing things — implementing an interface with pass-through methods that chain to methods of an implementation class — is a common enough idiom that it ought to be an official design pattern, but as far as I know, it’s as yet unnamed. You can use this mechanism to implement true multiple inheritance in Java: from the perspective of a user of Rpn
objects, Menu_site
effectively acts not as an interface but as a base class that contains an implementation. Rpn
really uses a paired interface/implementation class, however. This design pattern, then, lets you implement multiple inheritance with all the flexibility of C++, for example, without any of the potential ambiguity problems implicit in C++’s inheritance mechanism.
|
The Math stack
Now let’s look at the classes that make up the abstraction-layer “model,” starting with the Math_stack
(Listing 2). The UML version of the static model is in Figure 7. The stack is made up of an array of doubles (stack
: see Listing 2, line 51) and the stack pointer (sp
: see Listing 2, line 53).
The constructor controls the size of the array. The math engine also supports 10 registers (accessed by number), implemented by an array of doubles (Math_stack
register: see Listing 2, line 48).
The vast majority of Math_stack
‘s methods just implement stack-manipulation requests (push, pop, add, and so on), so they don’t need further comment. The proxy maintenance is worth looking at, however. The stack must notify the proxies whenever it changes state so that they can redraw themselves. It does this by means of Swing’s ActionListener
interface. The Stack_viewer
proxies (Listing 2, line 86) implement ActionListener
, and the Math_stack
object keeps a list of the proxies (the Stack_viewer
s) in an AWTEventMulticaster
called stack_proxies
(Listing 2, line 58).
All of the stack-manipulation methods (such as push) finish up with a call to update()
(Listing 2, line 178), which passes an ActionPerformed
message to the multicaster, which in turn relays the message proxies. The visual_proxy(...)
method (Listing 2, line 148), when it creates the proxies, both manufactures the proxy object and adds it to the multicaster.
The Stack_viewer
inner class (Listing 2, line 86) is a Scrollable_JTextArea
(presented in January’s Java Toolbox). In Stack_viewer
, the actionPerformed(...)
method (Listing 2, line 112) performs the only real work by redrawing and nicely formatting the text area. (The Align
class, used to align the numbers on the decimal point, was also presented in January’s Java Toolbox.)
Note that the Stack_viewer
is an inner-class object, so it has direct access to the actual stack
and sp
fields in the Math_stack
outer class, and it indeed accesses these fields directly. Though such a tight coupling does violate the integrity of the outer-class object, which makes it suspect from an object-oriented perspective, all proxies are inherently tightly coupled to the objects they represent.
If you change the object, you’ll have to change the proxies too. Since the coupling is inevitable, there’s no point in adding complexity (additional methods) to present the illusion of decoupling. In any event, nonstatic inner classes are members of the outer class in a very real sense, so it is reasonable for them to use the access privilege available to all members. The maintenance problem inherent in this tight coupling is obviated to some extent by the fact that the two classes being declared in the same place. Consequently, when you make a change to the outer class, it’s easy to find all the other affected classes — they’re all inner classes of the outer class.
|
The Parser
Unlike the Math_stack
, the Parser
is fairly complicated. Let’s start by looking at the pieces; then we’ll put the pieces together.
The Keypad view
The first piece is the Calculator_keypad class (the UML is in Figure 8).
To refresh your memory, the keypad is shown in Figure 9.
The keypad — a JPanel
— uses a GridBag
to lay itself out into two columns and five rows as seen in Figure 10 below.
In Figure 10, the top row holds a JLabel
accumulator, and the right columns of the next three rows hold the add, subtract, and multiply keys. The bottom row holds the “Enter” and “divide” keys.
The left column of the second, third, and fourth rows hold a sub-JPanel
, which uses a second GridBag
to lay itself out as a numeric keypad, as seen in Figure 11 below.
This organization — a class both derives from and contains instances of another class, and objects of that class are organized in a containment relationship — is an example of the Gang of Four Composite design pattern (for more on the Gang of Four, see Resources).
Other design patterns are represented here as well. For example, the Calculator_keypad
and Tape
widgets use the Gang of Four Observer pattern to communicate with the outside world. When an object wants to find out when a user enters text into either widget, the object expresses its interest by telling the widget to send a notification to the object. (It does so by calling addActionListener(...)
. See (Listing 3, line 142.) The widget notifies the listeners when text is available by sending them actionPerformed(...)
messages.
The JButton
objects, when pressed, also notify their listeners (the Observer pattern again), but the current implementation uses a single Listener
object to mediate between the whole set of buttons and the Calculator_keypad
object itself (Mediator
). That is, all the buttons talk to a single Mediator
object, whose job is to update the keypad as necessary. The Controller
object’s actionPerformed(...)
method (Listing 3, line 171) does the work, intercepting button-press notifications and figuring out what to do with them. Numbers accumulate (and are echoed to the accumulator label) until a non-number is encountered, in which case an actionPerformed()
message is sent to the keypad’s observers. Non-numeric key presses are dispatched immediately (and the accumulator window is cleared).
18: * 19: *
|
The tape view
The tape-style view is also straightforward, though it uses a somewhat different internal architecture than does the keypad view (see Figure 12). As is the case with the keypad, the Tape
is a Jpanel
, but it contains only two fields: a JTextField
(input
: see Listing 4, line 30) into which you type your input and a Scrollable_JTextArea
(output
: see Listing 4, line 31), which stores a record of your data entry in a manner similar to the tape on a tape calculator. A log file (log
: see Listing 4, line 33) — implemented using the Log
class discussed in January’s Java Toolbox — also stores the contents of this window. As is also the case with the keypad, any object interested in input from this view should call addActionListener(...)
(Listing 4, line 191) to register as an ActionListener
in the observers
list.
In contrast to the keypad view with its listener-based strategy, the tape view employs a Swing-model object to discover when the user enters characters. For reasons that are not clear to me, the JTextField
doesn’t support the AWT TextField
‘s TextListener
(which was notified when new text was entered into the control). Instead, JTextField
supports an extremely complex set of listeners such as the CaretListener
, which is notified when the cursor position changes, but none of these let you easily find out when the text changes.
The easiest way to trap character-by-character text entry is to provide an implementation of the PlainDocument
— the Swing model associated with the text objects — and override its insertString()
method, which is called when a character is inserted into the string. I’ve done that in insertString(...)
(Listing 4, line 94), which works much like the keypad’s Controller
object, accumulating alphanumeric strings until it encounters a non-alphanumeric character, but processing non-alphanumerics immediately. The Model
object calls process_newline()
(Listing 4, line 123) to dispatch the string off to the listeners by sending an actionPerformed()
message to the observers
multicaster (Listing 4, line 190).
An instance of the model is installed into the JTextArea
UI delegate when the object is created on line 30. (See Resources for more on the Swing architecture.)
Another main difference between the keypad and the tape view is that you can write to a tape view. (The messages are displayed on the tape.) There are two string overrides of write
: They are write(String,Color)
(Listing 4, line 142) and write(String)
(Listing 4, line 145). There’s another for printing numbers (write(double)
: see Listing 4, line 154). Strings are just appended to the current line buffer (buffer
: see Listing 4, line 32) and flushed to the tape when a new line is encountered. Numbers are formatted consistently and then appended to the buffer.
|
The Parser itself
Now we can look at the Parser
class itself, in Listing 5.
You’ll find the UML in Figure 13. Starting at the UML’s bottom, the Parser
needs to talk to both the Keypad_viewer
and the Tape_viewer
in a consistent way. Though these classes are similar, the Tape
supports write()
methods and the Calculator_keypad
doesn’t. I’m reluctant, in a situation such as this, to introduce bogus write()
methods to a class like Calculator_keypad
, which has no possible valid implementation of those methods. The only robust implementation is to throw an exception if the bogus method is called, but I don’t like to move a compile-time error (“method not found”) into a runtime error (the exception toss).
The problem can be solved with the Gang of Four Adapter pattern, in this case a Class Adapter. Viewer_ui
(Listing 5, line 264) defines a uniform interface to the two view classes. I then implement the interface in two adapters: The Keypad_viewer
(Listing 5, line 272) is a Calculator_keypad
(it extends Calculator_keypad
) that implements the Viewer_ui
, and Tape_viewer
(Listing 5, line 278) is a Tape
that implements the Viewer_ui
. The Keypad_viewer
implements empty write methods, since in the current context it isn’t an error to throw away the output. The Tape_viewer
interestingly, has no methods at all: all the methods that the Viewer_ui
interface requires are inherited from the Tape
base class. Java, unlike C++, doesn’t need us to supply any derived-class methods in this situation.
As we see at the center of Figure 13, the Parser
‘s visual proxy is an instance of Viewer
(Listing 5, line 288), which is a JPanel
that contains either a Calculator_keypad
or a Tape
, depending on which UI the user requests. The Viewer
‘s other roles include: installing the Interface menu we discussed earlier on the main-frame’s menu bar and dynamically updating the menu bar with the menu items appropriate for the current view. Finally, the Parser
can write messages on the Viewer
, which relays the messages to the current contained view object.
The Viewer
keeps a reference to the current view in view
(Listing 5, line 291), which is a reference to a Viewer_ui
— the common interface supported by the two adapters. Consequently, it doesn’t need to know which concrete class it’s talking to. The Viewer's
constructor (Listing 5, line 293) installs an AncestorListener
that initializes Menu_site
to point at the main frame when the proxy is installed into the frame. The constructor also installs an instance of the tape-style view by calling use_tape()
(Listing 5, line 372). Additional construction happens in addNotify()
(Listing 5, line 484), which is called when the Viewer
‘s Panel component is realized on the screen. The make_menu()
method (Listing 5, line 492) creates the “Interface” menu, and sets things up so that use_tape()
is called when the user requests a tape view and use_keypad()
(Listing 5, line 392) is called when the user wants a keypad view. The matching removeNotify()
method (Listing 5, line 525), called when the JPanel
is destroyed, removes the “Interface” menu.
The use_tape()
method (Listing 5, line 372) first tries to install a tape-style view by calling replaced_view_with(...)
(Listing 5, line 337). This method is called whenever the user requests (by selecting the appropriate menu item) that the view change. It’s passed the Class
object for the requested view, and if an object of that class isn’t being used as the current view, an object is created and installed, replacing the earlier view. Any menus installed by the previous view are also destroyed at this time. The replaced_view_with()
returns false if the view wasn’t replaced, in which case use_tape()
does nothing. Otherwise, it modifies the menu bar to hold items appropriate to the tape-style view. The use_keypad()
(Listing 5, line 392) method does essentially the same thing, though the menus it creates are different from the ones created by use_tape()
.
Moving over to the left side of Figure 13, we see that the Tape
and Calculator
keypad both notify listeners when they have input by sending them an actionPerformed()
message. The mediator
object (Listing 5, line 319) (an ActionListener
) is notified when text is available, and this mediator relays it to the Parser
‘s parse(...)
method (Listing 5, line 110), which does the actual work. The Viewer
installs the mediator into a view (the Tape
or Calculator_keypad
object) every time a new view is installed (on line 352). So, when the current Viewer
‘s view has text for the parser, it calls parse(...)
, which does a brute-force parse of the input string and makes appropriate calls to the Math_stack
that was specified in the constructor.
|
Related content
- analysisBeyond the usual suspects: 5 fresh data science tools to try today The mid-month report includes quick tips for easier Python installation, a new VS Code-like IDE just for Python and R users, and five newer data science tools you won't want to miss.By Serdar YegulalpJul 12, 20242 minsPythonProgramming LanguagesSoftware Development
- analysisGenerative AI won’t fix cloud migration You’ve probably heard how generative AI will solve all cloud migration problems. It’s not that simple. Generative AI could actually make it harder and more costly. By David LinthicumJul 12, 20245 minsGenerative AIArtificial IntelligenceCloud Computing
- newsHR professionals trust AI recommendations HireVue survey finds 73% of HR professionals trust AI to make candidate recommendations, while 75% of workers are opposed to AI making hiring decisions. By Paul KrillJul 11, 20243 minsTechnology IndustryCareers
- how-toSafety off: Programming in Rust with `unsafe` What does it mean to write unsafe code in Rust, and what can you do (and not do) with the 'unsafe' keyword? The facts may surprise you.By Serdar YegulalpJul 11, 20248 minsRustProgramming LanguagesSoftware Development
- Resources
- Videos