Once the problem of loading plugins is solved (in .NET through MEF in out case), the next step to solve is the communication with them. The simple way is to implement an interface and use the plugin implementation, but sometimes the plugin just needs to extend the way the application works and there may be a lot of extension points.
My question is about how to deal with that extension points. I've seen different ways of doing that but I'm not sure of the pros and cons of each one and if there are more and better ways to accomplish this:
- Events: Adding static events to all the stuff we want to make "extendable". For example, if I want to add custom validation for a User class, I can add a OnValidation static event handler and add events to it from the plugin when it's constructed.
- Messaging: Having a bus and a messages. The plugin can subscribe to a particular message and do stuff when some other class publishes that message. The message should contain the context in which the plugin can work. In the validation case, the logic layer will publish a UserValidation message and the plugin will act when the message is received.
- Interfaces: The host application is responsible of calling all the plugins that implement certain interfaces and give them the context of the current operation. In the case of validation, the plugin may implement a IValidator or IUserValidator with a Validate(object context) method.
Have you ever user one of the exposed approaches? Which one worked best for you?
And before you ask, our application is an extensible core (user, rola and content management) to build our client specific content centric web applications on top of that. Everything built on ASP.NET MVC.
A key for your design decision is to analyze and get a clear picture of how different the plugins will be from eachother.
E.g. when dealing with static events, you will probably have to define each event as some form of token, enum, object etc. Having to define a new set of events for each plugin naturally works against your whole design, particularly in terms of loose coupling and reuse.
If your plugins are very different you might benefit from having a bus/messaging architecture since you in such case can introduce domains/categories of communication exchange, which the plugins can subscribe to. I.e. a range of events and messages can be in a certain interest domain. Note here that communication within a certain category can still utilize static events, so those two alternatives are not mutually exclusive.
Direct interfaces implemented by the plugins is in my experience the strictest approach of plugin architecture. Extending the plugin interface usually implies code modification at both plugin and provider. You need to have a solid general interface which you know your application can live on for quite some time.
It may be easier for you to deal with the design by breaking it down into two aspects - communication channel and protocol. Static event handling is a protocol issue, while bus-messaging and direct interfaces is a channel issue.
Generally I would say that the protocol is the hardest to design correctly from the beginning, since you may not have a solid feel for how general or specific you can draw the line.
EDIT: Lars made an important point in his comment - if your platform supports exceptions, you can centralize a lot of the error handling when using direct interfaces, relieving the plugins from having to handle errors that are generic and perhaps outisde their particular domain (e.g. "plugin load error", or "file open failed"). However, such benefits will seem to fade if you have to maintain interfaces each time you add plugins. Worst case is when the interfaces start becoming inconsistent along the way because you didn't realize what they should support from the beginning. Refactoring the entire interface design when a substantial amount of plugins already have been conceived is not an easy task.
I'd go with the Observer pattern. From the GOF:
Define a one-to-many dependency between objects so that when one object changes state all its dependents are notified and updated automatically.
Also known as publish-subscribe I would suggest it most closely matches case two in your examples.