写代码难免出现失误。在对某些已经发布的库进行升级或者审查的时候,就有可能会发现一些接口名称需要变更。比如,早期命名不符合特定规范,或者出现了难以发现的拼写错误等。有错当然是要改的,但是直接更名会影响到已发布的接口。粗暴的名称变更本质上是删除了旧接口,创建了新接口,对 API 用户来说极具破坏性 —— 用户会发现所有用到这些接口的地方都编译不过,或者不能运行了,这简直就是一场灾难。
本文主要以 C# 为例介绍对库接口更名的处理 —— 在 Assembly 内部,直接使用“重命名”重构方法,借助 IDE/Editor 的能力就可以完成变更;但是对于开放出去的接口,就必须保持接口兼容性,并声明过期时间。
本文以 C# 为例,但是处理方式和重构思想是语言无关的!处理不符合规范的类属性命名 这一节主要是解决属性更名的问题。方法更名和属性更名是同样的道理,后面不再赘述。
【小失误,大问题 —— 为已发布的接口更名】假设有如下定义:
public class ActionResult : IActionResult {
public int code { get;
set;
}
public string message { get;
set;
}
}
这个定义命名不符合“公开属性使用 Pascal Case 命名规则”的规范。但作为一个已经被广泛使用的库,直接重命名将产生巨大的破坏,所以这里应该按正确的名称添加属性,并将旧的属性声明为过期。
public class ActionResult : IActionResult {
public int Code { get;
set;
}
public string Message { get;
set;
}[Obsolete("因规范命名,将从 v3.4 版本中删除,请使用 Code 代替")]
public int code {
get => Code;
set => Code = value;
}[Obsolete("因规范命名,将从 v3.4 版本中删除,请使用 Message 代替")]
public string message {
get => Message;
set => Message = value;
}
}
不过修改后的
ActionResult
会在序列化的时候出现问题。库中使用了 Newtonsoft JSON 来得到 JSON,按项目中的 JSON 规范,键名会使用 Camel Case 命名规则,所以在配置中添加了 CamelCasePropertyNamesContractResolver
实例。那么 ActionResult
的 Code
和 code
属性都会被处理为 "code"
这个键名,于是发生了重名的冲突。为了解决这个问题,可以为重名属性中的一个添加 Newtonsoft JSON 的
[JsonIgnore]
特性声明。为了保持兼容性,应该在新加的 Code
和 Message
属性上添加这一声明,并在 Obsolete
描述和相关文档、发行说明中充分说明这一情况,告诉用户这一变更可能产生序列化问题。也许你已经发现了,在这个问题上,我们作为库的发行方始终不能保持完全的兼容,毕竟用户使用什么样的序列化方式不是我们能决定的。也许用户会序列化成 XML,也许用户不使用 Newtonsoft JSON 而是使用别的 JSON 序列化工具,也许用户是需要遍历所有属性来实现某种逻辑 —— 作为库的发行方,我们除了通知和警告,无能为力 —— 这也是很多知名大库会把一些奇怪的接口保留很久的原因之一。
处理不符合规范的接口属性 注意到,上面示例的类实现了
IActionResult
接口。code
和 message
的命名规范问题其实是从接口引入的,所以我们还需要处理接口属性的命名。处理接口属性会更为繁琐,但好在 C# 8.0 引入了接口默认实现的特性。假设之前 IActionResult
接口定义如下public interface IActionResult {
int code { get;
set;
}
string message { get;
set;
}
}
使用默认实现添加
Code
和 Message
并声明原来的属性即将失效public interface IActionResult {
int Code { get => code;
set => code == value;
}
string Message { get => message;
set => message = value;
}[Obsolete("因规范命名,将从 v4.0 版本中删除,请改为实现 Code 属性")]
int code { get;
set;
}[Obsolete("因规范命名,将从 v4.0 版本中删除,请改为实现 Message 属性")]
string message { get;
set;
}
}
变更之后,前述
ActionResult
规范化前后的代码都可以通过编译。这里特别需要注意的是,接口变更影响会比实现(类)的影响更大,应该给予用户足够的修改时间来处理,通常会在下个大版本或者越过几个大版本之后才实际删除声明为 Obsolete 的接口,进行破坏性的升级。
处理类名中的拼写错误 在代码审查的过程中,发现类
ApiErrorActioin
的名字中出现了拼写错误(追究为什么会让这样的错误出现在已发布的库中是有必要的,因为这通常是因为过程管理中存在漏洞,但不是本文的研究内容)。直接更改类名同样是极具破坏性的,我们可以使用“重命名”重构方法更正名称,再添加一个与之存在继承关系,但保持原来错误名称的类来处理。
先使用重命名重构方法将
ApiErrorActioin
更正名称为 ApiErrorAction
。现在所有 Assembly 内的错误名称都更正了。然后再定义一个继承自 ApiErrorAction
的 ApiErrorActioin
(注意要实现与原来相同签名的所有构造函数重载),并添加废弃声明。[Obsolete("因为拼写错误将从 v3.5 版本中删除,请使用正确命名的 ApiErrorAction")]
class ApiErrorActioin : ApiErrorAction {
// 构造函数示例
public ApiErrorActioin(string name): base(name) { }
}
这里需要特别注意的是,为保持兼容性而存在的
ApiErrorActioin
除了保持原来的非私有构造函数接口之外,其他接口都可以直接从修正后的 ApiErrorAction
继承而来。而且,这个要废弃的 ApiErrorActioin
中不应该包含任何逻辑代码。对接口更名也可以采用类似的方法。但要注意的是,这种处理方式添加了继承层次。虽然一般情况下不会造成用户的困扰,但是如果用户的“反射”代码有涉及到继承层次的逻辑,就有可能出现问题。因此这样的变更同样需要在文档中注明并发出警告。
小错误,大问题 对于人类来说,大脑的自动修正能力非常强(真正的人工智能!)所以一个小小的拼写错误或者大小字错误并不是什么大问题。但对计算机来说,只差一个字符,那就是完全不同的两个标识。更正拼写本身是个小事,但对于公共库接口的更名,可能会对用户产生巨大的影响,需要非常谨慎地处理。
然而,即使我们非常谨慎的处理了能想到的各种兼容性问题,差异仍然是不可避免的。你永远不知道用户会怎么使用这个库,所以也不知道用户会因为变更遇到什么奇怪的问题 —— 所以请重视文档和发行说明中的详细描述和警告。问题是藏不住的,公布它才是正确的选择。