devxlogo

Design for Extensibility

Design for Extensibility

oday’s customers demand easier-to-modify and more flexible applications than ever before. 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, many concepts and paradigms have been introduced to improve the process of writing code and defining how software should be developed. For the most part, each has built on its predecessors, enhancing the development process each time. Object-oriented programming redefined how developers think of and communicate with application entities. SOA showed how to expose object-oriented entities so they could service both similar and dissimilar clients. And the design-pattern craze hit the industry with the publishing of the famous GoF book. All together, these things 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 lets you change and/or enhance them with minimum effort and in a clean, elegant, and efficient manner.

Extensibility Patterns
You’ll see information and examples of three different patterns that I use to make my applications extensible. None of these patterns 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 (see the sidebar “Providers in the Framework” for more information about how the .NET Framework uses this pattern).
  • Plug-Ins. This pattern 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 plug-ins are similar to providers; however, where you generally use providers to obtain information, you typically use plug-ins 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 the extensibility capabilities in your application.

To illustrate how the three patterns evolve, I’ll start by describing a very trivial three-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 three-step process involves obtaining the name of a text file, reading in string data from it, and logging that data to another file.

A Concrete Example
I’ll first start by writing a class that will house the functionality I want for my three-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)

This code shows that the GetFileName method returns the name of a file, which FileReader then sends to the GetFileData method. GetFileData retrieves a string of data, which it sends in turn to the LogTextData method.

Now, assume you were building an application to run this little three-step process. Integrating this into a form or a controller class of some kind would not be difficult at all, and would be 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 as follows:

   ' 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);   }

To avoid having to change the code later, you can 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. In the interest of even greater flexibility, you can generalize this process even further, from this:

  1. GetFileName
  2. GetFileData
  3. LogTextData

to this:

  1. GetSource
  2. GetData
  3. 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.

Editor’s Note: This article was first published in the January/February 2008 issue of CoDe Magazine, and is reprinted here by permission.

Building the Providers
The provider model allows 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?in other words, basic polymorphism.

It is pretty customary to set up a separate project for these abstractions. Then, later?when you want to write a different implementation class?you need to reference only this one assembly (see the sidebar “Assembly Separation” for more information).

When I first wrote my little three-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 so 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, because in the future, the “source” can change; it doesn’t necessarily have to be a file name. In fact, the data obtained no longer necessarily needs to come from a file. The only requirement is that the source must be represented as a string and that the three-step process returns string data, which you will later log.

Using this interface abstraction, here’s a first provider based on it, called TextFileProvider (see Listing 2), which resides in its own project, producing its own assembly. Here are the class method signatures.

   ' 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 method implementations in the TextFileProvider class are identical to those in the original FileReader class. Because the signatures are also the same, it plugs quite nicely into my client application. But I still have to change my client (just this once). Instead of directly instantiating the FileReader class, the client will communicate only through the IDataProvider interface. I’ll also take advantage of the app.config file to declaratively specify which IDataProvider implementation the application will use; so far I only have one: TextFileProvider.

The app.config file additions for this first example are simple, so I’ll just use the section. Later examples will use custom configuration sections instead, although explaining that process is beyond the scope of this article. Here’s the app.config addition:

   

The text in the value attribute is standard .NET type notation?the fully qualified class name, a comma, and then the name of the assembly in which it resides. If the class is in the current assembly (the one hosting the app.config file), you can leave out the assembly name here, however relying on that convention is contrary to the problem I’m trying to solve here: to avoid having to adjust the client application when modifying the provider behavior.

The assembly in which the provider class resides will not be added to the client in the conventional manner, by adding a reference; instead, it needs only to be accessible to the client. This means all I have to do is drop it into the client’s Bin folder.

Having defined the app.config as the place the application should look for which implementation of my provider interface it should use, I can now remove the old FileReader code 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 of the preceding code obtains the appropriate type from the app.config file. The second line creates an instance of that type using the CreateInstance method in the Activator class, which lets you instantiate an object from a type name in string form. However, because CreateInstance method does not know what it’s instantiating, it returns a standard Object type. Therefore, you need to change that Object to a type you can actually work with.

