Our first pattern story is about the Eclipse-type extension mechanism that is provided by the Eclipse core run-time component (see Figure 31.1).
Eclipse is an extensible platform. It cannot anticipate all the services its objects need to provide. As Eclipse evolves, new features will be added that will require additional services or behavior from existing classes. If we want stable APIs, we can't simply change the interface of an existing class or add a new interface. We need a mechanism to evolve the Eclipse interfaces without changing the definitions of the base interfaces. We need to be able to extend the behavior of Eclipse-provided types. The basic Java language doesn't help.
Let's see how this all got started in Eclipse. Eclipse strictly separates UI from non-UI parts. The org.eclipse.resources
plug-in provides abstractions like IFile
, IFolder
, and IProject
. Objects of these types need to be presented to the user in the UI. For example, the Navigator presents an icon and user label for IFile
objects. The challenge is that the basic IFile
interface shouldn't be “polluted” with UI details. It has to be fully UI agnostic. One solution is to add UI-specific aspects in a wrapper object. There could be an IUIFile
type that wraps an IFile
and adds the presentation aspects to IFile
. This is not very elegant and adds overhead. Every time we add another IResource
, we also have to add an IUIResource
. Instead, we need a way to extend the behavior of IFile
, IProject
, IFolder
so they can also be presented.
As a motivational example consider the Eclipse Properties view. The Properties view presents a set of properties of the currently selected object. Figure 31.2 shows the properties of the selected resource in the Navigator.
There are two parties involved in the properties display: the Properties view and the selected object. The Properties view needs an interface to fetch the properties so that it can display them and the selected object needs to support this interface. In Eclipse, the interface required by the Properties view is IPropertySource
and is defined as follows:
Example . org.eclipse.ui.views.properties/IPropertySource
public interface IPropertySource {
public Object getEditableValue();
public IPropertyDescriptor[] getPropertyDescriptors();
public Object getPropertyValue(Object id);
public boolean isPropertySet(Object id);
public void resetPropertyValue(Object id);
public void setPropertyValue(Object id, Object value);
}
The selected object somehow has to surface this interface to the Properties view. A straightforward solution is to implement it directly in the class of the selected object:
public interface IFile extends IPropertySource
This solution has several problems:
Adding many such service interfaces for other components to a class will result in a bloated interface.
The fact that a class implements a service is an implementation detail that you don't want to expose if the type is API. Adding such an interface once the class is published is a breaking API change.
An IFile
should not have to know about the Properties view, which is a concept from the UI layer. This is precisely the coupling of model and user interface we want to avoid.
We need a mechanism that allows us to
Add a service interface to a type without exposing it in that type
Add behavior to preexisting types such as IFile
The pattern is called Extension Object[1] and is also known as Extension Interface. To quote its intent: “Anticipate that an object's interface needs to be extended in the future. Extension Object lets you add interfaces to a class and lets clients query whether an object has a particular extension.”
When implementing Extension Object, you have to answer several questions:
Do you want to extend an individual object or a class? With a class-based mechanism you add behavior to an entire class. You can only add behavior but not state, that is, no fields.
How is an extension described and identified?
The Eclipse extension support is class-based. That is, you can add behavior to existing classes, but not add state to its existing instances. The additional behavior is described by an interface.
The key interface of the Eclipse extension support is IAdaptable
. Classes that support adaptability implement this interface. While browsing the Eclipse source, you will find that IAdaptable
is a popular interface, as illustrated in the following snapshot of the Hierarchy view shown in Figure 31.3.
What is behind the IAdaptable
name? When doing some Eclipse code archeology we found the class was originally called IExtensible
. This captured the idea that the mechanism existed to support extending a type with additional behavior. Over time the name got changed to IAdaptable
to emphasize the fact that the mechanism enables adapting an existing class to another interface.
IAdaptable
is an interface with a single method that allows clients to dynamically query whether an object supports a particular interface:
Example . org.eclipse.core.runtime/IAdaptable
public interface IAdaptable { public Object getAdapter(Class adapter); }
The getAdapter()
method answers a particular interface. Callers of getAdapter()
pass in the class object corresponding to the interface (see Figure 31.4).
The class object is typically retrieved with a class literal (for example, IPropertySource.class
). GetAdapter()
could have taken a String
argument instead, but then the compiler couldn't check to see whether the interface exists at compile time. GetAdapter()
returns an object castable to the given class (or null if the interface isn't supported). Here is an example of how the Properties view queries the currently selected object for its IPropertySource
interface:
Example . org.eclipse.ui.views.properties/PropertySheetEntry
getPropertySource(Object object) {
//...
if (object instanceof IAdaptable) {
IAdaptable a= (IAdaptable) object;
return (IPropertySource)a.getAdapter(IPropertySource.class);
}
}
The IAdaptable
interface is used in two different ways in Eclipse:
A class wants to provide additional interfaces without exposing them in the API—. In this case, getAdapter()
is implemented by the class itself. Adding a new interface requires changing the implementation of getAdapter()
. This is useful when a class wants to support additional interfaces without changing its existing interface and thereby breaking the API.
A class is augmented from the outside to provide additional services—. In this case, no code changes to the existing class are required and the getAdapter()
implementation is contributed by a factory.
Let's first consider how a class can provide interfaces using IAdaptable
.
Here is an implementation of a getAdapter()
method in a class that supports the IPropertySource
interface:
Object getAdapter(Class adapter) {
if (adapter.equals(IPropertySource.class)
return new PropertySourceAdapter(this);
return super.getAdapter(adapter);
}
In PropertySourceAdapter
, you implement the IPropertySource
:
public class PropertySourceAdapter implements IPropertySource { private Object source; public PropertySourceAdapter(Object source) { this.source= source; } public IPropertyDescriptor[] getPropertyDescriptors() { // return the property descriptors. } //... }
Implementing getAdapter()
directly helps with the API evolution. If later on the class wants to provide an additional interface, say IShowInSource
, then we only have to augment the getAdapter()
method. No changes to the external interface of the class are required:
Object getAdapter(Class adapter) { if (adapter.equals(IPropertySource.class) return new PropertySourceAdapter(this); if (adapter.equals(IShowInSource.class) return new ShowInSourceAdapter(this); return super.getAdapter(adapter); }
Implementing getAdapter()
with conditional logic is simple. But since Extension Object is supposed to support unanticipated extension, conditional logic isn't the final answer. The next step is externally augmenting the interfaces supported by an existing type.
Let's consider how to add property-sheet support to IFile
without having to pollute IFile
with UI-specific behavior. A layer-preserving solution is to introduce a property-sheet wrapper for an IFile
. Let's go down this path for a while to see its problems.
public class FileWithProperties implements IPropertySource {
private IFile file;
public IPropertyDescriptor[] getPropertyDescriptors() {...}
public Object getPropertyValue(Object id) {...}
public boolean isPropertySet(Object id) {...}
public void resetPropertyValue(Object id) {...}
public void setPropertyValue(Object id, Object value) {...}
public IFile toFile() { return file; }
}
The class FileWithProperties
wraps an existing IFile
and implements the IPropertySource
interface. As an improvement, we could also implement the IFile
interface (which the API contract doesn't allow) and implement the wrapper using the Decorator pattern. This would make the use of the wrapper more transparent to clients. However, there is another problem. We now end up with two different objects representing the same IFile
. This introduces subtle complexities when it comes to testing files for equality. Life would be much simpler without any wrapping. Clients can just deal with IFile
but extend its behavior. This is exactly the purpose of adapter factories. Here are the steps for how to extend IFile
:
Implement an AdapterFactory
with the adapters you want to add to a particular type.
Register the factory for a specific type with the AdapterManager
. The AdapterManager
is provided by the Platform
class, a façade with static methods for general platform services.
In the implementation you have to declare which adapters the factory provides. This is done in the getAdapterList()
method. The purpose of this declarative method is to enable a quick lookup for an adapter. With this information, Eclipse can quickly decide whether a type supports a particular adapter.
An AdapterFactory
encapsulates the extensions you want to add for a particular type.
class FileAdapterFactory implements IAdapterFactory { public Class[] getAdapterList() { return new Class[] { IPropertySource.class }; } //... }
In our case, the factory contributes a single adapter for IPropertySource
. The other method is getAdapter()
. In contrast to the IAdaptable.getAdapter()
, it has an additional argument for the object that originally received the request:
public Object getAdapter(Object o, Class adapter) {
if (adapter == IPropertySource.class)
return new FilePropertySource((IFile)o);
return null;
}
Next we have to register the factory for our desired type (IFile
) with the AdapterManager
. This has to occur early enough so that calls to getAdapter()
return the correct result. This is commonly done in a plug-in's startup()
method or in a static initializer. Here is a a corresponding code snippet:
IAdapterManager manager = Platform.getAdapterManager(); IAdapterFactory factory = new FileAdapterFactory(); manager.registerAdapters(factory, IFile.class);
Figure 31.5 illustrates these relationships.
Now we have added an adapter factory. However, unless it gets an opportunity to participate in the getAdapter()
requests, we haven't made any progress. Therefore, the getAdapter()
invoked on IFile
objects has to enable adapter contributions from the adapter manager (the Invitation Rule). The IFile
interface is implemented in File
. Figure 31.6 illustrates its type hierarchy.
The class File
descends from Resource
, which descends from the class PlatformObject
. PlatformObject
implements IAdaptable
and forwards getAdapter()
invocations to the Platform
's adapter manager (see Figure 31.7). If you cannot derive from PlatformObject
, you invite others to contribute adapters by calling:
Platform.getAdapterManager().getAdapter(this, adapter);
Once we have done all this, clients can call getAdapter(IPropertySource.class)
without having to worry whether the interface is provided by the class itself or whether it was added externally.
Figure 31.8 shows the implementors of IAdapterFactory
in Eclipse.
Here are some points about the extension mechanism:
Multiple adapters for the same type—. What happens when the same adapter is registered more than once in a type's hierarchy? The rule is that the most specific adapter wins. Most specific means the first adapter in the base class chain followed by a depth-first order search in the interface hierarchy.
Stateless adapters—. Adapters that have no state are the most space-efficient and easiest to manage. You store a single instance of the adapter in a field of the adapter factory or a static variable and return it when it is requested. To reuse a single instance of the adapter for all adapted objects, the adapter methods need to support passing in the adapted object. IWorkbenchAdapter
is an interface that enables a stateless implementation. IPropertySource
is an interface that does not.
Instance based extensions—. IAdaptable
supports class extensions. How can you extend instances? IResource
supports dynamic-state extension with properties. A property is identified by a qualified name and can be managed either per session or persistently. Refer to the API specification of IResource
for more details.
Which adapters are supported?—. You cannot determine the available adapters from just looking at a class interface. You have to read the API documentation to find out which adapters are expected by a service (see for example, org.eclipse.ui.views.properties.PropertySheet
). Alternatively, search for references to IAdaptable.getAdapter()
to uncover all uses of adapter interfaces.
Adapter negotiation—. An IAdaptable
client can ask for different interfaces until a suitable one is found. For example, a property sheet can be populated from an IPropertySource
adapter. However, a client can also get full control over the Property Sheet view contents by providing an IPropertySheetPage
adapter. The Property Sheet view first queries for an IPropertySheetPage
adapter and if there isn't one then it uses a default Property Sheet page that queries for the IPropertySource
adapter.
Reduced programming comfort—. Programming with adapters is more bulky than programming with interfaces directly. With an adapter a direct method call is replaced by multiple statements:
IPropertySource source= (IPropertySource) object.getAdapter(IPropertySource.class); if (source != null) return source.getPropertyDescriptors();
Extension Object—implemented as IAdaptable
, IAdapterManager
, and IAdapterFactory
—furthers Eclipse's goal of supporting unanticipated extension by allowing contributors to extend the classes an object can pretend to be. While more complicated than just using objects, the extra complexity is balanced by the additional flexibility.
[1] See “Extension Object” in R. Martin, Pattern Languages of Program Design 3. Addison-Wesley, Reading, MA, 1998. For a comprehensive description with several other implementation variations, see “Extension Interface” in D. Schmidt, M. Stal, H. Rohner, F. Buschmann, Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects. John Wiley & Sons, 2000.