1. MonoDevelop has always allowed creating new types of projects by subclassing the Project class. A Project subclass can for example override the OnBuild and OnExecute methods to provide custom behavior for building and executing. In this way add-ins can provide support for new languages. That's what I call vertical extensions (I always envision inheritance as a vertical hierarchy)

    At some point I saw the need of allowing "horizontal extensions". For example, the gtk# designer needs to plug into the build process to generate the GUI code just before the project is compiled. Using a project subclass and override OnBuild is not possible in this case. We don't want to create a new project type, just add some behavior to all existing project types (well, maybe only those which can use the designer, but it is still an undetermined number of project types).

    The first quick and dirty approach to allow this kind of extensibility was to create a new extension point where add-ins could register implementations of an interface like this:
    public interface IBuildStep
    {
    ICompilerResult Build (IProgressMonitor monitor, Project project);
    }
    When building a project, the project would get a list of all IBuildStep implementations and would call Build() on each of them. In fact, the default implementation of the project build was just another IBuildStep implementation, so add-ins would be able to add build steps before or after the default project build operation.

    This design has the advantage of being very simple, but it has some important limitations. For example, an extension cannot have control over the sequence of execution. If an add-in wants to do something before and after the project build, it has to add two steps in the correct position of the pipeline, and it won't be easy to share data between them.

    Those limitations were not a problem for the gtk# designer add-in, but they became evident as I tried to extend the build pipeline for other purposes. So, I decided to rethink the design and do something more generic.

    The first idea was to use the "message sink" model on which Remoting is based. Using this model the interface would look like this:
    public interface IBuildExtension
    {
    ICompilerResult Build (IProgressMonitor monitor, Project project);
    IBuildExtension Next { get; set; }
    }
    The idea here is that the project service chains all extensions by setting the Next property of every extension to the next extension in the chain. Then it calls the Build method on the first extension, and the extension is supposed to do whatever custom operations it needs to do and then call Build on the next extension in the chain. In this model, an extension has more control on the build process because it can do whatever operation before, after, or in replacement of the build operation (it can decide to not call Next.Build at all, for example). This model is not as direct to implement as the previous model, because the extension has to handle the Next property, but still it's quite easy.

    One problem I find in this model is that it can't easily scale. The Build operation is not the only operation that makes sense to extend in a project. Other operations like Clean, Execute, Load and Save might also be extensible. One option is to add all those methods to a more generic IProjectExtension interface. The problem with this option is that if an add-in only wants to extend one operation it has to add dummy implementations for all methods. Another option is to create an IxxxxExtension interface for each operation. This solution is maybe better but it isn't ideal because it requires dealing with many interfaces and having one extension chain per operation. Every time you want to extend some operation you would have to find the interface that defines it, implement it in a class, find the correct extension point for that interface and register it.

    Also, I'm becoming lately a bit tired of using interfaces. Interfaces are very commonly used in extensible APIs: you define an interface and the extender implements it. Using interfaces has advantages, but also important limitations: you are forced to implement all methods (even if you are not interested in all of them), they can't provide a default implementation, and you can't add new methods without breaking the compatibility with old versions. I've seen many APIs which define interfaces without a clear idea of what benefit does using those interfaces have. In many cases interfaces are not really needed, and using a simple class is just enough. I think the build extension example is one of those cases.

    I finally designed the build extension more or less like this:

    public class ProcessServiceExtension
    {
    internal ProcessServiceExtension next;

    public virtual ICompilerResult Build (IProgressMonitor monitor, Project project)
    {
    return next.Build (monitor, project);
    }
    }
    So, to provide an extension to the build method you don't implement an interface, but a subclass of ProcessServiceExtension, and then you override the Build method. For example:
    public class MyExtension: ProcessServiceExtension
    {
    public override ICompilerResult Build (IProgressMonitor monitor, Project project)
    {
    // Do some custom processing
    bool res = base.Build (project);
    // Some post-processing
    return res;
    }
    }
    This class is similar to the previous IBuildExtension interface, but using a class has several advantages:
    • You don't have to implement the Next property. The base class does it. In fact, it is hidden to the extender. What I like about this model is that it allows implementing horizontal extensions just like if they were regular vertical extensions.
    • I can add new overridable methods to ProjectServiceExtension without breaking old extensions, and without having to define new interfaces or extension points.
    • ProjectServiceExtension can define overridable methods for all project operations, and extenders can override only the methods they are interested in.
    • ProjectServiceExtension can provide specialized overloads of the operations. For example, it could define generic Build(item) method, which would call a Build(project) method, or a Build(solution) method depending on the item type. Extenders interested in projects can just override Build(project), while extenders interested in more generic extensions which apply to all types of items can override Build(item).
    I'm getting addicted to this extension model. MonoDevelop is using it in several places, and so far I haven't missed the "benefits" of interfaces at all.
    7

    View comments

Blog Archive
About Me
About Me
My complete name is Lluis Sanchez Gual, and I work as a developer for Novell. I'm part of the Mono team, and I'm leading the MonoDevelop project, a very exciting open source IDE for GNOME.
Loading