However, you don’t want to do this by referencing a concrete class; that would defeat the whole purpose. 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. So the third line of code casts the object variable to the interface type.

I now have the o_Provider variable that I can use to obtain a source, obtain data, and log data. In this case, the type that CreateInstance created is “Providers.TextFileProvider,” so the interface methods will use that implementation.

   ' 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);   }

As you can see, this still meets my application’s original requirements but now it’s far easier to change the behavior or even swap it for entirely different behavior. 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.

Thanks to the provider model, all I need to do is to write a new provider class that implements my same interface and replace the entry in app.config. You can see the full code for the new provider, appropriately named TableDataProvider, in Listing 3. To keep things short, I won’t describe the database layout here, but if you read through the code, you’ll see that the “source” changed from the name of a file to a string used in a table, and the TableDataProvider uses that string to return data from another table. The new code maintains the same contract between the interface and the client, even though the implementation is now very different. To implement the change in the application, you simply change the line in the app.config file to look like this:

   

The client will continue to run without ever knowing that the source of the data has changed.

Wrap 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 its Membership, Roles, and Profile classes. Describing the Membership class should give you a good feel for what I’m going to do in the application example.

In ASP.NET, you can create a user using the Membership class, using:

   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. At that point, 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 delegates the call to the CreateUser method of said variable, which executes the appropriate implementation. That’s what I’m going to do with the provider model.

So here are the steps:

  1. Create a class that will contain static (shared) members only.
  2. Declare a class-level private static variable of type IDataProvider.
  3. In the static constructor, execute the code that will fill the static IDataProvider variable.
  4. Set up static methods that mimic those of the IDataProvider interface.
  5. In each of the methods, call the corresponding method in the IDataProvider variable.

For example, if I called 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, 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);   }

Note that the code doesn’t even call the GetSource method here; instead, GetData does 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 may ever 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, but used in a different way.

Whereas you can use a provider to determine which implementation to use for a certain process, you can use plug-ins to inject functionality into a process, swap that functionality out later, or make it grow.

Consider the example application again. Suppose I need to further enhance my three-step process with the ability to send out an email to some managers regarding the data the process has handled. I can easily just add that functionality to the client right after it logs 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 email, such as 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; the first step is to define an abstraction for my plug-in. I’m going to call the plug-in after logging the data, so I’ll name the interface appropriately: IPostLogPlugin. 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 runs these potential plug-ins. Where I put this code is called a “point of extensibility,” which in this case goes is directly after the logging code. Also, the inserted code will take into account the possibility that more than one such plug-in might be installed. Plug-ins written to this new interface get installed in app.config just like the provider, but plug-ins will go in their own configuration section, called 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 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 o_Plugins =      section as List;   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);   }

The preceding code reads the collection of plug-ins from app.config 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:

              

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.

The client doesn’t know what the plug-in classes do nor how many plug-ins are installed. All that remains is to write the plug-in classes themselves. I’m not going to actually show any implementation details for sending emails or archiving data, but you can envision 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; it merely has to be accessible (meaning just dropped into the client’s 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.

You can use this plug-in model 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?when you’re just securing your application for the future. The drawback comes when you have more than one type of plug-in, each of which may 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 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 this subject.

The preceding section showed you how to write an interface for each type of plug-in; the signature of the interface member(s) identifies the data that the plug-in class needs for its processing. Extensibility modules require a little more work, but it’s a lot more organized. The example used here shows you how to do this by writing what would otherwise be three different plug-ins. One serves the same purpose as the one in the plug-ins example, a “post log” plug-in, called “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, and will determine whether processing should continue.
  • 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 of class will contain the data I need to get from the 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 class is going to need the same data as the PreProcessDataEventArgs class. To make things simpler, I’m just inheriting one from the other.

With the EventArgs class to carry information to and from each plug-in process in place, here’s a class that will use them, called ModuleEvents, which contains a set of properties?one for each plug-in process I want to define. Each property type is a delegate that defines a signature of the corresponding EventArgs-based class. Each property wraps a member variable of the same delegate type. Does that sound confusing enough? Take a look at the code and it should clear things up.

First, you need the delegate types, but fortunately, you need only 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 e);

