Brian Maso's Tecno-Geek Weblog
The musings of a mild-mannered tecno-geek.
        

Delegation Pattern Allotropes

Design patterns, made popular by [GoF], describe common cross-language software component patterns. Design pattern descriptions are language-neutral by... well, by design. The genericity is good because it gives programmers a common language.

But take any design pattern and make a concrete realization in different languages, and you are going to get code that is structured very differently, to the point of being unrelatable. Even within a single language, a design pattern may take on many different forms. Each separate realization will have different API charateristics, internal structure, performance and maintenance characteristics. The term allotrope describes a specific form of a design pattern.

A catalogue of allotropic forms of a design pattern provides a vocabulary for physical code, in the same way that design patterns provide a vocabulary for high-level software design and architecture. By identifying and cataloguing implementation-specific details of pattern implementations, models involving well-known design patterns can elide implementation specifics and achieve a maximum simplicity. This fits in with the Apply Patterns Gently practice in Agile Modeling. Applying a pattern gently means using the simplest allotropic form to get the job done. [Maso] memorializes my thoughts about allotropic forms and Agile Modeling.

This paper describes the five allotropic forms of the Delegation pattern [Grand], a.k.a. the inter-object "uses" relationship. A delegation implementation may physically involve multiple objects, but for the purposes of modeling only the delegater and the delegee are interesting. All the other objects in the implementation are merely allotropic artifacts. Because all allotropic forms implement the same "uses" relationship between delegation collaborators, you can refactor between the forms to achieve desired performance or organization without significantly effecting the overall model.

The Delegation Pattern

This basic pattern describes an inter-object collaboration in which one object uses a second object to complete an operation. Objects use other object operations this way to avoid duplicating code, to separate concerns, and (when used with abstractions) to de-couple implementation classes.

Distinctions

Delegation is an inter-object collaboration. One object must be sending messages (invoking methods) on another object for delegation to take place. It is also generally acceptable for a class to take the place of either member of the collaboration. For example, an object may delegate to a static method instead of an object method as part of an operation. For the rest of this paper I'll be refering to objects in the roles of delegater and delegee, but a class may take on either role.

Merely having one object invoke a method on another is not necessarily delegation. Delegation is one object using another to perform significant work to complete an operation. This is as opposed to one object using another to merely obtain a reference to a third object (this third object then is actually used to perform part of an operation). Using an intermediary's "lookup" operation to obtain a third object reference is merely reference book-keeping. The intermediary object in this case shouldn't show up on architectural representations of the uses relationship between the first and third objects.

For example, consider the uses relationship between a Car and a GasStation. The Car uses the GasStation as part of its fillUp() operation. In one possible implementation, the Car obtains a GasStation reference through an intermediary Map object. With respect to the Car.fillUp() operation, the Car does not delegate to the Map object. It is merely using the Map to help with reference book-keeping.

The Five Forms

The five allotropic forms of the Delegation pattern are:

  • Containment. The delegee is full contained by the delegater, and is part of its black-box state.
  • Association. The delegater is given the delegee reference prior to delegation.
  • Per-invocation parameter. The delegee is given as a formal parameter to the delegater method.
  • Per-invocation creation. The delegater creates a delegee object as part of the operation.
  • Intermediary lookup. The delegater asks an intermediary object for a delegee.

Note I am assigning these names to these allotropic forms in an effort to create a common vocabulary. The forms don't come from thin air, though. [Riel] describes six possible implementations of the inter-object "uses" relationship. I have assigned names to these implementations, and have identified one of the forms as a combination of two of them.

Containment

The delegater creates and prepares the delegee prior to delegation. Creation occurs some time between delegater constructor invocation and, in the most extreme implementation of lazy initialization, at the latest possible moment when the delegation takes place for the first time. The delegater duduces the proper mechanism and parameters to create a delegee instance through delegater state or through ambient data available to the delegater. Valid creation mechanisms might include using a delegee class constructor, any one of the well-known creational patterns (Factory, Object Pool, etc.), or by some other mechanism.

(Note that Factory, Object Pool, use of a constructor, and other so-called "creational" patterns should be considered allotropic forms of a more general Allocation design pattern for high-level modeling purposes. In that case, the containment form of Delegation may employ any one of the allotropic forms of Allocation interchangeably.)

Whatever the technique may be, the delegater creates the delegee and stores a reference so that the same delegee may be used for different operation invocations. The lifetime of the delegee is bound to the delegater, which is part of containment. In the simplest form the delegater stores the delegee reference in an instance variable, or possibly a collection is used to implementation a 1-to-many contianment relationship.

Association

