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:
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:
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)
{
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:
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);
}
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);
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"
public interface IDriver2 : IDriver.IDriver
{
byte[] ReadDevice();
}
Time progresses. We are at Time3 now. The Interface looks like this:
public interface IDriver3 : IDriver2.IDriver2
{
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 ?
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);
}
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);
PropertyInfo interfacePropertyInfo = driverType.GetProperty("GetInterface3");
if (interfacePropertyInfo != null)
{
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);
iDriver3.WriteDevice(new byte[1] { 0 });
byte[] data = iDriver3.ReadDevice();
bool result = iDriver3.VerifyDevice(data);
}
else if ( (interfacePropertyInfo = driverType.GetProperty("GetInterface2")) != null )
{
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);
iDriver2.WriteDevice(new byte[1] { 0 });
byte[] data = iDriver2.ReadDevice();
}
else if ((interfacePropertyInfo = driverType.GetProperty("GetInterface")) != null)
{
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);
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