This short article is also available as postscript or PDF.
Marc-Antoine Parent
Centre de Recherche Informatique de Montréal
550 Sherbrooke West, suite 100
Montréal (Québec) Canada H3A 1B9
+1 514 840 1234
maparent@acm.org
The RoleAdapter Design Pattern allows using objects of any model as if they implemented any given programmatic interface, with contextual behavior. To achieve this, it makes objects from many basic building blocks of OOP, like methods, method signatures, interfaces, etc. This allows clients of a data model to define, at run-time, an interface for any data model they have to use. Objects encapsulating methods, defined independently for the model, are chosen and bound to the signatures included in the interface according to external configuration hints. Since the adaptation is done local to a context, different view instances can show different aspects of a complex model. The resulting composite definition of interface is similar in intent to that of subject-oriented programming, but achieved wholly within a traditional OOPL like Java.
Keywords
Design Patterns, Roles, Subject-Oriented Programming
The RoleAdapter pattern allows us to use objects of any model as if they implemented any given programmatic interface, with contextual behavior. Traditional OO design emphasizes that the data model should be generic enough to be used with many views. The views, on the other hand, must reflect the model's structure in their display, and hence are more closely tied to one model's structure. We cannot normally reuse view components with different models, unless they share an interface. In CASE tools, each node in a class diagram is involved in both an inheritance and an aggregation hierarchy. We would want to display the hierarchy obtained from these distinct interfaces in the same reusable display tool.
Ideally, each view we use should contribute to the model's interface at run-time, as in Subject-Oriented Programming [1]. Within conventional OO languages, many Design Patterns are aimed at altering a frozen object's interface. The original Adapter [2] does this, but only at compile-time, and it does not scale well; in a complex system of objects, one adapter is built for each object of the original system, relationships between adapted objects have to be translated into relationships between adapters, etc. Other relevant Patterns are Erich Gamma's Extension [3] pattern, Baümer and Riehle's Role objects [4], etc. Unfortunately, those patterns require the object to follow some extension protocol, which we wanted to avoid.
RoleAdapters are defined on arbitrary classes (not instances) at run-time. To that end, many basic constructs of OOP were made into first-class objects: the method implementation; its signature (name, return and parameter types); the role (or interface: the set of signatures that a class must implement when used in a certain role by a client), etc. (cf. diagram.)
The client expects to use objects according to a set of Signatures
defined in a Role
. Those Signatures
will correspond to Methods
. Binding
objects, grouped in RoleAdapters
, will contain the corresponding pairs. Initially, Bindings
only contain Signatures
; the unbound RoleAdapter
is then equivalent to the Role
. As a client attempts to make an instance of a certain class play a certain Role
, it asks the Context
to create a valid Class-RoleAdapter
relationship (Casting
) by fulfilling each of the RoleAdapter
's Bindings
with a Method
that satisfies the Bindings
's Signature.
In order to find appropriate Methods
, each Context
owns a distinctive set of Hints
, containing factories that will attempt to build the Method
from its Signature
(if the required Signature
fits that of the Hint
.) Configuring different views' Contexts
with different Hints
allows for fine-grained simultaneous access to various aspects of a model at run-time. The MethodFactory
's job may be simple, like creating a Method
of a given class, or retrieving an existing Method
with an alternate Signature
from the Context
, but more complex MethodFactories
allow us to specify arbitrarily complex derived Method
objects.
If no Hint
specifies how to build our Method
, the Context
queries the context-independent MethodRegistry
for a model-specific Method
that adequately fulfills the Binding
. The set of those model-specific Methods
amounts to a dispersed adapter for the model. The candidate Casting
, and any Casting
registered previously, establish equivalence classes between the Signatures
of those Methods
defined strictly in terms of classes, and more abstract Signatures
owned by, and defined in terms of Roles
. In case of failure, the Context
asks a broader Context
that may have further Hints
(using Chain of Responsibility.)
After the binding phase, the client can access the Method
through the RoleAdapter
as follows:
roleAdapterXXX.bindingYYY.theMethod.execute(objectZZZ, params...);
instead of, traditionally:
objectZZZ.methodYYY(params...);
The latter code is also what will often be found in the Method
object's execute
method. But the flexibility we gain offsets the cost of doubling all virtual calls.
Defining Methods
as first-class objects allows us to build and define complex structures from them. For example, we can compose links between objects through Object Composition on accessor-like Methods
. Similarly, we have found it useful to define some setter-like Methods
to be observable, through wrapping them in an observable Decorator. We also use the Observer pattern to create dynamic Methods
, from a Binding
that observes the Context
for changes in the Hint
definitions. In all cases, these complex Methods
are used interchangeably with the atomic ones. We believe that these objects are an interesting Design Pattern in their own right, inspired by Strategies and reminiscent of ValueModels [5].
Using this pattern, we believe that we have achieved a sizeable fraction of the advantages of subject-oriented programming within the confines of a traditional object-oriented language. Readers interested in the uses of the RoleAdapter
pattern should refer to papers on the Giza framework [6].
The author wishes to thank CRIM for its support of our team's research in information visualization. Many thanks go to Luc Beaudoin, who made Giza necessary by inventing innovative visualization techniques. The project to apply them to diverse data models received support from Frances de Verteuil, director of the HCI group at CRIM. Many people at CRIM contributed to the Giza architecture in various ways, most notably Louis Vroomen who made it yield visible results long before it should have. Ruedi Keller, of Université de Montréal, also offered useful input.