Now the property types can all use the same delegate type, but each can provide a different value for the generic type. To 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               _CheckDataSource;   private       AcmeModuleDelegate             _PreProcessData;   private       AcmeModuleDelegate             _PostProcessData;

Now you need to define the public properties that expose the member variables. You can see that in Listing 4.

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.

You still need the interface that you’ll use to write plug-ins later. As I said earlier, instead of creating 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 there’s only one method, Initialize, which receives a ModuleEvents argument type, which I just created. Now, the best way to continue is to write a plug-in using this new model.

From what you’ve seen so far, developers have only one interface they need to know about to extend this application. So I’m going to write a new class in a new project called NotificationModule. This class will perform the same process as the EmailProcessing plug-in described earlier; it will implement the IAcmeModule interface and implement the Initialize method. The Initialize method, accesses the events argument, whose properties are delegates. The IntelliSense itself will tell me what my possible extensibility points are. Because 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, you can decide within the module class itself which extensibility point you want to tap into.

Adding Extensibility to the Client
The client will also have to be changed. To support multiple plug-ins, instead of having the client look for a specific plug-in type to instantiate, it will read in all the modules, and loop through them, calling each one’s Initialize method. That process will build 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 o_Modules = section as       List;   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 that the preceding code declares the o_FilterEvents ModuleEvents object outside the loop, and sends the same object to 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 near the beginning of my application, but I still have to decide where to insert extensibility points within the body of the application, just like the earlier example called the IPostLogPlugin-based classes from a specific point in the application.

Upon making these decisions, I’ll check for the property in o_FilterEvents that corresponds to the extensibility point within my application. Checking that property for a value other than null (Nothing in VB.NET) is enough to determine whether 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 inserted 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;   }

Note the Boolean variable declared before this event fires, which is set to the Cancel value that comes back from the 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 so, it sets the Cancel property to true. Now the code added to the client earlier that worked with the b_Cancel variable should make more sense. Code that follows the preceding example would check the value of b_Cancel to determine if it should continue processing.

Extensibility with Multiple Modules
It gets a little more complicated when you have more than one module tapping into this event. The code that executes the Invoke method will fire all the modules wired to the CheckDataSource events, one after the other. Remember, this happens because the initialization routine sends and receives 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, because the Invoke method gets called once with the one EventArgs object. This means that it is up to me to code the module event to check the value of e.Cancel before executing any of the event logic. With that in mind, 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.

Another choice for this kind of logic is probably a bit safer. The previous examples put the responsibility of check for cancellation in the module’s code. But the same developer may not write all your modules, so it may not be reliable to do that. By writing a little more code on the client, you can iterate through the delegate invocation list (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                 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;         }      }   }

Using this technique, as soon as one module sets its e.Cancel property to true, the code breaks out of the loop and stops further processing. In both techniques, the b_Cancel variable determines whether processing should continue, but the responsibility for making the check differs.

You can insert 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 want to subscribe to, or you can group several together. If you 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. You might want such a filter to check for profanity at different points in your application. For example, in the scenario you’ve seen, you could check for profanity at both the CheckDataSource point or at one called PreProcessData 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, grouping them together logically. Listing 5 shows the entire code for such a module.

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, more than likely you will find extensibility points that just snap out at you. Try to resist abstracting at the beginning of the design process, and don’t be afraid to refactor into it later; but beware?after you get the hang of using these patterns, you will be addicted.

You can download the code for this article from the User Group Downloads section of the link. You can find some basic references in the “Additional References” sidebar. You can also watch a DNR-TV episode on this entire subject.

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist