Multidimensional variation can be expressed by means of method dispatch by multiple parameters, known as multi-dispatch or multimethods. For example, dependency of widget rendering functionality on the type of widget and the look-and-feel style could be expressed by a multi-dispatched paintWidgetmethod, as shown in Fig. 2.7. The method takes a widget, a look-and-feel style as parameters and has different implementations for different pairs of concrete types of these parameters.2
The design with multimethods modularizes dependencies of a method on different com- binations of variants of independent variation points, e.g., one variant point representing different widget types, and another one representing different look-and-feel styles. As a result, the method can be extended with respect to new variants of all variation points on which it depends, e.g., methodpaintWidgetcan be extended with respect to both new widget types and new look-and-feel styles.
Consistency of the implementation of a multimethod can be ensured by two kinds of static checks. Completeness checking (also known as exhaustiveness checking) ensures
2
The third parameter of the method is not used for dispatch in the example, but we could also specialize the method for subclasses ofGraphics, e.g., in order to make use of the graphical operations supported by specific graphical contexts.
that the method is completely implemented for all possible combinations of parameter values. Multi-dispatch also makes it possible to provide various default implementa- tions of the method, e.g., if we define a new look-and-feel style class as a subclass of
BasicLookAndFeel, it would inherit all implementations ofpaintWidgetexcept for the over- ridden cases. Uniqueness checking (also known as ambiguity checking) ensures that for every method call the most specific implementation of the method can be uniquely se- lected. In this way it is guaranteed, that default implementations of the method are not conflicting. Compleness and uniqueness checking help to prevent accidental errors caused by forgetting to implement a method for certain cases or by giving conflicing implementations. These checks are especially important for ensuring implementation consistency when a method is extended with new variations.
Multi-dispatch of methods is able to express variations of individual methods only. In Fig. 2.7, we simplified the widget visualization functionality, introduced in Sec. 2.3.3, to a single paintWidget method. In the original example, the visualization functionality is implemented by objects containing multiple operations and visualization-related state. The state is necessary for storing the configuration related to widget visualization and various cached computation results. Thus, for a full support of variation of widget visualization we need to express variations of objects rather than individual methods. Although multimethods cannot directly express dependencies of objects on multiple variations points, they can be used for polymorphic instantiation of objects with respect to multiple parameters. For example, visualization helpers of widgets can be instantiated by a factory methodcreateUIdispatched both by the widget and the look-and-feel style, as shown in Fig. 2.8.
Such design provides the same extensibility properties as instantiation of helpers using reflection, presented in Sec. 2.3.3, at the same time avoiding the problems of reflection. We can extend the library both with new types of widgets and with new types of look- and-feel styles by implementing new subclasses of ComponentUI and instantiating them in corresponding new cases ofcreateUI. Differently from the solution based on reflection, it is statically checked if the instantiated classes exist and are instances ofComponentUI. Moreover, it is statically checked ifcreateUIis completely implemented for all variants of widgets and look-and-feel styles, and if there are no conflicting default implementations of the method.
In general, multi-dispatched factory methods provide a solution for implementing the mapping from the selected configuration to the instantiated classes. As was discussed in Sec. 2.2.2 and 2.3.2, such mapping can also be implemented using conditional statements. The advantage of a multi-dispatched factory method is that it is extensible with respect to new variants, because we can extend the factory method with new cases in a modular way. It is also more reliable than conditional statements, because completeness and
1 abstract ComponentUI createUI(JComponent comp, LookAndFeel lf);
2 ComponentUI createUI(JList comp, BasicLookAndFeel lf) {
3 return new BasicListUI();
4 }
5 ComponentUI createUI(JList comp, MotifLookAndFeel lf) {
6 return new MotifListUI();
7 }
8 ComponentUI createUI(JButton comp, BasicLookAndFeel lf) {
9 return new BasicButtonUI();
10 } 11 ...
12 abstract class JComponent {
13 ComponentUI ui;
14 void updateUI() {
15 ui = createUI(this, UIManager.style);
16 ui.installUI(this);
17 }
18 void paint(Graphics g) { ui.paint(g); }
19 ...
20 }
Figure 2.8: Dispatching instantiation of helper objects for widget visualization both by the type of the widget and the type of the look-and-feel style
uniqueness checking ensure that the method is implemented for all possible cases and detect conflicting default implementations of the method.
Nevertheless, multi-dispatched factory methods do not solve the scalability problem, because we still need to combine variations of an object using multiple inheritance, which means that we still need to define a class for every valid combination of variants and an implementation of the factory method instantiating that class. Also, note that the design of Fig. 2.8 provides a solution for instantiation of helper objects, but not to the typing problems discussed in Sec. 2.3.3.