Juconcurrent 学而不思则罔,思而不学则殆。

App版本升级兼容性思考

2016-08-04

接口的实现,并不能考虑到方方面面,越到后期越容易出现接口调整带来的问题。如何做好兼容?如何正确引导软件的“可持续发展”?本篇博客为你揭晓。

问题引出

在项目中,实现针对订单的收银的时候,如果使用储值支付,需要记录储值支付的会员ID,这个值开始是存放在收银明细中,后续因为其他考虑,变更了这个值存放的位置。 虽然结构上更为合理了,但是实际上却增加了维护的成本,我们需要兼容老版本,以便对其进行支持。

数据传输格式

数据格式一般使用JSON,其标准定义只有以下6种数据类型。

Number:整数或浮点数
String:字符串
Boolean:true或false
Array:数组,以方括号括起“[]”
Object:对象,以大括号括起“{}”
Null:空类型

传输的类型不能超过这6种。我们所有涉及到的类型,都可以用这6种来标识。如日期格式、时间格式,都可以用毫秒数来标识。 我们知道时间戳是以1970-01-01 00:00:00为开始时间(时间戳数字0),之后每隔1毫秒,数值增加1。如果要表示之前的日期,可以使用负值来标识。

服务端接口返回的数据一般如下:

{
    "code": 1000,
    "message": "操作成功",
    "content": {
        "key1": "value1",
        "key2": "value2"
    }
}
  1. code,状态码
  2. message,描述信息
  3. content,数据内容,一般是成功才返回

不同的错误,需要定义不同的状态码。同时,客户端错误码和服务端错误码最好也能区分开。错误码不宜定义过多,客户端很多时候只需要提示错误信息,而不必知晓错误码,这时可以使用统一的错误码。 常用的状态码举例如下:

  • 1000,操作成功
  • 1001,签名错误
  • 1002,校验错误
  • 1003,业务操作失败
  • 1004,不支持的业务
  • 1005,数据访问错误
  • 1006,数据已存在
  • 1007,数据不存在
  • 1008,时间戳不正确

接口版本迭代

接口一开始设计和实现,不可能考虑得非常全面,也不能保证后续不会进行修改和变化。接口对外暴露的变化一般有以下几种:

  1. 返回数据的变化,返回参数增加返回数据
  2. 请求参数的变化,新增了xx参数
  3. 接口废弃,不再使用了

针对1和2,我们只有尽量考虑兼容情况;针对3,我们可以使用版本迭代的方式来处理。

版本兼容

版本兼容,意味着接口重用、沿用。我们一般不会出现这个迭代实现了接口A,下个迭代马上废弃接口A的情况。更多的是在接口上进行入参和出参的变化。 要做好版本兼容,不是一件容易的事,需要的是经验的积淀。在设计的时候,时常需要询问自己以下的问题。

  1. 是一个接口,还是多个接口,能否重用;
  2. xx请求字段参数是否必须,是否合理;
  3. xx返回字段参数是否必须,是否合理;
  4. 接口的聚合性如何,能否统一处理核心业务;

例如:我们需要实现一个功能来根据用户id列表,查询用户的信息。

设计的第一版

请求,看上去很简单,也容易理解,不错。

{
    "userIds": "id1, id2, id3"
}

返回

{
    "code": 1000,
    "message": "操作成功",
    "content": {
        "users": [
            {
                "id": 1,
                "name": "zhangfb",
                "...": "..."
            }
        ]
    }
}

设计的第二版

现在产品经理要求在原有的基础上,过滤出时间在指定时间之后添加的用户,且对用户进行分组展示。Oh,设计还是很简单的,只需要对请求和返回参数做一定的调整。 而我们发现我们我们传入的userIds不是很合理。其一、客户端需要对id进行拼装;其二、服务端需要对id进行解析;其三、如果针对不同的id需要有不同的判断准则,不好扩展。 好了,既然发现了这些问题,那就高高兴兴地完成第二版。

请求

{
    "userIds": [
        {
            "id": 1
        }
    ],
    "afterAddTime": 1234
}

返回参数既然需要对用户进行分组,那么每个user,需要有一个组别。

{
    "code": 1000,
    "message": "操作成功",
    "content": {
        "users": [
            {
                "id": 1,
                "name": "zhangfb",
                "group": 1,
                "...": "..."
            }
        ]
    }
}

返回数据还是没有达到我们预期的效果,我们需要对用户进行分组,而不是每个用户有一个组的属性。好像意义开始有一定的扭曲了。再次对返回数据进行重新定义。

{
    "code": 1000,
    "message": "操作成功",
    "content": {
        "userGroups":[
            {
                "groupId": 1,
                "users":[
                    {
                       "id": 1,
                       "name": "zhangfb",
                       "...": "..."
                   }
               ]
           }
        ]
    }
}

总结

我们逐渐发现我们的接口入出参不像我们最初设计的那么简单,它从一种状态变化成了另外一种状态。 这种演变的过程是有可能的,一成不变的那不叫软件,那叫化石。我们定义接口的时候,需要做以下约定。

  1. 基础功能必须稳定(核心逻辑、数据持久、方法复用);
  2. 清晰的接口边界定义(它不能跨过的场景、意义);
  3. 接口的入出参需要做到足够精炼;
  4. 接口可能出现的扩展点;
  5. 版本迭代,大版本的演进;

版本迭代

软件生命周期,接口作为软件的一部分,同样也存在生命周期。对于小版本,我们做接口兼容;对于大版本,我们做url兼容。 所谓url兼容,就是在url层级区分出版本号。比如:

  • v1版本的接口定义,/v1/trade/info
  • v2版本的接口定义,/v2/trade/info
  • 以此类推

这样定义带来的好处是,不影响原来的接口,同时又可以对功能进行最大幅度地“重构”。当后续大版本迭代到一定层度,可以对一些版本的接口进行废弃操作。

版本迭代的同时,意味着客户端需要进行升级,对于小版本做了兼容,所以不需要“强制升级”,但是在对一些重大bug进行修复时,可以对此例外进行“强制升级”。对于大版本,需要“强制升级”。

版本管理系统,在版本迭代中是非常非常重要的,也是抽象度比较高的系统。它一般情况下不会做太大的调整,所以在做接口之前,需要先把此功能做好、做强、做到易扩展。

参考链接

  • http://www.tuicool.com/articles/FrIR32R
  • http://www.zhihu.com/question/30682469
  • http://www.jianshu.com/p/a06c183e8494
  • http://www.tuicool.com/articles/BFJZjuj

下一篇 JSON语言

Content