在 C# 中扩展接口的简单设计模式






4.92/5 (4投票s)
如何管理多个版本的设备驱动程序,以及共享通用操作约定的多个版本的应用程序。
引言
本文提供了一种简单的方法,用于开发软件组件,例如设备驱动程序和应用程序,随着时间的推移,由分布式团队开发。团队之间的操作约定通常是接口定义,它是不可变的。一个常见的问题是如何在不破坏当前和未来用户的情况下扩展接口。
背景
典型的场景如下:像 ANSI 或 ISO 这样的标准委员会发布接口定义。硬件供应商编写设备驱动程序来实现该接口。应用程序编写者使用第三方设备驱动程序,因为他们知道必须遵循发布的接口定义。这种行业实践将设备驱动程序开发与应用程序开发解耦。到目前为止一切都很好。
但是,如果硬件供应商出于竞争原因,想要开发一个更高级的驱动程序,具有更多功能呢? 这样一个新的驱动程序必须像以前一样与所有旧的应用程序一起工作,并启用具有更新功能的更新应用程序。 除此之外,更新的应用程序也必须与旧的设备驱动程序一起工作。
我们提出一个使用反射的简单设计模式,以最小的复杂性来支持这一点。 在 .NET 中有很多方法可以扩展接口,例如可扩展性对象。 但这是一种更简单的方法,负担最小。
使用代码
如果您已经熟悉 .NET、C# 和反射,您可以简单地解压附加的源代码存档。 它包含一个完整的 Visual Studio 2010 解决方案,其中包含 9 个简单的项目。 这些项目代表 3 个不同的时间点。 例如版本 1、版本 2、版本 3。这些项目还代表了这些时间点的接口定义、设备驱动程序和应用程序的状态。 只需构建解决方案并在 3 个驱动程序的任何一个上运行 3 个应用程序的任何一个,看看如何处理向上和向下兼容性。
以下是代码的简要浏览
一开始,在 Time1,接口定义如下
public interface IDriver
{
string Name { get; }
int DeviceId { get; }
bool WriteDevice(byte[] data);
}
在 Time1,一些硬件供应商在某些设备驱动程序中实现上述接口,如下所示
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;
}
}
}
请注意 GetInterface() 方法。 它不是接口定义的一部分,但有助于管理版本。
在 Time1,应用程序开发人员使用设备驱动程序,如下所示
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});
}
这里的 assemblyName 是设备驱动程序程序集的完整路径。 代码中散布着大量的调试跟踪。 但关键思想是加载驱动程序程序集,看看它是否支持“Driver.Driver”类型。 如果是,则获取接口。 一旦应用程序获取了接口,就调用 WriteDevice() 方法。 这是 Time1 唯一支持的方法。
一切看起来都很好。 只有一个版本的接口,只有一个设备驱动程序和一个应用程序。 它们都配合得很好。 没有任何兼容性问题。
现在随着时间的推移,在 Time2,IDriver 接口已扩展到 IDriver2。 开发了更新版本的驱动程序和应用程序。 这些都在 IDriver2、Driver2 和 App2 项目中。 请注意,App2 可以与 Driver2 和 Driver1 一起使用。 App1 仍然可以与 Driver1 和 Driver2 一起使用,尽管功能有所降低。
在 Time2,接口如下所示。 请注意,它是从原始接口派生的。 它有一个额外的方法,“ReadDevice”
public interface IDriver2 : IDriver.IDriver
{
// IDriver2
byte[] ReadDevice();
}
时间流逝。 我们现在处于 Time3。 接口如下所示
public interface IDriver3 : IDriver2.IDriver2
{
// IDriver3
bool VerifyDevice(byte[] data);
}
现在这是本文的关键部分。 最新的应用程序 App3(在 Time3 编写)如何处理任何版本的设备驱动程序?
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);
}
我们只需询问设备驱动程序,它支持的最高级别是什么,并使用该接口。 这是通过每个连续版本的设备驱动程序支持一个新的 GetInterfaceX 方法,以及应用程序询问它所知的最佳 X 来实现的。
这是各种版本的应用程序和驱动程序运行时的屏幕截图。 请注意,任何版本的应用程序都可以与任何版本的驱动程序一起使用,但只有最新的应用程序和最新的驱动程序才具有最佳功能集。
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
关注点
有人可能想知道为什么 GetInterfaceX() 方法不是接口约定的一部分。 一般来说,标准组织不指定任何实现工件,GetInterface 纯粹是为了实现的方便。 另一种处理方法是只提供 GetInterface 方法,应用程序可以向上或向下转换它。 有些人可能更喜欢使用 .NET 可扩展性对象来扩展类或接口。 但当前的框架在序列化此类类方面做得不好。 所以这里没有使用它。
总而言之,这里提出的模式易于理解且易于编码。 将其添加到您的框架模式库中。
历史
版本 1.1