2.3 Multiple Variations of Individual Objects
2.3.3 Combining Inheritance and Helper Objects
As discussed in Sec. 2.2, inheritance and object composition provide different advantages for expressing variation. Object composition is used to implement dynamic variations, but for static variations inheritance is often preferred, because it supports structural variations of classes. Thus, these two mechanisms are often used together to express different variations of an object. In this subsection, we use an example from Java Swing library to analyze the problems that appear in the interaction of these two variation mechanisms.
Object composition supports dynamic variation of an object by outsourcing the varying functionality to helper objects, as described in Sec. 2.2.1. In this way implementation of
a logical object is split into multiple objects implementing different parts of its behavior. Such objects tend to be covariant with respect to other variation points of the logical object.
An example that illustrates the need for covariant dependencies between classes and the problems of current mechanisms in this respect concerns the variations of visualization functionality in the Java Swing library. Visual representation of a widget is mainly determined by its type, e.g., a tree has a different representation than a button or a dialog. In addition, the Swing library provides a possibility to change the look-and-feel style of widgets in a running application. Thus visualization of a widget also depends on the selected style.
Figure 2.5 shows an excerpt of a slightly simplified implementation of look-and-feel functionality in the Swing library. The visualization related methods are defined in the strategy interface ComponentUI. The base class for widgets, JComponent, delegates these methods to the strategy object referenced by the variable ui. For each pair of a widget type and a look-and-feel style, there is a class that implements the corresponding visualization functionality. For example, the classesBasicListUIandMotifListUIin Fig. 2.5 implement the visualization functionality of the list widget for the basic look-and-feel style and the Motif toolkit style respectively.
Whenever the look-and-feel style is changed, the widget is notified by calling the method
updateUI. The latter usesUIManagerto create a visualization implementation – an instance ofComponentUI) – that corresponds to the type of the widget and the current look-and- feel style.
There are several problems with the design of the library.
The first problem concerns the covariant dependency between a widget and its visu- alization helper. For example, the precise positions of list items depend on the visu- alization style of the list. Therefore, the interface of list visualization helpers, ListUI, defines additional methods to get information about the locations of the list items (e.g., locationToIndex on line 31). These methods are used in the implementation of
JList (line 16). However, the type system of Java cannot specify that instances of JList
can be composed only with instances of ListUI. Hence, a type cast is needed to access the additional methods ofListUI.
An analogous problem exists in the opposite direction: widgets pass themselves to visu- alization objects (seeinstallUIon line 4). However, subclasses ofListUIcan work only with instances of JList, hence they must cast the parameter to the expected type (line 38). Another problem of the library is related to the instantiation of the visualization objects. The class of the visualization helper depends on both the type of the widget to be vi-
1 abstract class JComponent {
2 ComponentUI ui;
3 String getUIClassID() { return ”ComponentUI”; }
4 void updateUI() {
5 ui = UIManager.getUI(this); 6 ui.installUI(this);
7 }
8 void paint(Graphics g) { ui.paint(g); }
9 ...
10 } 11
12 class JList extends JComponent {
13 String getUIClassID() { return ”ListUI”; }
14 ListModel getModel() { ... }
15 String getTooltipText(Point pt) {
16 int idx = ((ListUI)ui).locationToIndex(pt);
17 ... 18 } 19 ... 20 } 21 ... 22 23 interface ComponentUI { 24 void installUI(JComponent c); 25 void paint(Graphics g); 26 Dimension getPreferredSize(JComponent c); 27 ... 28 } 29
30 interface ListUI extends ComponentUI {
31 int locationToIndex(Point pt);
32 ...
33 } 34
35 class BasicListUI implements ListUI {
36 JList list; 37 void installUI(JComponent c) { 38 list = (JList)c; ... 39 } 40 void paint(Graphics g) { 41 ... list.getModel() ... 42 } 43 ... 44 } 45
46 class MotifListUI extends BasicListUI {
47 void paint(Graphics g) { ... }
48 ...
49 }
1 class UIManager {
2 static LookAndFeel style;
3 static ComponentUI getUI(JComponent c) {
4 String uiCls = style.uiTable.get(c.getUIClassID());
5 /* load and instantiate uiCls */
6 }
7 ...
8 } 9
10 abstract class LookAndFeel {
11 Hashtable<String, String> uiTable;
12 abstract void initialize();
13 ...
14 } 15
16 class BasicLookAndFeel extends LookAndFeel {
17 void initialize() { 18 uiTable.put(”ListUI”, ”BasicListUI”); 19 uiTable.put(”ButtonUI”, ”BasicButtonUI”); 20 ... 21 } 22 ... 23 } 24
25 class MotifLookAndFeel extends BasicLookAndFeel {
26 void initialize() { 27 uiTable.put(”ListUI”, ”MotifListUI”); 28 uiTable.put(”ButtonUI”, ”MotifButtonUI”); 29 ... 30 } 31 ... 32 }
Figure 2.6: Instantiating visualization objects in Swing
sualized and the look-and-feel style selected in the application. Unfortunately, the type system and the dispatch mechanism of Java are not capable of expressing such a depen- dency. Complex and not type-safe designs based on reflection and factory infrastructure are used as workarounds.
A simplified version of the code of the Swing library responsible for selecting and in- stantiating visualization classes is shown in Fig. 2.6. The singleton classUIManagerkeeps track of the currently selected look-and-feel style (referenced by the variable style); its method getUI creates a visualization helper for the given widget and the current style by using Java reflection. Every widget class implements getUIClassID, which returns a textual identifier of the visualization class required by the widget (e.g., see lines 3 and 13 in Fig. 2.5). Further, each look-and-feel style class maintains a table that maps these identifiers to the names of the visualization classes for this style (lines 10-32, Fig. 2.6). Finally, the selected class is instantiated using Java reflection.
1 abstract void paintWidget(JComponent comp, LookAndFeel lf, Graphics g);
2 void paintWidget(JList comp, BasicLookAndFeel lf, Graphics g) { ... }
3 void paintWidget(JList comp, MotifLookAndFeel lf, Graphics g) { ... }
4 void paintWidget(JButton comp, BasicLookAndFeel lf, Graphics g) { ... }
5 ...
Figure 2.7: Expressing variations of widget rendering by multi-dispatch on the type of the widget and the type of the look-and-feel style
Such a mechanism is very flexible, because it allows installing new look-and-feel styles at runtime. However, the usage of reflection has several disadvantages with regard to type safety and IDE tool support (e.g., refactoring or dependency analyses).
An alternative solution would use the Abstract Factory design pattern [GHJV95]: the abstract factory would contain methods for the creation of visualization implementations for different types of widgets; there would be a concrete factory for each look-and-feel style implementing these methods correspondingly. A solution based on the factory pattern has its own disadvantages: it makes it difficult to add new types of widgets and introduces undesired interdependencies between widgets, because the abstract factory knows about all widgets and all widgets know about the abstract factory.