Wed 20 Feb 2008
Extensibility is very important to me. I like to work with architectures that allow you to write classes that implement an interface, then have those classes be “discovered” and instantiated into working extensions of the existing system. Maybe that’s why I love working on plug-ins for Visual Studio… it’s fun to get into the guts of an application and test the limits of what you can do within it. In addition, I love “object discovery”, which to me is what plugging in your code is all about. I want to be able to write a plug-in, put the compiled assembly in a specific directory and have it be found automatically.
When it comes to extensibility, SlickEdit benefits from the fact that it is largely written in macro code, which can easily be loaded dynamically. Unfortunately, in our Tools product, extensibility is a challenge because all of our product is compiled code. Two of the features of .Net that I rely on to “discover” objects that plug into our architecture are reflection and dynamic loading. Reflection, a term borrowed from Java, refers to the ability to dynamically load an assembly (a .Net or Java compiled binary) and query that assembly’s metadata about the types it contains. In other words, if I’m looking for a class called Foo, I can load any .Net assembly and traverse its types looking for the Foo class.
Looking for a specific class is pointless for our purposes, though, because we want to dynamically discover unknown objects that fit the description of a plug-in type. To do this, we’ll define an interface called IDoSomething. IDoSomething defines a “get” property called Name and an method called DoSomething().
Any class can implement IDoSomething. One of the beauties of interfaces is that they do not have to obey the law of single inheritance, which states that a class may only derive from at most one other class. Classes in .Net and Java may implement as many interfaces as they choose. That makes interfaces very good candidates for defining the contract between a plug-in class and the framework that creates an object of it. Instead of using reflection to find a specific class, we can use reflection to find classes that implement a specific interface. Therefore, if we scan [n] assemblies and find two classes that implement IDoSomething, we have found two types that are candidate plug-ins.
Our main application does not need to have ever seen these types before because we aren’t binding to these objects through their class interface, we’re binding to them through their IDoSomething interface. Our main application will never know the real identity of these two discovered objects, it only knows them by their IDoSomething interface.
This workflow of dynamic object discovery allows us to set up a pattern for plug-in types. We can create a factory class, called PluginFactory, that scans a set of assemblies for objects implementing a specific interface (IDoSomething in this case).
The main application will create the plug-in factory and will determine which assemblies should be scanned for plug-ins (this is usually all assemblies in some pre-configured directory). The scanning for plug-ins will then be initiated. During the scanning process, when it it finds a class that implements IDoSomething, the plug-in factory object will do the following:
- Create an object of it
- Get the object’s logical name via the Name property
- Get the object’s type
- Hash that type using the Name property value
Once the scan is done, our application can query the factory object for the list of logical names reported by the objects discovered during reflection. The UI can be populated with this list, either as a combo box or a dynamic menu, to show all available plug-ins.
When the user selects a plug-in item from this list, the application uses the name to ask the factory for an instance of the object it represents. The factory provides a function called CreatePlugin(string) that takes the logical name of a plug-in type and finds that type in the hash table created during the initial reflection scan. Once the type is found, the factory uses Activator.CreateInstance() method in .Net, or the Class.newInstance() method in Java, to create an instance of that object. The main application has no idea what type of object it has… it only knows that that object implements IDoSomething. The main application can now call DoSomething() on that object and the object will do its specific task.
This implementation is very simplistic. However, the concept, like all patterns, is universal. We can even make the factory class generic by implementing it as a generic class in .Net, or a template in Java. We can now create a factory to scan assemblies for any interface. The one dependency is that the interface supports a “get” Name property, since we rely on that property in the reflection scan.
The plug-in factory approach can also lead to trouble, because it opens a doorway to the system which can be exploited by plug-in objects that behave maliciously. For instance, imagine an object whose DoSomething() implementation was to format the C:\ drive. It’s an extreme example, but a serious case to consider.
Reflection and dynamic discovery can be a powerful way to make an application or architecture more flexible. Flexibility and extensibility are key components to any software architecture. In my next blog post, I’ll expand on this technique to describe how to implement UI configuration of dynamically discovered objects.
[i] Shmoo appeared in Al Capp’s newspaper comic strip Li’l Abner and is a registered trademark of Capp Enterprises, Inc..