版本之间的界面变化 – 如何管理?

这是我们在客户网站上遇到的一种相当不愉快的泡菜。 客户端有大约100个工作站,我们在其上部署了产品“MyApp”的1.0.0版本。

现在,该产品所做的一件事就是它加载了一个加载项(称之为“MyPlugIn”,它首先在中央服务器上查找是否有更新的版本,如果是,则复制该文件在本地,然后它使用Assembly.Load加载加载项并调用某个已知的接口。这已经好几个月了。

然后客户想在某些机器上安装我们产品的v1.0.1(但不是全部)。 随之而来的是MyPlugIn的新版本。

但后来出现了问题。 有一个共享的DLL,由MyApp和MyPlugIn引用,称为MyDLL,它有一个方法MyClass.MyMethod 。 在v1.0.0和v1.0.1之间, MyClass.MyMethod的签名发生了变化(添加了一个参数)。 现在新版本的MyPlugIn会导致v1.0.0客户端应用程序崩溃:

找不到方法:MyClass.MyMethod(System.String)

客户端明确地不希望在所有客户端站点上部署v1.0.1,因为v1.0.1中包含的修复程序仅对少数工作站是必需的,并且不需要将其推广到所有客户端。 遗憾的是,我们还没有(还)使用ClickOnce或其他大规模部署实用程序,因此推出v1.0.1将是一项痛苦且不必要的练习。

有没有办法在MyPlugin中编写代码,以便它能够同样正常工作,无论它是处理MyDLL v1.0.0还是v1.0.1? 也许在实际调用它之前,有一些方法可以使用reflection来探测预期的接口,看它是否存在?

编辑:我还应该提一下 – 我们有一些非常严格的QA程序。 由于QA已正式发布v1.0.1,因此我们不允许对MyApp或MyDLL进行任何更改。 我们唯一的行动自由是改变MyPlugin,这是专门为这个客户编写的自定义代码。

我从我前一段时间写的应用程序中提取了这段代码并删除了一些部分。
这里假设了很多东西:

  1. MyDll.dll的位置是当前目录
  2. 获取reflection信息的命名空间是“MyDll.MyClass”
  3. 该类有一个没有参数的构造函数。
  4. 您不期望返回值
 using System.Reflection; private void CallPluginMethod(string param) { // Is MyDLL.Dll in current directory ??? // Probably it's better to call Assembly.GetExecutingAssembly().Location but.... string libToCheck = Path.Combine(Environment.CurrentDirectory, "MyDLL.dll"); Assembly a = Assembly.LoadFile(libToCheck); string typeAssembly = "MyDll.MyClass"; // Is this namespace correct ??? Type c = a.GetType(typeAssembly); // Get all method infos for public non static methods MethodInfo[] miList = c.GetMethods(BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly); // Search the one required (could be optimized with Linq?) foreach(MethodInfo mi in miList) { if(mi.Name == "MyMethod") { // Create a MyClass object supposing it has an empty constructor ConstructorInfo clsConstructor = c.GetConstructor(Type.EmptyTypes); object myClass = clsConstructor.Invoke(new object[]{}); // check how many parameters are required if(mi.GetParameters().Length == 1) // call the new interface mi.Invoke(myClass, new object[]{param}); else // call the old interface or give out an exception mi.Invoke(myClass, null); break; } } } 

我们在这里做什么:

  1. 动态加载库并提取MyClass的类型。
  2. 使用该类型,向reflection子系统询问该类型中存在的MethodInfo列表。
  3. 检查每个方法名称以找到所需的方法名称。
  4. 找到方法时,构建该类型的实例。
  5. 获取方法预期的参数数量。
  6. 根据参数的数量,使用Invoke正确的版本。

问题是你所做的改变基本上基本的 ,而不是改变 。 因此,如果您希望在部署中恢复兼容(就像我在当前部署策略中所理解的那样,这是唯一的选择),您永远不应该更改界面,而是为其添加新方法,并避免将插件紧密链接共享DLL,但动态加载它。 在这种情况下

  • 你将添加一个新的function而不会打扰旧的function

  • 您将能够选择在运行时加载哪个版本的DLL。

我的团队犯了同样的错误。 我们有类似的插件架构,从长远来看,我能给你的最好建议就是尽快改变这种架构。 这是一个可维护性的噩梦。 向后兼容性矩阵随每个版本非线性增长。 严格的代码审查可以提供一些缓解,但问题是您总是需要知道何时添加或更改方法以适当的方式调用它们。 除非开发人员和审阅者都确切知道上次更改方法的时间,否则在找不到方法时会冒运行时exception的风险。 您永远不能安全地在插件中调用MyDLL中的新方法,因为您可能在没有最新MyDLL版本的旧客户端上运行。

目前,您可以在MyPlugin中执行以下操作:

 static class MyClassWrapper { internal static void MyMethodWrapper(string name) { try { MyMethodWrapperImpl(name); } catch (MissingMethodException) { // do whatever you need to to make it work without the method. // this may go as far as re-implementing my method. } } private static void MyMethodWrapperImpl(string name) { MyClass.MyMethod(name); } } 

如果MyMethod不是静态的,你可以创建一个类似的非静态包装器。

至于长期变化,你可以做的一件事就是让你的插件接口进行通信。 发布后不能更改接口,但可以定义插件的更高版本将使用的新接口。 此外,您不能从MyPlugIn调用MyDLL中的静态方法。 如果您可以在服务器级别更改内容(我意识到这可能超出您的控制范围),另一种选择是提供某种版本控制支持,以便新插件可以声明它不适用于旧客户端。 然后,旧客户端将仅从服务器下载旧版本,而较新的客户端将下载新版本。

实际上, 更改发布之间的合同听起来是个坏主意。 在面向对象的环境中,您应该创建一个新契约,可能inheritance旧契约。

 public interface MyServiceV1 { } public interface MyServiceV2 { } 

在内部,您可以使引擎使用新接口,并提供适配器以将旧对象转换为新接口。

 public class V1ToV2Adapter : MyServiceV2 { public V1ToV2Adapter( MyServiceV1 ) { ... } } 

加载程序集后,扫描它并:

  • 当您找到实现新接口的类时,可以直接使用它
  • 当您找到实现旧接口的类时,可以使用适配器

使用黑客(比如测试界面)迟早会咬你或其他任何使用合同的人 – 任何依赖于界面的人都必须知道黑客的细节,从面向对象的角度来看这听起来很糟糕。

在MyDLL 1.0.1中,弃用旧的MyClass.MyMethod(System.String)并使用新版本重载它。

你可以重载MyMethod接受MyMethod(字符串)(版本1.0.0兼容)和MyMethod(字符串,字符串)(v1.0.1版本)吗?

鉴于这种情况,我认为你唯一可以做的就是有两个版本的MyDLL“并排”运行,
这意味着像Tigran所建议的那样,动态加载MyDLL – 例如作为一个不相关但可能对你有所帮助的一个例子,看一下RedemptionLoader http://www.dimastr.com/redemption/security.htm# redemptionloader (这是一个Outlook插件,通常会有一些问题,相互之间引用不同版本的助手dll,就像一个背景故事 – 这是一个更复杂的COM原因,但在这里没有太大变化) –
这是你可以做的,类似的东西。 通过它的位置,名称动态加载dll – 您可以在内部指定该位置,硬编码,甚至可以从配置或其他内容设置它(或者如果您看到MyDll的版本不正确,请检查并执行此操作),
然后’包装’对象,调用形成动态加载的dll以匹配你通常拥有的 – 或者做一些这样的技巧(你必须在实现上包装一些东西或’fork’)以使一切都在两种情况下都有效。
还要加上’no-nos’和你的QA悲伤:),
它们不应该破坏从1.0.0到1.0.1的向后兼容性 – 那些(通常)是微小的变化,修复 – 不破坏变化,需要主要版本#。