Build user interfaces for object-oriented systems, Part 2: The visual-proxy architecture

A scalable architecture for building object-oriented user interfaces

1 2 3 4 5 6 7 Page 2
Page 2 of 7

My experience tells me that it's not possible for a well-designed business object to be used outside of a specific problem domain, and that within a problem domain, the object's UI can indeed be well-known. Consequently, having the object create its own UI is not only possible, it's desirable. The state information that has to be displayed on the UI is, after all, inside the object, and it should stay there. Violate the encapsulation with an accessor (methods that provide access to the underlying implementation, typically coded as get/set methods), and you've damaged the integrity of the object. Moreover, the UI will be consistent -- the same attribute will always be represented in the same way. As I said earlier, this consistency is usually considered desirable, and it is, in any event, a characteristic behavior of object-oriented systems. If you can't put up with that behavior, you might want to consider a different design methodology.

A solution

The solution that I'll discuss this month demonstrates one of several possible approaches to solving the I/O problem. This particular solution works well in the simple forms-based I/O situation that I discussed earlier. I'm using a personal design pattern that has worked well for me for the past 10 years or so that I've dubbed "visual proxy." The visual-proxy pattern is in some ways a specialization of the Presentation/Abstraction/Control (PAC) architecture described in Pattern Oriented Software Architecture: A System of Patterns, by Buschmann, et al. (see Resources), so I'll use their terminology here.

(Digression: A design pattern is never invented, it's discovered. By definition, unless the pattern was developed independently by different designers intent on solving similar problems, then no pattern emerges. I suspect that the "visual proxy" is a pattern because I've used it many times in my own work, and it seems to fit into design discussions I've read in various books. I haven't documented the use, however. Consequently, if anyone else has used a system such as the one I'm about to describe, send me a note. If it turns out to be a real pattern -- as compared to a design idea on my part -- I'll write it up more formally.)

The abstraction layer

The abstraction layer in the PAC architecture is what I call the logical model -- the group of objects that directly models abstract concepts in the problem domain. The classes that define these objects are the product of the analysis (as compared to design) process, and shouldn't have a specific presentation associated with them. Sometimes, people call the objects in this layer "business objects" -- a term that I'm not particularly fond of, because it implies that the only programming that's possible is business programming. The abstraction layer is a rough equivalent to the "model" in MVC.

The presentation layer

Those classes that are responsible for drawing on the screen comprise the presentation layer. These classes render one or more attributes of the classes in the abstraction layer. I think of them as visual proxies because they really are proxies for -- representatives of -- abstraction-layer objects. The control layer, whose job is to assemble the screen seen by the user, requests visual proxies from the underlying abstraction layer. These proxies are quite abstract from the perspective of the control object -- in my implementation they're returned as JComponent objects -- so essentially there is no coupling between the control layer and the other two layers.

There is a tight coupling between an abstraction-layer class and its proxy classes, but the proxies are not coupled to each other at all. That is, abstraction-layer objects might communicate with each other, but proxies communicate only with the abstraction-layer object that creates them. (For example, event-style messages flow directly from the presentation layer to the model layer.)

The Swing "separable-model" architecture provides a good way for the presentation layer to communicate with the abstraction layer. The Swing UI Delegate (or View/Controller, as it's sometimes called), without the model installed in it yet, is a good example of a visual proxy. A Swing "model" object (such as a ListModel or Document) is used to store the state of some attribute inside an abstraction-side object; it's actually a field of the abstraction-layer class. The UI Delegate (the JList or JTextField) is created when the presentation layer asks the abstraction layer for a visual proxy, and at that time, the UI Delegate is hooked up to the model object. Presentation-side modifications to the proxy are then reflected directly in the abstraction-side object with no need for listeners: Swing takes care of updating the abstraction-layer's state in response to user input. From the perspective of the abstraction-layer object, the state of the model changes magically as the user interacts with the associated UI Delegate. (Be sure to synchronize access if you access the "model" field while the form is displayed; user-mandated modifications will be happening on the Swing event-loop thread, not on the thread that's examining the state of the object.) Also, a given attribute might be displayed simultaneously on several screens but share the same model object, so all the presentations will reflect any changes made to the shared model.

An alternative, equally viable solution is to use an AWT object as the visual proxy and then register some inner-class object of the abstraction-layer object as a listener. Again, events effectively flow directly from the proxy to the abstraction layer with no MVC-style "controller" involved (though you could add one in this situation if that makes sense).

Think of the visual proxy -- the presentation object -- as being part of the abstraction-layer object that created it, as if a single object is actually in two places at once. Douglas Adams, in his book Hitchhikers Guide to the Galaxy, postulated that mice were a projection into our three-dimensional universe of a small piece of an intergalactic pan-dimensional being. You can think of it like that (or not). The proxy is the mouse, the abstraction-layer object is the intergalactic pan-dimensional being. Put another way, consider a puppet stage. What you're looking at from the outside (the view) is a cleverly disguised presentation of the puppeteer's hand attribute. That hand is part of the (model-side) puppeteer, even though it appears not to be connected to the puppeteer. If the puppeteer goes away, so does the hand (and presumably, the associated puppet). Figure 1 illustrates the visual proxy.

Figure 1. The visual proxy

An implementation note:

