4
头图

写代码难免出现失误。在对某些已经发布的库进行升级或者审查的时候,就有可能会发现一些接口名称需要变更。比如,早期命名不符合特定规范,或者出现了难以发现的拼写错误等。有错当然是要改的,但是直接更名会影响到已发布的接口。粗暴的名称变更本质上是删除了旧接口,创建了新接口,对 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 实例。那么 ActionResultCodecode 属性都会被处理为 "code" 这个键名,于是发生了重名的冲突。

为了解决这个问题,可以为重名属性中的一个添加 Newtonsoft JSON 的 [JsonIgnore] 特性声明。为了保持兼容性,应该在新加的 CodeMessage 属性上添加这一声明,并在 Obsolete 描述和相关文档、发行说明中充分说明这一情况,告诉用户这一变更可能产生序列化问题。

也许你已经发现了,在这个问题上,我们作为库的发行方始终不能保持完全的兼容,毕竟用户使用什么样的序列化方式不是我们能决定的。也许用户会序列化成 XML,也许用户不使用 Newtonsoft JSON 而是使用别的 JSON 序列化工具,也许用户是需要遍历所有属性来实现某种逻辑 —— 作为库的发行方,我们除了通知和警告,无能为力 —— 这也是很多知名大库会把一些奇怪的接口保留很久的原因之一。

处理不符合规范的接口属性

注意到,上面示例的类实现了 IActionResult 接口。codemessage 的命名规范问题其实是从接口引入的,所以我们还需要处理接口属性的命名。处理接口属性会更为繁琐,但好在 C# 8.0 引入了接口默认实现的特性。假设之前 IActionResult 接口定义如下

public interface IActionResult {
    int code { get; set; }
    string message { get; set; }
}

使用默认实现添加 CodeMessage 并声明原来的属性即将失效

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 内的错误名称都更正了。然后再定义一个继承自 ApiErrorActionApiErrorActioin(注意要实现与原来相同签名的所有构造函数重载),并添加废弃声明。

[Obsolete("因为拼写错误将从 v3.5 版本中删除,请使用正确命名的 ApiErrorAction")]
class ApiErrorActioin : ApiErrorAction {
    // 构造函数示例
    public ApiErrorActioin(string name): base(name) { }
}

这里需要特别注意的是,为保持兼容性而存在的 ApiErrorActioin 除了保持原来的非私有构造函数接口之外,其他接口都可以直接从修正后的 ApiErrorAction 继承而来。而且,这个要废弃的 ApiErrorActioin不应该包含任何逻辑代码

对接口更名也可以采用类似的方法。但要注意的是,这种处理方式添加了继承层次。虽然一般情况下不会造成用户的困扰,但是如果用户的“反射”代码有涉及到继承层次的逻辑,就有可能出现问题。因此这样的变更同样需要在文档中注明并发出警告。

小错误,大问题

对于人类来说,大脑的自动修正能力非常强(真正的人工智能!)所以一个小小的拼写错误或者大小字错误并不是什么大问题。但对计算机来说,只差一个字符,那就是完全不同的两个标识。更正拼写本身是个小事,但对于公共库接口的更名,可能会对用户产生巨大的影响,需要非常谨慎地处理。

然而,即使我们非常谨慎的处理了能想到的各种兼容性问题,差异仍然是不可避免的。你永远不知道用户会怎么使用这个库,所以也不知道用户会因为变更遇到什么奇怪的问题 —— 所以请重视文档和发行说明中的详细描述和警告。问题是藏不住的,公布它才是正确的选择。


边城
59.3k 声望29.2k 粉丝

一路从后端走来,终于走在了前端!