Login | Register   
RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Design for Extensibility : Page 2

Build extensibility and flexibility into your applications to simplify maintenance and accommodate changes.

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 <appSettings> 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:

<add key="dataProvider" value="Providers.TextFileProvider, Providers"/>

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:

<add key="dataProvider" value="Providers.TableDataProvider, Providers"/>

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

Comment and Contribute








