The use of plugins in the design of software provides an elegant and popular solution for a lot of common architecture challenges, especially when dealing with a set of several similar but different components. The encapsulation of functionality into a plugin is intended to make it easy to manage and define the behavior of a component without modifying the calling framework. It also helps enforce a number of other best practices concerning the modularity and scalability of an application. To illustrate this, we'll be taking a look at designs that make use of a factory pattern and LabVIEW Classes, concepts we'll spend more time on in a minute.
For LabVIEW programmers, plugins can provide an especially scalable mechanism for large systems that have to interface with a wide variety of hardware - especially when the hardware may vary across different systems, or has to be regularly changed or added to. Because of this, a plugin approach can help mitigate the costs and risks associated with hardware that becomes obsolete and has to be replaced, making plugins an integral part of almost any Hardware Abstraction Layer (HAL). This also makes it possible for development to proceed concurrently on separate parts of the application with minimal or no risk of impacting unrelated parts of a project, something which is especially useful when dealing with large teams.
Another example of the benefit of a plugin-based approach is a complex user interface such as an options dialog or a wizard-like dialog. As an example, consider the LabVIEW options dialog, which makes use of plugins. It displays different interfaces depending upon what software is installed. Like the hardware scenario, you have a set of similar objects that have different functionality and you want to be able to change, add or remove functionality without modifying any code that is above it in the application hierarchy. This brings me to a very important point... a plugin architecture inverts the dependency you would see in a statically linked application. This is typically referred to as the dependency inversion principle. This principle states (source: wikipedia)
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
To understand the dependency inversion principle, consider a large, monolithic application whose dependencies are entirely statically linked. Top level components are dependent on the VIs it calls, which are dependent on the VIs they call. As a result, modifying the lowest-level component can create changes that propagate up and at the very least, require recompiling the entire application or rebuilding the executable. If the code is entirely statically linked and monolithic, it tends to expose developers to additional risk when making changes because standard APIs and communication with other components has not been defined in an abstract way. In a well designed application, the dependency is inverted such that a set of low-level components (ie: plugins) are actually dependent on the calling framework through standard APIs and communication mechanisms. As long as these are maintained, the plugins can be modified and developed without impacting any callers. These same abstractions and APIs make it equally possible to modify the framework without recompiling or changing the plugins - again, this relies on those abstractions remaining the same.
So how do design an application to use plugins in LabVIEW? Before jumping to a solution, there are several very important things to consider (this is not an exhaustive list):
will they be loaded upon initialization or during execution? Is a delay at the start of the application okay, or should it be a background or on-demand process?
what information will the framework need to send the plugins? Will commands dispatch a VI or send a commandthat the plugin handles?
what information will the plugins need to send to the framework? Should commands return an acknowledgement?
will messages need to be broadcast to all the plugins? If the number of plugins is potentially large, queues might not be an ideal communication mechanism. Consider user events.
how much functionality will be reused across plugins? Consider putting this functionality in a parent of a class hierarchy, allowing all children to easily make use of it.
how will plugins be installed and organized? Should they reside in a folder on disk, set a registry variable or be built into the exe?
should users be able to modify plugins? What deployment format should be used?
And perhaps most importantly, how do you define what goes into a plugin versus what is owned by the framework? The answers to all of these will obviously vary based upon your specific use case, but there are some common approaches that we will look at. For starters, plugins are almost always going to be loaded dynamically, be it at initialization or at the request of some operation during execution. In general, the act of loading a plugin is either done by bringing a VI or set of VIs into memory, or by loading a class. All of them make use of the Factory Design Pattern, making it one of the most important design patterns to familiarize yourself with.
LabVIEW Classes are an especially powerful way to create plugins. If the plugins all need to make use of some similar functionality, this can easily be encapsulated in the parent and reused by the various plugins. If plugins need to further customize or extend the default behavior, they can override the dynamic methods. Instead of dynamically loading a new VI for every dynamic method, the use of a class hierarchy means that the act of loading a new child brings all the associated methods into memory, which can then be dynamically dispatched. The image below shows the simplified UML for the UI Plugin Framework's plugin class hierarchy. Note that the 'Generic Plugin' class defines functionality that can then be reused by all of the children. All of the children override the 'Configure Plugin' method, but they selectively override the other methods, depending upon what they need to do.
I've uploaded several examples of a plugin framework to this community page for you to explore.
Basic Subpanel Options Dialog - this is a very simple illustration of how you can dynamically load VIs as plugins. In this case, the plugins are a VI which is displayed in the subpanel, much like the LabVIEW Options dialog.
LabVIEW Benchmarking Utility - this is a utility that allows users to create and add their own benchmarks for comparing LabVIEW and executables written in other languages. Each benchmark is a plugin.
User Interface Plugin Framework - this plugin framework shows how LabVIEW classes can be used to represent the individual plugins. This framwork uses Dynamic User Events to broadcast or unicast messages to registered listeners, eliminating the need for individual queues. It also makes use of an API that is defined by the parent class of the plugins, which is then reused by the various children.
Hardware Abstraction Layer - this simple example loads different plugins depending upon the configuration that is specified in an XML file. It abstracts the measurement tasks in avery simple test application to exemplify a hardware abstraction layer.
In the next entry, I'll discuss the use of User Events for sending messages to the plugins.