After creating a plug-in architecture in .NET based on interfaces, attributes and reflection a while ago, I'm now doing something that looks like it will require a similar approach. This post will discuss some of the things I've learned from the first time around and what I'll do to make my next plug-in framework as robust as possible.
If you're familiar with design patterns then think of a plug-in architecture as an application of the Strategy pattern in the most flexible way, where the actual strategies are implemented in seperate packages from the context, which can then discover and use them at runtime. For instance, when creating a wordprocessor, you might want to add additional formats for the application to load and/or save over time. The typical way to implement this is by defining an interface for an implementation of an algorithm to adhere to. Next you just look for classes that implement this interface - or have any class handed to you and accept it as long as it implements the interface - and use its methods to complete the task. To truly make the system pluggable, you'll want to eliminate recompiles just to add new algorithms by allowing the system to dynamically load classes supporting your plug-in interface at runtime. Once you do that, you've got a plug-in system.
First, when you get the path to an assembly handed to you (for instance, through an
InstallPlugin-method), how do you determine what classes inside it are plug-ins? You need to define an interface that you use to communicate with the plug-in, so all you need to do is look for classes that implement this interface. This is not the ideal method however. Ofcourse plug-in classes must implement an interface, but using that as the only criterium to determine whether it's a plug-in is a bad idea. In the past I've created plug-ins that were actually composites of other classes that all behaved like plug-ins, but just the main composite was actually supposed to be used by the application. Looking for classes implementing the interface would have forced me to define the same interface with a different name just to hide those other classes. Not to mention that having two seperate interfaces that should be identical is just a bunch of strange maintenance bugs waiting to happen. So besides implementing the interface, look for an attribute first. Just declare a
PluginAttribute and require plug-in classes to be decorated with it.
There's another advantage to using attributes however: instantiation costs. These attributes can contain a lot of information, such as display name, description, notes, all kinds of filter data to determine when to use them, etc. This means that if you have a list of available plug-ins, all you have to do upon discovery is remember where they are, you don't have to actually instantiate them to retrieve information to show the user of your application. Remember however that you can't just unload an assembly from memory unless you unload the entire application domain. This is actually not so bad because you'll want to load external code into another application domain anyway, to prevent it from taking down your entire application if it messes up. Each plug-in can get its own application domain.
Discovery, instantiation and initialization
On to the instantiation. Where you keep the actual references depends on the application, I usually have a list of objects that contain location information and metadata. When you actually do the different types of work associated with instantiation however is a big deal: make sure you seperate discovery from instantiation and seperate that from initialization. Why? Suppose that during startup, you load all available plug-ins (either by retrieving a bunch of names from a config-file or looking for assemblies in a specific location), instantiate all those classes and call all of their initialization methods (if you do this the constructors may as well be used for all initialization work). Now suppose in the user interface, some functionality normally available and provided by a plug-in is missing. The user will wonder what exactly is the matter. Is the file missing? Can't the application find it? Or maybe this version of my application doesn't work with the plug-in? Perhaps there is some .NET-version mismatch? Or is some resource required for initialization missing? Because everything is done in a row, it's impossible for the end-user to determine what went wrong. Detailed error messages might help with this, but most users I know just call me up to say that "there was an error" instead of actually reading what the error message says, so it's much easier to make sure these types of things are done at different times.
Do the discovery during start-up. That way you know that if a plug-in shows up in the list of available ones that the pathname is good and the assembly it's in could be loaded. When to do instantiation and whether to seperate it from initialization at all depends on the nature of the application and its plug-ins. If initialization is risky (for instance, it requires an online connection or does some communication with an external device), it's best to put this off until the user actually wants to do something that requires the use of the plug-in. Do the instantiation first, show the user that it was a success ("plug-in loaded") and that you're now initializing.
Plug-ins will have to reference your application to be able to implement your interface and decorate themselves with your attribute. However, you don't want to force plug-in makers to recompile their plug-ins whenever you release a new version of your application, so make sure the interface definitions are in a seperate assembly: since interfaces don't implement anything, these will be relatively stable. Only when you want to actually change the plug-in interface should these plug-ins be recompiled and since the plug-ins will probably have to actually be modified then anyway, that's the right time to do it. So decouple the application from its plug-ins by creating a seperate interface library. Note that this is all a lot less of an issue if you don't use strong-named assemblies. Especially with plug-in systems however, you'll want users to be able to verify who created the plug-ins they intend to use, so strong-naming is often a must.
That's not all there is to it though, because there are bound to be some actual classes that plug-ins will want to use as well. For starters there's the attribute class that they all need, but more often than not, some classes are defined that facilitate communication between application and plug-in. You can't put them in the application assembly because that will make the plug-in dependent on the application implementation again. Putting them in the interface library will make it a lot more likely that this assembly will receive more changes than you might want: classes that actually do something, as opposed to interfaces, often contain bugs and will need to be updated. My choice is usually to put simple classes that are required by plug-ins in the interface library, for instance the attribute class that won't do much but manage a couple of properties. All the other supporting classes should go in a seperate assembly, so that when bugs are fixed in them, only its users will need to be updated as well.