Click here to Skip to main content
15,881,938 members
Articles / Programming Languages / C#

A Simple Design Pattern to Extend Interfaces in C#

Rate me:
Please Sign up or sign in to vote.
4.92/5 (4 votes)
20 Apr 2015CPOL4 min read 29.8K   166   15   2
How to manage multiple versions of device drivers and with multiple versions of applications, which share common operational contracts.

Introduction

This article provides a simple approach for developing software components  such as  device drivers and applications,  over time , by distributed teams. The operational contract between the teams is usually the Interface definition, which is immutable.  A common problem is how to extend the interface without breaking current and future users.

Background

Typical scenario goes like this:  Standards committees such as ANSI or  ISO  publishes the interface definition.  Hardware vendors write device drivers to that interface.  Application writers use the 3rd party device drivers knowing that they must follow the published interface definitions.  This industry practice de-couples device driver development from application development.  So far everything is fine.

But, what if a Hardware vendor, for competitive  reasons, wants to  develop an advanced driver with lot more capabilities? Such a new driver must work with all the old application as before and enable newer applications with newer features. Added to this,  newer Applications must also work with older device drivers.

We present a simple design pattern using reflection to support this with minimal complexity. There are many ways to extent an interface in .NET, such as Extensibility Objects. But here is a much simpler approach with minimal baggage.

Using the code

If you are already familiar with .NET, C# and refection, you can simply unzip the attached  source code archive. It contains a complete Visual Studio 2010 Solution containing 9 trivial projects. These projects represents 3 different points in time. Such as  Version 1, Version 2, Version 3.    These projects also represents the status of Interface definitions, device drivers and application at these time points.  Simply build the solution and run any of the 3 apps against any of the 3 drivers and see how upward and downward compatibility is handled.

Here is a brief tour of the code:

In the beginning,  At Time1, the interface is defined as follows:

C#
public interface IDriver
{
	 string Name { get; }
	 int DeviceId { get; }
	 bool WriteDevice(byte[] data);
}

At Time1, some hardware vendor implements the above interface in some device driver as follows:

C#
public class Driver : IDriver.IDriver
{
	 public Driver()
	 { 
		  Console.WriteLine("INFO: Driver::IDriver created");
		  Name = "My Name is IDriver.IDriver";
		  DeviceId = 0;
	 }
	 public string Name { get; set; }
	 public int DeviceId { get; set; }
	 public bool WriteDevice(byte[] data)
	 {
		  // use date here
		  Console.WriteLine("INFO: Driver::IDriver::WriteDevice(). Data Length = {0}", data.Length);
		  return true;
	 }
	 public IDriver.IDriver GetInterface
	 {
		  get
  			{
			   return this as IDriver.IDriver;
		        }
	 }
}

 

 

Please note the method GetInterface().  It is not part of the Interface definition but helps to manage versions.

At Time1, App developers use device drivers as follows:

C#
Assembly testAssembly = Assembly.LoadFile(assemblyName);
Console.WriteLine("INFO: Listing types in {0}", assemblyName);
foreach (Type typeName in testAssembly.GetTypes())
{
	 Console.WriteLine("INFO: {0} has type {1}", assemblyName, typeName.FullName);
}
//
// Create Instance
//
Type driverType = testAssembly.GetType("Driver.Driver");
if (driverType != null)
{
	 object driverObj = Activator.CreateInstance(driverType);
	 PropertyInfo namePropertyInfo = driverType.GetProperty("Name");
	 PropertyInfo deviceIdPropertyInfo = driverType.GetProperty("DeviceId");
	 string name = (string)namePropertyInfo.GetValue(driverObj, null);
	 int deviceId = (int)deviceIdPropertyInfo.GetValue(driverObj, null);
	 Console.WriteLine("INFO: Object Properties: Name = {0} DeviceId = {1}", name, deviceId);
	 // GetInterface
	 PropertyInfo interfacePropertyInfo = driverType.GetProperty("GetInterface");
	 IDriver.IDriver iDriver = (IDriver.IDriver)interfacePropertyInfo.GetValue(driverObj, null);
	 Console.WriteLine("INFO: Interface Properties: Name = {0} DeviceId = {1}", iDriver.Name , iDriver.DeviceId );
	 iDriver.WriteDevice(new byte[1]{0});
}

Here assemblyName is the full path to the device driver assembly.  Code is sprinkled with copious debugging traces. But the key ideas is to load driver assembly, see if it supports the "Driver.Driver" type. If so get the interface. Once the app gets the interface, call the  WriteDevice() method. This is the only method supported at Time1.

 

Every thing looks good. There is only version of interface, only one device driver and only one App. They all play well. There is no compatibility issue what so ever.

