版本之间的接口更改-如何管理
本文关键字:何管理 管理 之间 接口 版本 | 更新日期: 2023-09-27 18:29:36
这是我们在客户端网站上遇到的一个相当令人不快的难题。客户端有大约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?也许有某种方法可以在实际调用之前使用反射来探测预期的接口,看看它是否存在?
编辑:我还应该提到——我们有一些非常严格的QA程序。由于v1.0.1已经由QA正式发布,我们不允许对MyApp或MyDLL进行任何更改。我们唯一的行动自由是更改MyPlugin,这是专门为该客户编写的自定义代码。
问题是,您所做的更改基本上必须是,而不是更改。因此,如果你想在部署中重新兼容(正如我在当前部署策略中所理解的那样,这是唯一的选择),你应该永远不要更改接口,而是添加一个新的方法,避免插件与共享DLL的紧密链接,而是动态加载。在这种情况下,
-
您将添加一个新的函数性,而不会干扰旧的
-
您可以选择在运行时加载哪个版本的dll。
我从一个不久前编写的应用程序中提取了这段代码,并删除了一些部分
这里假设了许多事情:
- MyDll.dll的位置是当前目录
- 要获取反射信息的命名空间为"MyDll.MyClass"
- 该类有一个不带参数的构造函数
- 您不期望返回值
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;
}
}
}
我们在这里做什么:
- 动态加载库并提取
MyClass
的类型 - 使用该类型,向反射子系统询问该类型中存在的
MethodInfo
的列表 - 检查每个方法名称以找到所需的名称
- 当找到该方法时,生成该类型的实例
- 获取该方法所需的参数数
- 根据参数的数量,使用
Invoke
调用正确的版本
我的团队不止一次犯了同样的错误。我们有一个类似的插件架构,从长远来看,我能给你的最好建议是尽快改变这个架构。这是一场可维护性的噩梦。向后兼容性矩阵随着每次发布而非线性增长。严格的代码审查可以提供一些缓解,但问题是您总是需要知道何时添加或更改了方法才能以适当的方式调用它们。除非开发人员和审阅者都确切地知道方法上次更改的时间,否则当找不到该方法时,就有出现运行时异常的风险。你永远不能在插件中安全地调用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中的静态方法。如果您可以在服务器级别更改内容(我意识到这可能超出了您的控制范围),另一种选择是提供某种版本控制支持,以便新插件可以声明它不适用于旧客户端。然后,旧客户端将只从服务器下载旧版本,而新客户端将下载新版本。
实际上,在发布之间更改合同听起来是个坏主意。在一个面向对象的环境中,您应该创建一个新的契约,可能继承旧的契约。
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(string,string)(v1.0.1版本)吗?
在这种情况下,我认为你唯一能做的就是让两个版本的MyDLL"并排"运行,
这意味着类似于Tigran所建议的动态加载MyDLL——例如,作为一个无关但可能对您有所帮助的附带示例,请查看RedemptionLoaderhttp://www.dimastr.com/redemption/security.htm#redemptionloader(这是针对Outlook插件的,它们经常在引用不同版本的帮助程序dll时出现问题,只是作为背景故事-这是涉及COM的更复杂的原因,但在这里没有太大变化)-
这是你能做的,类似的事情。根据dll的位置、名称动态加载dll-您可以在内部指定该位置、硬代码,甚至可以从配置或其他方面进行设置(如果您发现MyDll的版本不正确,请检查并执行此操作),
然后"包装"对象,从动态加载的dll中调用以匹配您通常拥有的内容,或者做一些类似的技巧(您必须在实现上包装一些东西或"fork"),以使一切在这两种情况下都能工作
还要加上"不"和你的QA悲伤:),
它们不应该破坏从1.0.0到1.0.1的向后兼容性——这些(通常)是次要的更改、修复——而不是破坏性的更改,需要主要的版本号。