Search code examples
c#oopextensibilityplugin-pattern

C# Plugin pattern without interfaces


I've encountered the need to implement a plugin pattern that doesn't fit with anything I've seen elsewhere and I'm just wondering whether I'm looking at it the wrong way or whether anyone else has encountered the same problem and might have a soluton.

Essentially, we have a system that comprises of a core assembly, and a number of modules that plug into it. Some of the modules rely on other modules, but the need has arisen to potentially remove or replace some of those dependencies from time to time and I'd like to avoid recompiles as far as possible.

The system is a bespoke CMS and the modules are plugins providing features within the CMS. For example, we have a comments module and several content modules such as a news module, a blogs module etc. that can include commenting functionality. My problem is that some customers may not purchase the comments module, so I either need to find a way to prevent the dependent modules from depending on the existence of a comments module and, in some cases, may need to cater for a modified version of the comment module.

We're loading the modules at runtime and, at present, to avoid interdependencies between the modules, we're handling this using interfaces that are held in the core CMS assembly. My concern is that to avoid having to modify the core CMS assembly every time we create new modules where a dependency could exist, I need to use something a lot looser than interfaces and implementations of those interfaces.

I'm considering the following:

  • Core assembly contains an object that allows the registration and unregistration of shared input/output messages (for example "Comments.AddComment" or "Comments.ListComments")
  • When modules are loaded, they advertise the services they require and the services they provide (for example, a news module would require the "Comments.AddComment" message and any variant of the comments module would provide the "Comments.AddComment" message).
  • Any objects or data that are passed to these messages will inherit from a very loose base class or implement an interface that exposes a property of type IDictionary that is contained within the core assembly. Alternatively, the contract for a message will require only a parameter of type object and I pass anonymous objects into them from the provider/consumer.

The downside is obviously losing strong typing, but the plus is that I don't rely on a strict interface implementation or require the inclusion of modules that may not exist at runtime.

Plugins are loaded via Reflection, checking referenced assemblies and looking for classes implementing a given interface. MEF and dynamic types aren't an option as I'm restricted to .NET 3.5.

Can anyone suggest anything better, or perhaps a different way of thinking about this problem?


Solution

  • You're right that if you use a base class or interface in your core app, then you need to rebuild the app and all the plugins that use that class/interface if it changes. So what can you do about it? Here are some ideas (not necessarily good ones, but they may spark some thoughts) that you can mix & match...

    • Put the interfaces in separate shared assemblies, so you at least don't need to recompile the core app if an interface changes.

    • Don't change any of your interfaces - keep them fixed in stone. Instead "version" them, so if you want to change the interface, you leave the old interface in place and just expose a completely new interface that extends or replaces the old API. This allows you to gradually deprecate old plugins rather than forcing an immediate global rebuild being required. This does tie your hands somewhat as it requires full backwards compatibility support for all the old interfaces at least until you know all your clients have moved on to newer builds of all their assemblies. But you can combine this with a less frequent "reinstall everything" release where you break backwards compatibility, clear out the defunct interfaces and upgrade all the client assemblies.

    • Look for interfaces where some parts of the interface are not needed by all plugins, and break up some interfaces into several simpler interfaces, to reduce dependencies/churn on each interface.

    • As you've suggested, convert interfaces into a runtime registration/discovery approach to minimise the churn on the interfaces. The more flexible and generic your interfaces are, the easier it will be to extend them without introducing breaking changes. For example, serialize data/commands to a string format, dictionary or XML and pass it in that form, rather than calling explicit interfaces. A data-driven approach like XML or a dictionary of name+value pairs is much easier to extend than an interface, so you can start supporting new elements/attributes while easily retaining backwards compatibility for clients that pass an older format to you. Instead of PostMessage(msg) + PostComment(msg) you could genericise the interface to a single method taking a type parameter: PostData("Message", msg) and PostData("Comment", msg) - that way it's easy to support new types without needing to define new interfaces.

    • If possible, try to define interfaces that anticipate expected future features. So if you think you might one day add an RSS capability, then think about how that might work, chuck in an interface, but don't provide any support for it. Then if you finally get around to adding an RSS plugin, it already has a defined API to plug into. Of course, this only works if you define flexible enough interfaces that they are actually usable by the system when it is implemented!

    • Or in some cases maybe you can ship the dependency plugins to all your customers, and use a licensing system to enable or disable their capabilities. Then your plugins can have dependencies on each other, but your customers can't utilise the facilities unless they've bought them.