Now over time, at Time2, IDriver interface has been extended to IDriver2.  Newer version of drivers and apps were developed. These are in  IDriver2, Driver2 and App2 projects.  Note App2 can work with both Driver2 and Driver1. App1 still works with Driver1 and ALSO Driver2, although with reduced functionality.

At Time2, the interface looks like this. Please note that is derived from the original interface. It has one extra method, "ReadDevice"

C#
public interface IDriver2 : IDriver.IDriver
{
 	// IDriver2
	 byte[] ReadDevice();
}

 

Time progresses. We are at Time3 now.  The Interface looks like this:

C#
public interface IDriver3 : IDriver2.IDriver2
{
 	// IDriver3
	 bool VerifyDevice(byte[] data);
}

 

Now here is the key part of this article. How does the latest App, App3 ( written at Time3 ) handle ANY version of the device driver ?

C#
Assembly testAssembly = Assembly.LoadFile(assemblyName);
//
//  List types
//
Console.WriteLine("INFO: Listing types in {0}", assemblyName);
foreach (Type typeName in testAssembly.GetTypes())
{

    Console.WriteLine("INFO: {0} has type {1}", assemblyName, typeName.FullName);
}

//
// Create Instance
//

Type driverType = testAssembly.GetType("Driver.Driver");
if (driverType != null)
{
    object driverObj = Activator.CreateInstance(driverType);
    PropertyInfo namePropertyInfo = driverType.GetProperty("Name");
    PropertyInfo deviceIdPropertyInfo = driverType.GetProperty("DeviceId");
    string name = (string)namePropertyInfo.GetValue(driverObj, null);
    int deviceId = (int)deviceIdPropertyInfo.GetValue(driverObj, null);
    Console.WriteLine("INFO: Object Properties:  Name = {0}  DeviceId = {1}", name, deviceId);

    // See if latest GetInterface3 is available
    PropertyInfo interfacePropertyInfo = driverType.GetProperty("GetInterface3");
    if (interfacePropertyInfo != null)
    {
        // GetInterface3
        Console.WriteLine("INFO: Great News. {0} supports the latest Interface", assemblyName);
        IDriver3.IDriver3 iDriver3 = (IDriver3.IDriver3)interfacePropertyInfo.GetValue(driverObj, null);
        Console.WriteLine("INFO: Interface Properties:  Name = {0}  DeviceId = {1}", iDriver3.Name, iDriver3.DeviceId);

        // 3 operations possible
        iDriver3.WriteDevice(new byte[1] { 0 });
        byte[] data = iDriver3.ReadDevice();
        bool result = iDriver3.VerifyDevice(data);

    }
    else if (  (interfacePropertyInfo = driverType.GetProperty("GetInterface2")) != null )
    {

        // GetInterface2
        Console.WriteLine("INFO: Good News. {0} supports intermediate Interface. VerifyDevice not supported.", assemblyName);
        IDriver2.IDriver2 iDriver2 = (IDriver2.IDriver2)interfacePropertyInfo.GetValue(driverObj, null);                      
        Console.WriteLine("INFO: Interface Properties:  Name = {0}  DeviceId = {1}", iDriver2.Name, iDriver2.DeviceId);

        // Only 2 operations possible
        iDriver2.WriteDevice(new byte[1] { 0 });
        byte[] data = iDriver2.ReadDevice();
    }
    else if ((interfacePropertyInfo = driverType.GetProperty("GetInterface")) != null)
    {
        // GetInterface
        Console.WriteLine("INFO: Running  {0} with the oldest Interface. ReadDevice and VerifyDevice not supported.", assemblyName);
        IDriver.IDriver iDriver = (IDriver.IDriver)interfacePropertyInfo.GetValue(driverObj, null);
        Console.WriteLine("INFO: Interface Properties:  Name = {0}  DeviceId = {1}", iDriver.Name, iDriver.DeviceId);

        // Only 1 operation possible
        iDriver.WriteDevice(new byte[1] { 0 });
    }
    else
    {
        Console.WriteLine("ERROR: Invalid Driver assembly: {0}", assemblyName);
    }

 

We simply ask the device driver, what is the highest level it supports and use that interface. This is a achieved by each successive version of the device drivers supporting a new  GetInterfaceX method and the App asking for the best X it knows.

 

Here is a screen shot of various versions of Apps and Drivers in action.  Please note that any version of app can work with any version of driver, but only the latest app and the latest driver, will have the best feature set.

c:\t15\ExtendingInterfaces\out>app1 c:\t15\ExtendingInterfaces\out\Driver1.dll          <-- At Time1,  latest app (v1) with latest driver(v1)
INFO: Using c:\t15\ExtendingInterfaces\out\Driver1.dll
INFO: Listing types in c:\t15\ExtendingInterfaces\out\Driver1.dll
INFO: c:\t15\ExtendingInterfaces\out\Driver1.dll has type Driver.Driver
INFO: Driver::IDriver created
INFO: Object Properties:  Name = My Name is IDriver.IDriver  DeviceId = 0
INFO: Interface Properties:  Name = My Name is IDriver.IDriver  DeviceId = 0
INFO: Driver::IDriver::WriteDevice(). Data Length = 1                                   <-- just one op in ver 1

c:\t15\ExtendingInterfaces\out>app2 c:\t15\ExtendingInterfaces\out\Driver2.dll          <-- At Time2,  latest app(v2) with latest driver(v2)
INFO: Using c:\t15\ExtendingInterfaces\out\Driver2.dll
INFO: Listing types in c:\t15\ExtendingInterfaces\out\Driver2.dll
INFO: c:\t15\ExtendingInterfaces\out\Driver2.dll has type Driver.Driver
INFO: Driver2::IDriver2 created
INFO: Object Properties:  Name = My Name is Driver2.cs  DeviceId = 0
INFO: Good News. c:\t15\ExtendingInterfaces\out\Driver2.dll supports latest Interface
INFO: Interface Properties:  Name = My Name is Driver2.cs  DeviceId = 0
INFO: Driver2::IDriver2::WriteDevice(). Data Length = 1
INFO: Driver2::IDriver2::ReadDevice(). Data Length = 1                                  <-- new op in ver 2

c:\t15\ExtendingInterfaces\out>app3 c:\t15\ExtendingInterfaces\out\Driver3.dll          <-- At Time3, latest app(v3) with latest driver(v3)
INFO: Using c:\t15\ExtendingInterfaces\out\Driver3.dll
INFO: Listing types in c:\t15\ExtendingInterfaces\out\Driver3.dll
INFO: c:\t15\ExtendingInterfaces\out\Driver3.dll has type Driver.Driver
INFO: Driver3::IDriver3 created
INFO: Object Properties:  Name = My Name is Driver3.cs  DeviceId = 0
INFO: Great News. c:\t15\ExtendingInterfaces\out\Driver3.dll supports the latest Interface
INFO: Interface Properties:  Name = My Name is Driver3.cs  DeviceId = 0
INFO: Driver3::IDriver3::WriteDevice(). Data Length = 1
INFO: Driver3::IDriver3::ReadDevice(). Data Length = 1                                  <-- new op in ver 2
INFO: Driver3::IDriver3::VerifyDevice(). Data Length = 1                                <-- new op in ver 3

c:\t15\ExtendingInterfaces\out>app1 c:\t15\ExtendingInterfaces\out\Driver3.dll          <--  oldest app(v1) and newest driver(v3)
INFO: Using c:\t15\ExtendingInterfaces\out\Driver3.dll
INFO: Listing types in c:\t15\ExtendingInterfaces\out\Driver3.dll
INFO: c:\t15\ExtendingInterfaces\out\Driver3.dll has type Driver.Driver
INFO: Driver3::IDriver3 created
INFO: Object Properties:  Name = My Name is Driver3.cs  DeviceId = 0
INFO: Interface Properties:  Name = My Name is Driver3.cs  DeviceId = 0
INFO: Driver3::IDriver3::WriteDevice(). Data Length = 1

c:\t15\ExtendingInterfaces\out>app3 c:\t15\ExtendingInterfaces\out\Driver1.dll          <-- newest app(v3) and oldest driver(v1)
INFO: Using c:\t15\ExtendingInterfaces\out\Driver1.dll
INFO: Listing types in c:\t15\ExtendingInterfaces\out\Driver1.dll
INFO: c:\t15\ExtendingInterfaces\out\Driver1.dll has type Driver.Driver
INFO: Driver::IDriver created
INFO: Object Properties:  Name = My Name is IDriver.IDriver  DeviceId = 0
INFO: Running  c:\t15\ExtendingInterfaces\out\Driver1.dll with the oldest Interface. ReadDevice and VerifyDevice not supported.
INFO: Interface Properties:  Name = My Name is IDriver.IDriver  DeviceId = 0
INFO: Driver::IDriver::WriteDevice(). Data Length = 1

 

 

 

Points of Interest

One may wonder why GetInterfaceX() method is not part of the Interface contract. In general the standards organizations do not specify any implementations artifacts an GetInterface is purely an implementation convenience.  Another way to handle would be to provide only GetInterface method, which can be up or down casted by the application.  Some people may prefer to use .NET Extensibility object to extend a class or interface. But current frameworks does a poor job of serializing such classes. So that is not used here.

In summary,  the pattern presented here is simple to understand and simple to code. Add this to your arsenal of framework patterns.

History

Version 1.1

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionUnneeded Reflection... Pin
Florian Sundermann20-Apr-15 23:44
Florian Sundermann20-Apr-15 23:44 
AnswerRe: Unneeded Reflection... Pin
Raj Nakkiran22-Apr-15 9:09
Raj Nakkiran22-Apr-15 9:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.