The delegater is given a reference to a shared delegee. The delegater caches the delegee reference it receives. The delegee reference may come as a formal parameter to a method, or may come indirectly through an intermediary object -- the exact mechanism is just reference book-keeping. The important point is that the delegater does not contain the delegee, so the delegee is not necessarily contained by the delegater. Insead the delegee may be shared between many other objects. An example is the association between a CarModel object and its CarManufacturer. Clearly the same CarManufacturer object may be shared between multiple CarModel instances. During CarModel construction and initialization it receives the CarManufacturer reference and caches it for later use.

Note that "global" data, such as object referenced by public static class fields, are considered ambient associated objects. An operation implementation that uses a global static method would be another form of the association allotropic form, in which the class of the static method would be the delegee.

Per-invocation Parameter

The delegater receives a delegee reference as a formal parameter to an operation. The delegater uses the delegee to complete the operation. The delegater does not cache the reference to the delegee. Note that a particular Association implementation may look very similar: the delegater caches a delegee it receives through the operation's formal parameters. Whether or not the delegation is Association or Per-invocation Parameter is not important unless there is state induction between the delegater and delegee objects when the reference is cached. State induction is described below, and is an important distinction between the different alloptropic forms of this pattern.

Per-invocation Creation

The delegater creates a new delegee instance to perform each operation invocation. (Rather, the delegater Allocates the delegee -- see note on Allocation above). This form is most closely related to the Containment allotrope, and a simple refactoring can be used to convert between the two.

Intermediary Lookup

The delegater uses an intermediary object to obtain a delegee to use for the duration of an operation. What distinguishes this form as unique is that it is basically a recursive "uses" relationship. The delegater "uses" an intermediary object -- any one of the six allotropic forms of Delegation listed in this paper may be used including this one. Potentially a chain of multiple intermediaries may be used to obtain a final one that will perform a substantive portion of the delegater's operation.

An implementation of the intermediary and 1-to-many containment (in which the containing object employs a collection) may actually look the same. In both cases the delegater obtains a reference to the final delegee through a third object -- the intermediary object or the collection object, respectively. The distinction between the forms is importnat if there is state induction between the delegater and the delegee. The importance of state induction is described below.

Discussion

State Induction and Refactoring

The allotropes above can be divided in to two categories based on whether or not each form exhibits state induction. State induction means one object tailors a second object's state so that the second may be used by a specific process. For example, let's say you are implementing a method in Java to list all files in a particular directory. The method creates a java.io.File object representing the directory, and in so doing employs the Per-invocation Creation form of Delegation. The File object's state is initialized specifically to be used by the delegater object. The File object's state has been induced to work specifically for the delegater.

State induction may be employed by the Containment and Per-invocation Creation forms of Delegation. In both of these forms the delegater assumes complete control over the delegee. This includes the assumption that the delegee will not service simultaneous requests from other objects or undergo asynchronous state changes.

Its very easy to refactor code between the two forms that have state induction, because both forms make the same basic assumptions about the relationship between the delegater and the delegee.

Similarly, the forms that don't employ state induction -- Association, Per-invocation Parameter and Intermediary Lookup -- can be refactored between each other without significant difficulty. Again, that's because the delegater does not assume it has exclusive control over the delegee. Any attempt to induce the state of the delegee in these forms is generally a mistake. Customizing the state of the delegee so that its state most desirable for a specific delegater will obviously work to the detriment of any other simultaneous clients of the delegee.

It is difficult to constrain the effects on a codebase of refactoring between state-inductive and non-state-inductive allotropes. The allotropic forms without state induction may require the client of the delegating operation to provide extra data to the delegater: a reference to the delegee, or sufficient data/references for the delegater to obtain a reference to the delegee. Forms with state induction do not require the client to propvide any information about the delegee to the delegater. So in both cases all clients using the delegation operation may need to be greatly modified to support a new collaboration pattern.

Gentlest to Strongest Forms

The simplest form of delegation, from the perspective of the delegation operation client, is containment. The delegation can be completely hidden from the client. The client may need to provide some information to the delegater's constructor (or initialization methods), but that is the extent of the visibility of the pattern to the client. So refactoring the delegater will not effect its clients.

Per-invocation creation is almost the same, though the client may have to provide some information in the form of formal parameters to the delegating method. This is conceptually no more or less complicated than the Containment form. But it turns out it is easier to refactor from Containment to Per-invocation Creation than the other way around, in terms of what potential modifications would need to be made to all clients.

Association is analogous to Containment, except that the delegee is not allocated by the delegater. Fro the perspective of clients, this is the simplest of the state-inductive forms. Per-invocation parameter is only slightly different, and would be more or less equal in terms of complexity except that it is easier to refactor an Association form to Per-invocation parameter than the other way around.

Intermediary object requires the most thought and design, because it potentially involves many objects, with consequent complexity. Java's JDBC and Windows ODBC demonstrate two examples of entire APIs constructed to support intermediary lookup for delegation.



© Copyright 2003 Brian Maso. Click here to send an email to the editor of this weblog.
Last update: 4/23/2003; 11:16:31 AM.