Chapter 31. Core Runtime — IAdaptable

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).

Core Runtime

Figure 31.1. Core Runtime

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.

Properties View

Figure 31.2. Properties View

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.

Extension Object/Extension Interface

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.

IAdaptable Type Hierarchy

Figure 31.3. IAdaptable Type Hierarchy

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).

Extension Object

Figure 31.4. Extension Object

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.

Surfacing 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.

AdapterFactories—Adding Interfaces to Existing Types

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:

  1. Implement an AdapterFactory with the adapters you want to add to a particular type.

  2. 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.

Register Adapter Factories with the Platform

Figure 31.5. Register Adapter Factories with the Platform

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.

IFile Type Hierarchy

Figure 31.6. IFile 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);
IAdapterFactory Returns Extensions for a Given Class

Figure 31.7. IAdapterFactory Returns Extensions for a Given Class

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.

IAdapterFactory Type Hierarchy

Figure 31.8. IAdapterFactory Type Hierarchy

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset