Today’s clients seem to be getting more and more demanding regarding the flexibility of their applications and the speed in which modifications can be made.
In this article, I will show you how to design applications with extensibility points so that they will grow with the clients’ needs as well as provide a way to “swap” functionality in and out as needed.
Throughout my years in software development, there have been many concepts and paradigms introduced in writing code as well as many methodologies for defining how software should be developed. For the most part, each has built on the previous, enhancing the development process each time. Object-oriented programming redefined how we think of entities within an application and communicate with them. SOA showed us how to expose object-oriented entities in a way that they can service similar and dissimilar clients. Several years ago, the design-pattern craze hit the industry with the publishing of the famous GoF book (see sidebar “Additional References”). All these things put together have inspired the creativity of many developers, including me. I’m going to show you how to use your existing OOP and pattern knowledge to develop applications in a way that they can be changed and/or enhanced with minimum effort and in a clean, elegant, and efficient manner.
Extensibility Patterns
Throughout the course of this article, I’m going talk about three different patterns that I use to make my applications extensible. None of these is absolutely defined in any patterns manual, though each bears resemblance to one or more patterns in the GoF catalog.
Providers
This pattern has its roots in the Strategy pattern and it lets you design your data and behavior in an abstraction so that you can swap out implementation at any time.
Plug-Ins
This builds on the same abstraction design I’ll use in writing providers and lets you build sections of your site in swappable modules. In a way they are similar to providers but where you generally use providers to obtain information, you use plug-ins typically to perform tasks; though many will argue quite successfully that their definitions can be interchangeable.
Modules
Modules have their roots in the Chain of Responsibility pattern and take plug-ins to the next level by allowing you to define many plug-ins within one class, thus centralizing your extensibility capability in your application.
To better illustrate how the three patterns I’m going to show you evolve, I’ll start by describing a very trivial 3-step process that I’ll build on as the article progresses. I’ll code this process concretely first, and then start applying my three patterns to it. The 3-step process will involve obtaining the name of a text file, reading in string data from it, and logging that data to another file.
Doing it Concretely
I’ll first start by writing a class that will house the functionality I want for my 3-step process. I’ll call this class FileReader.
Here’s the interface for the FileReader class. You can find the complete code in Listing 1.
In VB:
Public Function GetFileName()
As String
Public Function GetFileData(ByVal file
As String) As String
Public Sub LogTextData(ByVal data
As String)
In C#:
public string GetFileName()
public string GetFileData(string file)
public void LogTextData(string data)
From what this code shows, the GetFileName method is returning the name of a file, which FileReader then sends into the GetFileData method to retrieve a string of data, which it then sends into the LogTextData method.
Now, say you were building an application that was going to run this little 3-step process. Integrating this into a form or a controller class of some kind would not be difficult at all and quite well accepted by most developers in the industry. Unfortunately, if anything ever changes with the way the application obtains a file name or the data, it would involve coming back into this code and changing it appropriately.
A client can use my FileReader class like so:
In VB:
Dim o_FileReader As FileReader = New
FileReader()
Dim s_File As String =
o_FileReader.GetFileName()
Dim s_Data As String = _
o_FileReader.GetFileData(s_File)
If s_Data <> "" Then
o_FileReader.LogTextData(s_Data)
End If
In C#:
FileReader o_FileReader = new
FileReader();
string s_File =
o_FileReader.GetFileName();
string s_Data =
o_FileReader.GetFileData(s_File);
if (s_Data != "")
{
o_FileReader.LogTextData(s_Data);
}
So what I’m going to do is abstract the interface and separate it from the implementation. A client application, be it a class or a form, will then communicate only through the abstraction I’m about to design. In the interest of even greater flexibility, I’m going to generalize this process even further. I’m going to go from:
- GetFileName
- GetFileData
- LogTextData
to:
- GetSource
- GetData
- LogData
Notice the “from” can fit easily into the “to”, though not vice-versa.
I came up with this by pretending to be the client, and asking myself exactly what do I need; then coming up with a more generic process that still has the capability of feeding me exactly what I needed. Now all I need to do is turn this into a provider model.
Providers
The provider model will allow me to define my three steps in an abstraction, be it an interface or an abstract class. In the interest of simplicity, I’m going to use an interface, so let’s start with that.
In VB:
Public Interface IDataProvider
Function GetSource() As String
Function GetData(ByVal source As
String) As String
Sub LogData(ByVal data As String)
End Interface
In C#:
public interface IDataProvider
{
string GetSource();
string GetData(string source);
void LogData(string data);
}
Now I have an interface to which I can apply any implementation I want; so long as it meets the signature and return types defined-basic polymorphism.
It is pretty customary to have a project set up for these abstractions. Then, later when you or somebody else wants to write a different implementation class, they need only to reference this one assembly.
When I first wrote my little 3-step process, I did so for a reason; my application had a need and that process filled it; that has not gone away. Only now, I want to accomplish it in a way that I can change it later and not touch my client application.
Initially I’ll have to change my client code, which currently instantiates and uses the FileReader class, but that should be the last time I have to touch that class. Remember I designed the interface in such a way that in the future, the “source” can change; it doesn’t necessarily have to be a file name. And the data obtained does not always necessarily have to come from a file. If this ever changes in the future, my design can accommodate it. The only requirement is that you must represent the “source” as a string and that you must use it to obtain string data, which you will later log.
So now that I have an interface that serves as my abstraction, I’m going to write my first provider based on it. Appropriately, I will call this class TextFileProvider and it will reside in its own project, producing its own assembly.
I’m going to include the method signatures for my class here, but you can find the full listing in Listing 2.
In VB:
Namespace Providers
Public Class TextFileProvider
Implements IDataProvider
Protected Function GetSource()
As String Implements _
Core.IDataProvider.GetSource
Protected Function GetData( _
ByVal source As String) As
String Implements _
Core.IDataProvider.GetData
Protected Sub LogData( _
ByVal data As String) _
Implements Core.IDataProvider.
LogData
End Class
End Namespace
In C#:
namespace Providers
{
public class TextFileProvider :
IDataProvider
{
string IDataProvider.GetSource()
string IDataProvider.GetData(
string source)
void IDataProvider.
LogData(string data)
}
}
The implementation for these methods in this class is identical to my original FileReader class. Because the signatures are also the same, it can plug quite nicely into my client application. So now I have to change my client for what will be the only time. Instead of directly instantiating a class like FileReader, the client will only communicate through the IDataProvider interface. I’ll take advantage of the app.config file to determine which implementation of IDataProvider I will use; of which so far I only have one: TextFileProvider.
I’ll keep the additions to the app.config file simple for this first example, so I’ll just use the <appSettings> section. For the later examples, I’ll use custom configuration sections though I will not explain how they work in detail; that is the subject of another article.
<add key="dataProvider"
value="Providers.TextFileProvider,
Providers"/>
The text in the value attribute is called standard .NET type notation. It is the fully qualified class (namespaces and all), a comma, and the assembly in which it resides. If the class is in the current assembly, that is the one hosting the app.config file, you can leave out the assembly name here. However that will be contrary to what I’m trying to solve here, and that is to NOT have to adjust the client application if I want to modify the behavior defined in the provider.
The assembly in which the provider class resides will NOT be added to the client in the conventional manner, by “Adding a Reference”. This assembly needs only to be “accessible” to the client. This means all I have to do is drop it into the client’s Bin folder.
Now that I have a place to go looking for what implementation of my provider interface I need to use, I’m going to remove the client’s usage of FileReader and insert my new code. I’ll do this in a couple of steps for clarity.
In VB:
Dim s_Provider As String = _
ConfigurationManager.
AppSettings("dataProvider")
Dim o As Object = _
Activator.CreateInstance( _
Type.GetType(s_Provider))
Dim o_Provider As IDataProvider = _
DirectCast(o, IDataProvider)
In C#:
string s_Provider =
ConfigurationManager.
AppSettings["dataProvider"];
Object o =
Activator.CreateInstance(
Type.GetType(s_Provider));
IDataProvider o_Provider = o as
IDataProvider;
The first line obtains the value from the app.config file, the type notation I described earlier.
The second line creates an instance of the class described in the string variable using a technique similar to the old CreateObject you used to use in Visual Basic 6.0. The CreateInstance method in the Activator class lets you instantiate an object when the object type is in string form. However, since the CreateInstance method does not know what it’s instantiating (since it’s in a string) it returns a standard object type.
The last task now is to get a type with which I can actually work. However, I don’t want to do this by referencing a concrete class; that would defeat my whole purpose. But since the client just needs a “source” in string form and “data” in string form, it should not care how it gets it; that’s why I created an interface, and it’s through that interface that I’m going to communicate. That’s what the third line of code accomplishes; it casts the object variable to the interface type.
I now have a variable, “o_Provider”, which I can use to obtain a source, obtain data, and log data. Since the type on which I ran the CreateInstance against is “Providers.TextFileProvider”, the interface methods will use the implementation I defined there.
In VB:
Dim s_Source As String =
o_Provider.GetSource()
Dim s_Data As String = _
o_Provider.GetData(s_Source)
If s_Data <> "" Then
o_Provider.LogData(s_Data)
End If
In C#:
string s_Source =
o_Provider.GetSource();
string s_Data =
o_Provider.GetData(s_Source);
if (s_Data != "")
{
o_Provider.LogData(s_Data);
}
So now, I’ve still met my application’s original requirements but can change the behavior easily and can even swap it out. Let me prove that to you.
Fast forward the clock six months. The company for which I wrote this application has decided that the data store that housed the data my little application is processing is now going to come from a database as opposed to from a text file. However, it will still be string data and my application will still need to log it.
Well, thanks to my provider model, all I need to do is to write a new provider class that implements my same interface and replace the entry in the app.config. The new provider, appropriately named TableDataProvider, is listed in full in Listing 3. So that I don’t have to prolong this article with unnecessary details, I won’t describe the layout of the database in use here, but if you read through the code, you’ll see that my “source” now went from the name of a file to a string used in a table. I use that string to return data from another table, so you see I still maintain the contract between the interface and the client, though the implementation is now very different.
By replacing the line in the app.config to look like this:
<add key="dataProvider"
value="Providers.TableDataProvider,
Providers"/>
The client can now continue to run without ever knowing that the source of the data changed.
Wrapping It in a Factory
As an added bonus, let me show you a really cool way you can wrap all the code that determines what provider to use, instantiates it, casts it, and accesses the interface members into a static (shared) factory class. This is the same pattern that ASP.NET uses with the Membership, Roles, and Profile classes. Let me describe the Membership class so you can get an idea about what I’m going to do for the application.
In ASP.NET, you can create a user using the Membership class by saying:
Membership.CreateUser( all arguments
here…
ASP.NET uses a provider model behind the Membership class in order to determine how it will create a user. When you execute a statement like the one above, you execute the static constructor of the Membership class. It is in here that the application reads the app.config and makes the decision as to what provider to use. By the time you call the CreateUser method, the class has a member variable that you’ve declared as the membership abstraction (in this case, a base class called MembershipProvider) and instantiated as the appropriate provider class as defined in the app.config. The CreateUser method will delegate the call to the CreateUser method of said variable, and then it executes the appropriate implementation. This is what I’m going to do with the provider model.
So here are the steps in my plan:
- Create a class that will contain static (shared) members only.
- Declare a class-level private static variable of type IDataProvider.
- In the static constructor, execute the code that will fill the static IDataProvider variable.
- Set up static methods that mimic those of the IDataProvider interface.
- In each of the methods, call the corresponding method in the IDataProvider variable.
If I call my class, AcmeFactory, it would look like this (note that Visual Basic modules behave like C# static classes):
In VB:
Public Module AcmeFactory
Sub New()
o_Provider = GetDataProvider()
End Sub
Private o_Provider As
IDataProvider = Nothing
Public Function
GetDataProvider() As _
IDataProvider
Dim s_Provider As String = _
ConfigurationManager.
AppSettings(_
"dataProvider")
Dim o As Object = _
Activator.
CreateInstance( _
Type.GetType(
S_Provider))
o_Provider = DirectCast(o,
IDataProvider)
Return o_Provider
End Function
Public Function GetSource() As
String
Return o_Provider.GetSource()
End Function
Public Function GetData() As String
Dim s_Source As String = _
o_Provider.GetSource()
Dim s_Data As String = _
o_Provider.GetData(s_Source)
Return s_Data
End Function
Public Sub LogData(ByVal data As
String)
o_Provider.LogData(data)
End Sub
End Module
In C#:
public static class AcmeFactory
{
static AcmeFactory()
{
o_Provider = GetDataProvider();
}
private static IDataProvider
o_Provider =
null;
public static IDataProvider
GetDataProvider()
{
string s_Provider =
ConfigurationManager.AppSettings[
"dataProvider"];
Object o =
Activator.CreateInstance(
Type.GetType(s_Provider));
o_Provider = o as IDataProvider;
return o_Provider;
}
public static string GetSource()
{
return o_Provider.GetSource();
}
public static string GetData()
{
string s_Source =
o_Provider.GetSource();
string s_Data =
o_Provider.GetData(s_Source);
return s_Data;
}
public static void
LogData(string data)
{
o_Provider.LogData(data);
}
}
Now in order to actually use the provider, the client simply has to do the following:
In VB:
Dim s_Data As String =
AcmeFactory.GetData()
If s_Data <> "" Then
AcmeFactory.LogData(s_Data)
End If
In C#:
string s_Data = AcmeFactory.GetData();
if (s_Data != "")
{
AcmeFactory.LogData(s_Data);
}
Notice that I’m not even calling the GetSource method here; instead I’ve programmed the GetData to do that for me in the factory class.
You can inject the provider model in many places throughout an application. If you ever find yourself looking at a piece of code and wondering if it ever may be subject to change, think about using this pattern there. If you ever find yourself looking at a process and wondering if it may be one day swapped out, removed, or enhanced, you may want to look at a plug-in model.
Plug-Ins
Plug-ins are very similar to the provider model and, in fact, can be considered the same model; just used in a different way.
Whereas you can use a provider to determine what implementation to use for a certain process, you can use plug-ins to inject functionality into a process that may be either swapped out later, removed, or be made to grow. It’s a little difficult to explain without showing you some code so let’s jump right in.
Suppose the need has arisen for me to further enhance my 3-step process with the ability to send out an e-mail to some managers regarding the data my process handled. I can very easily just place that functionality in the client just after I “log the data”, but what if I want to make that subject to change in the future? Or even better, what if I can anticipate that a manager will later ask me to do something else with the data even after I’ve sent out an e-mail, like archive it somewhere?
Instead of adding this and any other functionality to my client, I’m going to inject a plug-in model and have the client interact with that.
The path I’ll take to do this is very similar to that of the providers, but remember that earlier I did say that each of the patterns would be building on the previous.
The first step is to define an abstraction for my plug-in. I’m going to be calling this plug-in after I “log my data” so I’m going to call the interface appropriately, IPostLogPlugin. And I don’t really know what each plug in will do; however, I do know that it will do something with the data I just finished logging, so the interface will define a single method that receives string data in an argument.
In VB:
Public Interface IPostLogPlugin
Sub PerformProcess(ByVal data
As String)
End Interface
In C#:
public interface IPostLogPlugin
{
void PerformProcess(string data);
}
Now, before I actually write any classes that use this interface, I’m going to inject the client with code that run these potential plug-ins. Where I put this code is called a “point of extensibility”. And I’m going to put a point of extensibility directly after my logging code. Also, the code I’m going to insert will account for the possibility of more than one such plug-in being installed. Plug-ins that are written to my new interface get installed in the app.config just like the provider was, but these will go in their own configuration section called “postProcessing” and be read into a collection of objects that adheres to the details in the configuration section. Details on writing a custom configuration section is way beyond the scope of this article, but the code I’ll put here should be pretty self-explanatory.
In VB:
Dim s_Data As String =
AcmeFactory.GetData()
If s_Data <> "" Then
AcmeFactory.LogData(s_Data)
End If
Dim section As Object = _
ConfigurationManager.GetSection(" _
postProcessing")
Dim o_PlugIns As List(Of
PluginInfo) = _
DirectCast(section,
List(Of PluginInfo))
For Each o_PluginInfo As
PluginInfo In o_Plugins
Dim o As Object =
Activator.CreateInstance( _
Type.GetType(o_PluginInfo.
PluginType))
Dim o_PlugIn As IPostLogPlugin = _
DirectCast(o, IPostLogPlugin)
o_Plugin.PerformProcess(s_Data)
Next
In C#:
string s_Data = AcmeFactory.GetData();
if (s_Data != "")
{
AcmeFactory.LogData(s_Data);
}
object section =
ConfigurationManager.GetSection("
postProcessing");
List<PluginInfo> o_Plugins =
section as List<PluginInfo>;
foreach (PluginInfo o_PluginInfo in
o_Plugins)
{
object o = Activator.
CreateInstance(
Type.GetType(o_PluginInfo.
PluginType));
IPostLogPlugin o_Plugin = o
as IPostLogPlugin;
o_Plugin.PerformProcess(s_Data);
}
As you can see, I’m reading the collection of plug-ins into a variable called “o_PlugIns”.
I’m going to write two plug-in classes called EmailProcessing and ArchiveProcessing, and they’ll go into the app.config file like this:
<postProcessing>
<process name="email"
description="Send notification
emails"
type="PlugIns.
EmailProcessing,
PlugIns" />
<process name="archive"
description="Check for
archive needs"
type="PlugIns.
ArchiveProcessing,
PlugIns" />
</postProcessing>
Given the contents of this app.config section and the previous code, I’m looping through each plug-in definition, creating the class stored in the “type” attribute, and then casting it to the IPostLogPlugin interface. After which I simply call the PerformProcess method, sending in my string data.
What the plug-in classes do is unknown to the client, as is how many plug-ins are installed. All I have left to do is write my plug-in classes. I’m not going to actually show any implementation details for sending e-mails or archiving data but you’ll certainly get the idea of what’s going on.
In VB:
Public Class EmailProcessing
Implements IPostLogPlugin
Protected Sub PerformProcess( _
ByVal data As String) Implements _
Core.IPostLogPlugin.
PerformProcess
' take data and e-mail it
' somewhere
End Sub
End Class
Public Class ArchiveProcessing
Implements IPostLogPlugin
Protected Sub PerformProcess( _
ByVal data As String) Implements _
Core.IPostLogPlugin.
PerformProcess
' take data and archive it
End Sub
End Class
In C#:
public class EmailProcessing :
IPostLogPlugin
{
void IPostLogPlugin.
PerformProcess(
string data)
{
// take data and e-mail it
// somewhere
}
}
public class ArchiveProcessing :
IPostLogPlugin
{
void IPostLogPlugin.
PerformProcess(
string data)
{
// take data and archive it
}
}
As in the case of providers, the assembly in which the plug-in classes reside is not directly referenced by the client but merely has to be accessible (meaning just dropped into its Bin folder).
Though I’m not going to do it here, with a little creativity you can probably wrap some of the client’s work into a factory or helper class as I did in the provider model.
The plug-in model is great and you can use it to inject as many “points of extensibility” in your application as you want. And you can do this even if you don’t have any plans to write an immediate plug-in that you call at a specific point-just securing your application for the future. The drawback comes when you have more than one type of plug-in, which may each require its own interface. At this point, you may be giving your developers too many interfaces to try to remember. That’s where extensibility modules come in handy.
Extensibility Modules
Extensibility modules let you consolidate your plug-ins into a centralized class, giving all your developers one place to go to in order to figure out what kind of extensibility is available to them for a given application.
If you’re familiar with the concept of HttpModules, you’re going to feel right at home with what I’m about to talk about.
In writing plug-ins, I showed you how to write an interface for each type of plug-in; with the signature of the interface member(s) identifying the data that the plug-in class needed for its processing. In extensibility modules, there’s a little more work but it’s a lot more organized. I’m going to show you how to do this by writing what would otherwise be three different plug-ins. One serves the same purpose as the one in our plug-ins example, a “post log” plug-in, which I’m actually calling “post processing”. Another will be a “pre-processing” plug-in, and a third is a “check data source” plug-in.
The points of extensibility I’ll insert into my client will be as follows:
- The “Check Data Source” plug-in process will run immediately following the “GetSource” method in my provider. This execution will also determine if you should continue or not.
- The “Pre-Process” plug-in process will run just before you “log” the data.
- The “Post-Process” plug-in process will run immediately after you “log” the data.
The cool thing here is that I’m going to do this using just one interface. But because of that, I’m going to need an EventArgs-based class for each of my three plug-in scenarios. Like any conventional EventArgs-based class, each one will contain the data that I need to get from my client app into the “plug-in” process code and back.
In VB:
Public Class CheckDataSourceEventArgs
Inherits CancelEventArgs
Public Sub New(ByVal source As
String)
_Source = source
End Sub
Protected _Source As String
Public Property Source() As String
Get
Return _Source
End Get
Set(ByVal value As String)
_Source = value
End Set
End Property
End Class
Public Class PreProcessDataEventArgs
Inherits EventArgs
Public Sub New(ByVal data As
String)
_Data = data
End Sub
Protected _Data As String
Public Property Data() As String
Get
Return _Data
End Get
Set(ByVal value As String)
_Data = value
End Set
End Property
End Class
Public Class PostProcessDataEventArgs
Inherits PreProcessDataEventArgs
Public Sub New(ByVal data As
String)
MyBase.New(data)
End Sub
End Class
In C#:
public class
CheckDataSourceEventArgs :
CancelEventArgs
{
public CheckDataSourceEventArgs(
string source)
{
_Source = source;
}
protected string _Source;
public string Source
{
get
{
return _Source;
}
set
{
_Source = value;
}
}
}
public class
PreProcessDataEventArgs : EventArgs
{
public PreProcessDataEventArgs(
string data)
{
_Data = data;
}
protected string _Data;
public string Data
{
get
{
return _Data;
}
set
{
_Data = value;
}
}
}
public class
PostProcessDataEventArgs :
PreProcessDataEventArgs
{
public PostProcessDataEventArgs(
string data) : base(data)
{
}
}
As you can see, the PostProcessDataEventArgs is going to need the same data as the PreProcessDataEventArgs. To make things simpler, I’m just inheriting one from the other.
Now that I have an EventArgs class to carry information to and from each plug-in process, I’m going to create a class that will use these EventArgs classes. Basically this class, called ModuleEvents, will contain a set of properties, one for each plug-in process I want to define. The type for each property will be a delegate that defines a signature of the corresponding EventArgs-based class. The member variables that each property will wrap will be of the same delegate type. Does that sound confusing enough? Take a look at the code and it should clear things up.
The first thing I need is my delegate types, but in actuality I only need one, thanks to the wonderful world of .NET Generics:
In VB:
Public Delegate Sub
AcmeModuleDelegate(Of T) _
(ByVal e As T)
In C#:
public delegate void
AcmeModuleDelegate<T>(T e);
Now the property types can all use the same delegate type, but each with their own value for the generic. Let’s first create the member variables:
In VB:
Private _CheckDataSource As
AcmeModuleDelegate( _
Of CheckDataSourceEventArgs)
Private _PreProcessData As
AcmeModuleDelegate( _
Of PreProcessDataEventArgs)
Private _PostProcessData As
AcmeModuleDelegate( _
Of PostProcessDataEventArgs)
In C#:
private
AcmeModuleDelegate
<CheckDataSourceEventArgs>
_CheckDataSource;
private
AcmeModuleDelegate
<PreProcessDataEventArgs>
_PreProcessData;
private
AcmeModuleDelegate
<PostProcessDataEventArgs>
_PostProcessData;
Now for the public properties that expose the member variables:
In VB:
Public Property CheckDataSource() As _
AcmeModuleDelegate(Of
CheckDataSourceEventArgs)
Get
Return _CheckDataSource
End Get
Set(ByVal value As
AcmeModuleDelegate( _
Of CheckDataSourceEventArgs))
_CheckDataSource = Value
End Set
End Property
Public Property PreProcessData() As _
AcmeModuleDelegate(Of
PreProcessDataEventArgs)
Get
Return _PreProcessData
End Get
Set(ByVal value As
AcmeModuleDelegate( _
Of PreProcessDataEventArgs))
_PreProcessData = Value
End Set
End Property
Public Property PostProcessData() As _
AcmeModuleDelegate(Of
PostProcessDataEventArgs)
Get
Return _PostProcessData
End Get
Set(ByVal value As
AcmeModuleDelegate( _
Of PostProcessDataEventArgs))
_PostProcessData = Value
End Set
End Property
In C#:
public
AcmeModuleDelegate
<CheckDataSourceEventArgs>
CheckDataSource
{
get
{
return _CheckDataSource;
}
set
{
_CheckDataSource = value;
}
}
public
AcmeModuleDelegate
<PreProcessDataEventArgs>
PreProcessData
{
get
{
return _PreProcessData;
}
set
{
_PreProcessData = value;
}
}
public
AcmeModuleDelegate
<PostProcessDataEventArgs>
PostProcessData
{
get
{
return _PostProcessData;
}
set
{
_PostProcessData = value;
}
}
Now, an instance of ModuleEvents will contain properties that are delegates. As you know, a delegate can have one or more function pointers in its invocation list.
Now it’s time to write the interface that I’ll use to write plug-ins later. As I said earlier, instead of having a different interface for each plug-in, I’m going to have only one, which looks like this:
In VB:
Public Interface IAcmeModule
Sub Initialize(ByVal events As
ModuleEvents)
End Interface
In C#:
public interface IAcmeModule
{
void Initialize(ModuleEvents
events);
}
Notice that I have only one method, Initialize, and it receives an argument of the type ModuleEvents, which I just created. Now, the best way to continue is to write a plug-in using this new model.
From what you see so far, developers only have one interface they need to know about to extend this application. So I’m going write a new class in a new project called NotificationModule. Note that this class will perform the same process as the EmailProcessing plug-in I coded earlier. The class will implement the IAcmeModule interface and implement the Initialize method. In this method, I’ll access the events argument, whose properties are delegates. The IntelliSense itself will tell me what my possible extensibility points are. Since each one is a delegate, I can just wire a method in this class to that delegate, effectively adding to its invocation list.
In VB:
Public Class NotificationModule
Implements IAcmeModule
Public Sub Initialize(ByVal _
events As _
Core.ModuleEvents) Implements _
Core.IAcmeModule.Initialize
events.PostProcessData = _
New AcmeModuleDelegate( _
Of PostProcessDataEventArgs)( _
AddressOf
events_PostProcessData)
End Sub
Private Sub
events_PostProcessData( _
ByVal e As
PostProcessDataEventArgs)
' perform e-mail functionality
' with processed data
End Sub
End Class
In C#:
public class NotificationModule :
IAcmeModule
{
void IAcmeModule.Initialize(
ModuleEvents events)
{
events.PostProcessData +=
events_PostProcessData;
}
void events_PostProcessData(
PostProcessDataEventArgs e)
{
// perform e-mailing of processed data
}
}
As you can see, within the module class itself, I made the decision as to what extensibility point I want to tap into.
The client will also have to be changed. As opposed to having the client look for a specific plug-in type to instantiate, it will read in ALL modules and immediately loop through them and call their Initialize method. This will have built the invocation lists of any delegates I’ve wired in any of the modules.
In VB:
Dim section As Object = _
ConfigurationManager.GetSection( _
"dataProcessingModules")
Dim o_Modules As List(Of String) = _
DirectCast(section, List(Of String))
Dim o_FilterEvents As New
ModuleEvents()
For Each s_ModuleType As String In
o_Modules
Dim o As Object = _
Activator.CreateInstance( _
_Type.GetType(s_ModuleType))
Dim o_Module As IAcmeModule = _
DirectCast(o, IAcmeModule)
o_Module.Initialize(o_FilterEvents)
Next
In C#:
object section =
ConfigurationManager.GetSection(
"dataProcessingModules");
List<string> o_Modules = section as
List<string>;
ModuleEvents o_FilterEvents = new
ModuleEvents();
foreach (string s_ModuleType in
o_Modules)
{
object o =
Activator.CreateInstance(
Type.GetType(s_ModuleType));
IAcmeModule o_Module = o as
IAcmeModule;
o_Module.Initialize(o_FilterEvents);
}
Notice in this code that I declared the ModuleEvents object, o_FilterEvents, outside the loop. And that I sent the same object into each module. This way, by the end of the loop, the delegate properties in the object may contain zero or more method pointers.
All this happens somewhere at the beginning of my application. I still have to decide where within the body of the application I am going to insert extensibility points, just like I called my IPostLogPlugin-based classes from a specific point in the application.
Upon making these decisions, I’ll check for the appropriate property in o_FilterEvents that corresponds to the extensibility point I am in within my application. Checking said property for a value other than null (nothing) is enough to determine if there are items in the invocation list for the delegate. At that point, it simply becomes a matter of invoking the delegate. The following is the extensibility point I am inserting just after I obtained a “data source” (remember the GetSource method).
In VB:
Dim b_Cancel As Boolean = False
If Not o_FilterEvents.
CheckDataSource _
Is Nothing Then
Dim o_EventArgs As _
CheckDataSourceEventArgs = _
New CheckDataSourceEventArgs(
s_Source)
o_FilterEvents.
CheckDataSource.Invoke( _
o_EventArgs)
b_Cancel = o_EventArgs.Cancel
End If
In C#:
bool b_Cancel = false;
if (o_FilterEvents.CheckDataSource !=
null)
{
CheckDataSourceEventArgs
o_EventArgs =
new CheckDataSourceEventArgs(
s_Source);
o_FilterEvents.
CheckDataSource.Invoke(
o_EventArgs);
b_Cancel = o_EventArgs.Cancel;
}
Take notice of the Boolean variable I’m declaring before I fire off this event and setting to the Cancel value that comes back from my EventArgs class. I’ve designed this event so that the code you can inject has the option to set the Cancel property to true or false. An example of code that taps into this event would look like this:
In VB:
Public Class ProfanityFilter
Implements IAcmeModule
Public Sub Initialize(ByVal events
As _
Core.ModuleEvents) _
Implements
Core.IAcmeModule.Initialize
events.CheckDataSource = New
AcmeModuleDelegate(Of
CheckDataSourceEventArgs)
(AddressOf
events_CheckDataSource)
End Sub
Private Sub
events_CheckDataSource( _
ByVal e As
CheckDataSourceEventArgs)
If e.Source.ToUpper().
IndexOf("BAD") _
> -1 Then
e.Cancel = True
End If
End Sub
End Class
In C#:
public class ProfanityFilter :
IAcmeModule
{
void IAcmeModule.Initialize(
ModuleEvents events)
{
events.CheckDataSource +=
events_CheckDataSource;
}
void events_CheckDataSource(
CheckDataSourceEventArgs e)
{
if (e.Source.ToUpper().
IndexOf("BAD")
> -1)
e.Cancel = true;
}
}
This module taps into the CheckDataSource event and checks to see if the data source coming in contains the word “BAD”. If it does, it sets the Cancel property to true. Now this will make more sense if you go back to the code I added to the client (the one that worked with the b_Cancel variable). The code that would follow will check the value of the b_Cancel variable to determine if I should continue any further with any kind of processing.
Where it gets a little more complicated is in the case of more than one module tapping into this event. With the code that executes the Invoke method, it will fire all the modules that are wired to the CheckDataSource events, one after the other. Remember, this happens because my initialization routine sent and received the same ModuleEvents object to each module’s Initialize method. Now think of the order of events here (no pun intended). If I have three modules that tap into this event, each checking the data source for something different, the first one that gets executed will receive the initial value of the Cancel property, which is false. If the code in any module changes that value, the next module that the code executes will contain that new value, since the Invoke method is getting called once with the one EventArgs object. This means that it is up to me to code the module event to check the “e.Cancel” before I execute any of the logic in the event. In this case, a properly written module that taps into the CheckDataSource event should look like this:
In VB:
Private Sub events_CheckDataSource( _
ByVal e As CheckDataSourceEventArgs)
If Not e.Cancel Then
If e.Source.ToUpper().
IndexOf("BAD") _
> -1 Then
e.Cancel = True
End If
End If
End Sub
In C#:
void events_CheckDataSource(
CheckDataSourceEventArgs e)
{
if (!e.Cancel)
{
if (e.Source.ToUpper().IndexOf("BAD")
> -1)
e.Cancel = true;
}
}
If you code all the modules this way, as soon as any module that intercepts this event sets the e.Cancel property to true, no other module’s CheckDataSource event will process any code.
I do have another choice for this kind of logic, which is probably a bit safer. The previous examples put the responsibility of check for “cancel” in the module’s code. But not all your modules may necessarily be written by the same developer so it may not be reliable to do that. If I’m willing to write a little more code on the client, I can iterate through the invocation list of the delegate (in this case, CheckDataSource) and fire each one separately, checking the value of the Cancel property after each one and deciding whether to continue.
In VB:
Dim o_InvocationList() As _
[Delegate] = _
o_FilterEvents.CheckDataSource. _
GetInvocationList()
For Each o_Filter As
AcmeModuleDelegate( _
Of CheckDataSourceEventArgs) In
o_InvocationList
If o_Filter IsNot Nothing Then
Dim o_EventArgs As New _
CheckDataSourceEventArgs(
s_Source)
o_Filter.Invoke(o_EventArgs)
If o_EventArgs.Cancel Then
b_Cancel = True
Exit For
End If
End If
Next
In C#:
Delegate[] o_InvocationList =
o_FilterEvents.CheckDataSource.
GetInvocationList();
foreach (
AcmeModuleDelegate
<CheckDataSourceEventArgs>
o_Filter in o_InvocationList)
{
if (o_Filter != null)
{
CheckDataSourceEventArgs
o_EventArgs = new
CheckDataSourceEventArgs(
s_Source);
o_Filter.Invoke(o_EventArgs);
if (o_EventArgs.Cancel)
{
b_Cancel = true;
break;
}
}
}
In this technique, as soon as one module sets its e.Cancel property to true, I break out of the loop and stop further processing. At this point, in either of the two techniques, the b_Cancel variable will determine if I should continue processing all additional code.
You can insert all additional extensibility points in either of the two methods: the one that calls all module events in one Invoke execution or the one that iterates through the invocation list of the corresponding delegate and invokes one event at a time.
Extensibility modules are a great way to put multiple points of extensibility in your client and, at the same time, centralize how you write the extensibility plug-ins. You can choose to develop a class for each event you are going to tap into or you can group several together. If you’re going to choose the latter, you should do so because the interception code you’re placing into the various events is somehow logically related from one event to another. An example of this would be in writing a Profanity Filter. Such a filter may want to check for profanity at different points in your application. Since I created an extensibility point called CheckDataSource and another called PreProcessData, I can check for profanity at both of these points by writing one module class that taps into both of these events. The events will still be called from their appropriate extensibility points within the client, but they will be housed in one class, logically grouping them together. You can see the entire code of such a module in Listing 4.
Conclusion
These patterns can help you design and code applications that are easier and more elegant to enhance or modify. They also introduce a style of programming that’s based on the ability to interchange functionality easily. However, don’t feel that you have to spend too much time on deciding where to insert points of extensibility in your application-just keep it all in mind. As you design and develop applications, you will more than likely have points that will snap out at you. Try to resist over abstracting at the beginning and don’t be afraid to refactor into it later; but beware, once you get the hang of using these patterns, you will be addicted.