2010-02-12

Dependency Injection in e4 - just why do we want to do that?

Lately, I have been giving a number of presentations of Eclipse e4. The focus has been on the changes that we will see when we program for e4 rather than 3.x.

With all the presentations, there have been a very lively discussion on the pros and cons of the new technologies in e4, but one subject in particular have been discussed: the use of Dependency Injection (DI) and what it will mean for productivity, testing and the tooling in Eclipse we all have grown so used to.

The following examples are taken from the excellent e4 contact manager by Kai Tödter that shows off some of the interesting features of e4 including theming and DI. The manager is very e4'ish and use the e4 features as these are probably meant to be used - with other words, I don't fault Kai for the examples or the problems I find in the examples.

At first glance the use of DI seems to give some very understandable code and it has the potential to do away with some of the listener code that we see in many places in 3.x code.

E.g. to follow the current selection in a view - here just expecting a single selected object - you would do something like the following in 3.x (forget about removing the listener again...):

public class DetailsView extends ViewPart {
  @Override
  public void createPartControl(Composite parent) {
    // ...

    final ISelectionService ss = getSite().getWorkbenchWindow().getSelectionService();
    ss.addPostSelectionListener(myListener);
    myListener.selectionChanged(null, ss.getSelection());
  }

  protected void setSelection(Contact contact) {
    // ... do something with the selection
  }

  private final ISelectionListener myListener = new ISelectionListener() {
    @Override
    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
      if (!(selection instanceof IStructuredSelection)) {
        setSelection(null);
        return;
      }

      final IStructuredSelection ss = (IStructuredSelection) selection;
      if (ss.isEmpty() || !(ss.getFirstElement() instanceof Contact)) {
        setSelection(null);
        return;
      }
      setSelection((Contact) ss.getFirstElement());
    }
  };

  // ...
}

whereas, you will just do something like this in e4:

public class DetailsView {
  @Inject
  public void setSelection(@Optional @Named(IServiceConstants.SELECTION) Contact contact) {
    // ... do something with the selection
    
  }

  // ...
}

So far, so good. A lot less code compared with the 3.x version and the intension is quite clear once you know the injection is persistent, so changes in the context is automatically propagated to the view.

The main difference between the two versions lies in the way that you find and access services: in 3.x you basically have a Java method chain - here getSite().getWorkbenchWindow().getSelectionService() - where the e4 version uses a magic class or string - here @Optional @Named(IServiceConstants.SELECTION). The general Java tooling helps us access the services in 3.x, but what type of tooling will help us in e4? And how will we get warned about accidental changed in the e4 string?

A much more problematic example - as far as I am concerned - can be found in the SaveHandler of the contact manager. This handler is used to save the current contact in the details view by delegating the save functionality to the doSave method of the view (Again, I have cut away some irrelevant stuff):

public class SaveHandler {
  public void execute(IEclipseContext context, @Optional IStylingEngine engine,
      @Named(IServiceConstants.ACTIVE_SHELL) Shell shell,
      @Named(IServiceConstants.ACTIVE_PART) final MContribution contribution) throws InvocationTargetException,
      InterruptedException {
    final IEclipseContext pmContext = EclipseContextFactory.create(context, null);

    final ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
    dialog.open();

    dialog.run(true, true, new IRunnableWithProgress() {
      public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
        pmContext.set(IProgressMonitor.class.getName(), monitor);
        Object clientObject = contribution.getObject();
        ContextInjectionFactory.invoke(clientObject, "doSave", pmContext, null);
      }
    });
    // ...
  }
}

Basically the handler creates a new context for the operation, opens a progress dialog, installs the progress monitor in the context, and then delegates to the doSave method with the new context.

The problem is found in the inner run methods: the name of the method of the view that is used to do the save operation itself is hard-coded as a string. How is this ever going to work with the Java tooling? How can we get quick assists or refactoring to work here? And how will we maintain this?

There are also some other problems with regards to testability. E.g. if a new doSave is ever created in the view, this will not provoke either a compile-time or a run-time error, but instead just execute both methods.

In the 3.x version, I would let the views that supports the doSave method implement a common Java interface - e.g. IDoSaveable and then test for this in the handler. An example of how this can be done is shown below:

The interface:

public interface IDoSaveable {
  void doSave(IProgressMonitor monitor);
}

The view:

public class DetailsView extends ViewPart implements IDoSaveable {
  @Override
  public void createPartControl(Composite parent) {
    // ...
  }

  @Override
  public void doSave(IProgressMonitor monitor) {
    // ... do save
  }
}

The handler - note that this version uses adaption, but you could use instanceof instead:

public class SaveHandler extends AbstractHandler {
  @Override
  public Object execute(ExecutionEvent event) throws ExecutionException {
    final IWorkbenchPart part = HandlerUtil.getActivePartChecked(event);
    final IDoSaveable saveablePart = (IDoSaveable) Platform.getAdapterManager().getAdapter(part, IDoSaveable.class);
    if (saveablePart != null) {
      final Shell shell = HandlerUtil.getActiveShellChecked(event);
      final ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
      dialog.open();

      dialog.run(true, true, new IRunnableWithProgress() {
        public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
          saveablePart.doSave(monitor);
        }
      });
    }
    return null;
  }
}

Personally I see many advantages with this version:
  • It is actually more readable, as it has very little magic - aprt from the use of the adaption
  • It is easy to find all the views that supports the save operation.
  • Everything is based on the Java tooling and we have a no magic constants of any sorts
  • Refactoring works!
  • The activeWhen expression for the handler can simply test whether the activePart adapts to IDoSaveaeble.
What do you think? Is the new magic DI code worth the lost functionality in the Java tooling? Can we invent some new tooling to make up for the lost functionality? 
    Post a Comment