Though the proxy effectively is part of the abstraction layer in that there's a very tight coupling between an abstraction layer object and its proxies, proxies are best implemented as separate classes. That is, the abstraction-layer object is not asked to render itself, it's asked to provide a proxy that can render an attribute. Therefore, while the render-yourself strategy has the advantage of simplicity, it's not workable unless the object is simple enough that the way in which it will render itself is not likely to change. (A String is a case in point.) Note, however, that the render-yourself strategy doesn't have to be monolithic. For example, I can ask an Employee to render only one attribute of itself, such as its name, without requesting a proxy for its name.

Whatever your feelings are about pluggable components, it's obvious that you don't want to rewrite a class simply to change the appearance of an object on the screen, which is another problem with the render-yourself strategy. Because the visual proxy is a separate class, objects of that class can be provided by derived-class overrides of the logical-model classes (or by the Gang of Four's "Abstract Factories"), thereby decoupling the implementation of the view from the "business" object's implementation. In any event, many proxies can be implemented entirely generically -- with no coupling to the abstraction layer. A Swing JTextField, for example, can be used as a proxy for an abstraction-layer attribute whose state is stored in a "Document" object. The JTextField doesn't know anything, however, about the way in which the abstraction-level object works.

That being said, I prefer to use inner classes rather than derived classes to implement nontrivial proxies, mostly because I'm nervous about using the protected keyword in Java. For reasons that I don't understand, the protected keyword grants package access as well as derived-class access, and I don't want just anyone in the package to be able to access the object's state. If that were possible, I'd be back to using get/set methods, which, as I explained in July, are not particularly object-oriented.

What I really want to do is create an interface that can be used only between the proxy and the object that it represents -- which, by definition, are tightly coupled -- so that the proxy can get itself initialized properly. In an ideal world, you'd be able to specify that "this method may only be called by those methods of that class." That's not possible in Java (or in C++), but you can implement the proxy class as an inner class of the abstraction-layer class and make the proxy class private. This way the outer-class object can access the methods (because the outer class has access to the private parts of the inner class), but nobody else can access them. If you use an inner-class strategy, though, derived classes can no longer supply the proxies. I'm willing to accept this limitation in order to write more robust code, because I've rarely had to modify the way in which a proxy displays itself. That is, my experience tells me that a given attribute of a given class typically will display itself the same way every time it's displayed, even in different programs. Occasionally, I'll need to write a proxy that can handle a couple of different presentations, but that's rare and can usually be handled by using a Gang of Four command object. (For example, I can pass a NumberFormat object into a proxy for some numeric attribute to tell it how to format itself.)

The control layer

I've already brought up the control object, but for the sake of completeness, let me define it: The control object's job is to build a view by requesting proxies from the model side and installing them in a window. The control object, then, is in charge of layout and large changes to the UI. A control object can itself be an abstraction-layer object, whose presentation is an entire form, for example. (This architecture is fundamentally recursive -- what the Gang of Four refers to as the Composite pattern.)

Though the abstraction and presentation layers are loosely analogous to the model and view of MVC, it really has no notion of a control object. An MVC "controller" is an object that accepts events from the view side and translates them into messages on the model side. A PAC control object, on the other hand, is passive with respect to message flow. The messages go directly from the visual proxy to the model-side object that manufactured the proxy. The window on which the view is built (which is typically the UI of the control object itself) usually is not involved in this message flow.

Modeling an implementation

As I said earlier, I'm going to demonstrate the visual-proxy architecture with a simple fixed-position form system. This system will build a simple generic form that displays proxies that represent model-side attributes. The proxies are at a fixed place within the form. A layout-manager approach, of course, is more in keeping with the UI philosophy of Java, but I didn't want to clutter up the example with unnecessary (from the architecture perspective) details.

I've sketched out the forms-system incarnation of the PAC architecture in the static- and dynamic-model diagrams in Figures 2 and 3. I'll start with the static model shown in Figure 2. Here, the core interface is User_interface in the upper-left corner. To be displayed on the screen, a class must implement User_interface and agree to provide visual proxies for its attributes when asked. I've coined the "displayable" stereotype for this purpose. Proxies are returned as instances of any class that implements JComponent, so you have a lot of latitude about what you might return.

Figure 2. PAC Static Model

The Form class (middle-right) is a PAC "control" object. Its job is to request proxies from the "displayable" objects that it knows about, and arrange the proxies appropriately on the screen. Form extends JComponent, so an object can use an entire form as its proxy if it wants to. (This forms-inside-forms approach is the Gang of Four Composite pattern.)

The Form uses Elements (a private inner class) to keep track of where you want various attributes of various objects to be displayed on the form. The Element holds a location rectangle as well as strings that identify the class and attribute names. Note that an attribute name, not the name of a field, is stored. Remember, if the designer is doing his or her work properly, the attribute set of a class should not change; the fields change all the time as implementation progresses.

Two types of Element objects are supported. An "invariant" element (is_invariant is true) is one that will be exactly the same every time the form is displayed. Things like labels and icons are "invariant." Invariant elements are put into a form by calling the add_field(invariant: JComponent, location: Rectangle) method, which creates an appropriate Element (using the Element constructor with the same arguments as add_field()) and attaches it to the list of fields. When the form is activated, an invariant JComponent object is positioned at the specified location and then displayed, but it's essentially passive: it typically won't interact with the underlying abstraction layer.

All fields that are not invariant are assumed to represent some attribute of some class. These fields are added to the Form using the other version of add_field, which is passed strings holding the class name and attribute name instead of a pre-built JComponent. The equivalent Element constructor is then called by add_field().

Related:
1 2 3 4 5 6 7 Page 2
Page 2 of 7