一、契约式设计原则:API 设计的指导书

无论是架构设计还是编码实现,现在都越来越离不开接口设计,接口可以说是新时代的“集装箱”,是得到了几乎所有人一致共识的通用标准。

GoF 在很多年前便建议大家应该针对接口编程,原因其实就是为了降低编程变化而导致风险出现的概率。

不过现实中,你可能遇见的更多是这样的情况:

  • 定义好的 API 接口,却接收了额外的参数,导致程序异常退出;

  • 查询返回的数据,没有回传预期的格式,数据处理出错;

  • 一个 API 服务进行分布式部署,遗漏了某个节点,导致处理数据不一致。

现代软件开发虽然一直在按照约定进行编程,但是程序员群体却是喜欢违反契约的群体之一,因为契约代表了约束,而编程本身是自由灵活的事情。那如何才能平衡这种“约束”与“自由”之间的关系?如何才能更好地避免再发生上述情况?以及如何才能真正做到 API 设计呢?

今天,就让我们一起带着这些问题来学习做好 API 设计的方法和技巧。

契约式设计原则(Design by Contract,缩写为 DbC,为表述统一,下文我们就简称为“契约原则”),是一种软件设计方法。其原理是:在软件设计时应该为软件组件定义一种精确和可验证的接口规范,这种规范要包括使用的预置条件、后置条件和不变条件,用来扩展普通抽象数据类型的定义。比如,在 Web 请求中,预置条件主要用于处理输入的参数校验,不变条件主要指一个请求中的共享数据状态,后置条件则是对返回的响应数据的检查与确认。如下示意图:

契约原则的核心思想是对软件系统中元素之间的相互合作以及“责任”与“义务”的比喻,这种比喻是从商业活动中“客户”与“供应商”达成“契约”而得来。

在我看来,理解三者之间的关系非常重要,因为这是帮助我们合理利用契约原则进行 API 设计的基础。

现在 API 设计已经变得越来越重要,但是不管怎么变,API 的设计实现依然需要回答三个关键问题:

  • API 期望的是什么?

  • API 要保证的是什么?

  • API 要保持不变的是什么?

能否设计好一个 API,其实只需要回答好上面这三个问题就行了,而这三个问题本质上就是契约原则的一种最佳实践。

首先,API 必须要保证输入是接收者期望的输入条件。这里的重点就在于“期望”两个字,换句话说,就是:使用者使用 API 的前提条件是什么?只有满足了这个前提条件,API 才能提供正常的功能。比如,API 需要 A、B、C 三个参数,那么使用者就需要提前准备好这三个参数。好的 API 接口都会提前告知使用者,它需要什么、不需要什么,当条件不满足时,它会拒绝;当条件满足时,才会开始处理。

其次,API 必须要保证输出结果的正确性。API 在处理过程中可能会遇见各种异常或错误的情况,这时 API 就不应该把错误或异常抛给其他系统去处理,而是在内部就得做好异常处理,并最终输出正确结果给使用者。

最后,API 必须要保持处理过程中的一致性。比如,同一个 API 被部署在 10 个服务器上,那么只要外部输入了正确条件后,每一个 API 内部的处理过程就应该是相同的、不变的,也就是说,当修改源代码后,你应该要同时部署这 10 个服务,才能让 API 整体的服务看起来是一个整体。不变条件通常有会话信息、共享的上下文数据状态等。所谓不变,你可以理解为多个相同的副本对同一个代码含义的统一解释。

你可能还会问:为什么一定使用契约原则来指导 API 的编程和设计呢?其实主要有以下三个原因。

  • 提前告知 API 有哪些约束会让你在编码过程中节省很多不必要的沟通成本。比如,你的 API 决定使用 RESTful 风格,那么当对方在使用你的 API 时,就会知道返回的数据格式需要采用 JSON。

  • 契约会强迫你去思考 API 是否满足更好的独立性。 API 更多时候是提供给不同类型客户端去使用的,而客户端是不需要关注你的 API 的内部实现,所以好的 API 一定是具备更好的独立性才能对外提供服务。如果 API 不使用标准的协议和消息格式,那么就意味着客户端在使用时需要适配你的 API,这样就会造成系统和 API 之间的强耦合。

  • 契约式设计会提醒你应该保证 API 的高可用性。契约就像是一种承诺,当你在提供 API 时,不仅要保证输入满足要求,还要保证经过处理后返回结果的正确性。同时,服务还应该是稳定可用的,而不能因为网络故障、流量过大等就无法使用服务。

二、如何做好 API 接口设计

理解了上面三个关键问题后,你可能会继续问:“道理我都懂,但实际做的时候有没有什么更具体的方法呢?”结合我多年的工作经验,我总结了以下六大关键技巧,希望能帮助你更好地设计 API 接口。

1. 让接口职责分离

在前面第 12 讲中,我们讲过接口隔离原则(ISP),它是能帮助你做好接口职责分离的一个好方法。当你在设计 API 的时候,应该尽量让每一个 API 只做一个职责的事情。这样做的好处在于能保证 API 功能的简单易用性和稳定性,一旦接口职责混在一起,就会造成接口间功能相互影响。

假设一个 API 既用于修改商品的价格,又用于修改订单优惠价格,还用于修改库存数量等,那么当调用修改商品价格的接口流量增大时,势必会影响同一个接口中修改订单或修改库存的功能。

在使用接口隔离原则时要注意尽量找到变化相似的职责来进行封装,不要在一个接口中加很多方法,方法越多越有可能变成多个职责,那么就无法实现真正的接口隔离。

2. API 命名很重要

好的 API 设计应该给使用者提供一种简单、直观、一致的体验。其中,最关键的就是对 API 的命名,常用的命名技巧有以下三种。

  • 使用英文的小写。例如,使用美式英语的 license、color,并且尽量使用小写,因为大写稍不注意就会看错。

  • 使用被大家都接受的通用缩写。比如,API 就比 Application Programming Interface 好。

  • 通过命名含义描述接口。换句话说,API 的名字应该能自解释,比如 list-userinfo,结合其英文字面含义就知道是查询用户信息的列表。

当然,现在也有很多自动生成接口说明的工具,比如 Swagger,但是对于开发者来说,依然不如直接阅读命名来得方便,并且像 Swagger 这类的工具一旦暴露到公网还会引发安全问题。

3. 尽量少创造自定义错误码

在 API 设计中,很多人会经常犯一个错误,那就是:想方设法地创建自己的 error code,用以表达自己对不同错误的个性化理解。

但实际上,在 API 设计中,这是一种错误应用惯例原则的实践。在前面《14 | 惯例原则:如何提升编程中的沟通效率?》一文中,我们知道自定义惯例的风险就在于各自理解可能有偏差,进而导致业务更大的混乱。比如,当使用方在使用 HTTP API 时,获得了一个 404 错误码(在 HTTP 中是未找到资源的错误),而你恰好也定义了 404 错误码(假设代表接口数据未找到),结果必然出现冲突。

你可能会说自定义的 error code 有助于传递信息,但这有一个前提条件:必须有系统且能对自定义的 error code 进行处理,这样才有意义。如果只是传递信息的话,error message 里面的字段也可以达到同样的效果。

为什么这么说呢?因为当 API 出现错误的时候,API 的使用方是否可以清晰地理解错误信息,是非常重要的。比如,以下几种常见的 REST API 返回 JSON 数据形式:

{"error message": "xxx", "code": "200", "success": true, "data": null}
{"msg": "xxx", "code": "XXX_SYSTEM_ERROR", "data": false}
{"code": 500, "error": "msg xxx","data":false}

可以看到,第三种形式就能很好地传递系统内部出现故障的信息,而第一种和第二种格式看上去虽然信息更多,但是很容易造成理解上的偏差。第一种格式里,success 和 code 作用有重合,这会让使用者不知道到底是应该看 code 还是看 success,又或者两者都要看。至于第二种格式,不同的人对于 XXX_SYSTEM_ERROR 可能理解完全不同。

4. 同一接口要做到幂等

什么是幂等?简单来说,就是当一个操作多次执行所产生的影响均与一次执行的影响相同,则它是幂等的。

幂等是一种通用策略。实际上,HTTP 规范中就明确指出 GET、PUT 和 DELETE 方法必须是幂等的。但 POST 方法不一定保证是幂等的,如果 POST 方法创建新资源,通常不能保证此操作是幂等的。

为什么要实现幂等?这是因为现代服务越来越多地往分布式服务方向发展,而分布式服务的特性就在于服务的分散性,分散性的服务会带来一个问题:多个服务在同一时间可能会并行修改同一份数据。比如,你在楼下超市买完一瓶水后进行微信支付,第一次支付时网络延迟,于是你又刷了一次,那么钱应该只减一份,如果扣两次那就是没有实现幂等。

那么如何在 API 设计中做到接口幂等呢?通常有以下五种方法。

  • 使用天然幂等的操作。比如,数据库中的 select 查询,只要数据没发生改变,那么查询一次和多次的结果始终是一样的;还有 delete 删除操作,删除一次和多次删除都是把数据删除了(不存在了),影响是一样的,典型的天然幂等操作。

  • 使用唯一键值。比如,在数据库中加唯一索引,或是使用 UUID 做唯一 ID,都可以防止在新增数据时出现不必要的脏数据,因为不同的服务节点操作数据时都收到了唯一 ID 的约束。

  • 使用加锁策略。比如,悲观锁、乐观锁、分布式锁等。加锁的目的就是让不同的服务在同一个数据变更时不被其他服务所影响,比如,订单服务 API 部署了 500 个相同实例,那么即便通过 Nginx 网关做了负载均衡的流量分配,在修改订单的时候如果不加锁,就会导致数据被重复多次的修改,也就是无法保证幂等。

  • 使用 Source+Token 验证机制。这和使用唯一键值有一点类似**,**不过这种方法更多用在对外提供的 API 中去保证幂等性。这两个字段实际上既做了联合的唯一索引,又做了使用来源的日志记录,这样既能保证接口的幂等性,也能记录不同客户端使用 API 的调用情况。

  • 使用有限状态机。在一些状态变更比较频繁的业务中,会经常使用到状态机,比如,订单、支付、秒杀等业务。当状态机已经处于下一个状态,这时候又来了一个上一个状态的变更,那么这时就不允许再进行状态变更了。正是通过这种状态扭转的约束,保证了接口服务的幂等性。

5. 安全策略

在做 API 设计时,对于内部系统和外部系统的 API 所考虑的安全策略会有所不同。

对于内部系统来说,更多的是考虑输入与输出数据的准确性,通常都采用更为基础的安全策略。比如,对接收的数据要有足够的验证,出现错误要能及时抛出异常并进行异常处理。当然,对于一些核心系统,比如,财务、订单、用户数据等,依然还是需要进行安全加固,避免外部被攻破后内部数据的泄漏。

对于外部系统来说,API 面临的主要挑战是来自外部的一些针对安全上的攻击、错误调用、接口滥用等。那么在设计的时候,针对黑客攻击,一般是购买业界一些专门做安全的防护公司的产品;针对错误的调用方式,API 在预置条件判断时就不应该让调用进入处理,一定要及时给出错误信息;对于接口滥用的情况,就需要做一些限流的措施。

6. 版本管理

从本质上来讲,API 是服务提供者与服务使用者之间的一种合同协议。一旦服务提供者对 API 进行更改,则有破坏服务使用者使用 API 的风险。因此,你在做 API 设计时一定要考虑更改次数所带来的风险。另外,API 还可能因为不断升级优化而出现旧功能与新功能兼容的问题。

实际上,解决这两个问题的办法就是对你的 API 进行版本控制和管理

当需要进行重大的 API 更改时,要提供一个完整的新版本,同时还要继续支持以前的版本。这里有两种简单的办法:一种是在同一服务 API 中公开两个版本,比如在接口中使用 V1 和 V2;另一种是并行运行两个版本的服务,可以通过增加 API 网关来根据路由规则将请求发送到对应版本中。

不过,要注意的是:同时维护两个或多个版本会提升维护成本。因此,在版本管理时,最好提前计划一个旧版本的下线时间。对于内部 API 来说,要及时通知使用方进行升级和迁移。对于外部(公共)API,则只能不断告知或减缓升级的周期来让使用方及时跟上升级的节奏。

三、总结

契约原则可以说是良好 API 设计的底层逻辑,一个 API 接口设计只要能解决三个关键问题(包括 API 期望什么、API 保证什么、API 保持什么),那么这个 API 基本上就是一个合格的 API。

不过在现实中,你会发现很多 API 连基本三要素都没做好,要么是不做输入校验,要么就是捕获异常不处理、不给出错误信息,或者没按照约定来输出结果,或者扩展后不保证事务一致性等。

为此我还专门总结了六大关键技巧,在我看来,要想设计好 API 接口,除了通用的方法和技巧外,你还是得时不时回到本质的三要素上去思考,看看还有没有你没想到的点,这样才能真正地找到适合你自己的 API 设计方法。

到此,我们模块二“设计原则”的内容就讲解完了。原则虽然很好记忆,但是应用时其实不如模式好把握“度”。这时就需要你反复实践再思考这个原则,甚至需要试着去打破这个原则,看看在实际编程中会带来什么负面效果,才能找到正确应用的路径

原则更多的是给你启发和思考,而不是非要生搬硬套它们。不过随着我们继续深入学习后面的模式,你会渐渐发现,模式里包含的很多原则思想是有原因的。

四、课后思考

为什么有的时候应该使用 RPC API 而不用 HTTP API?

16. 设计模式之契约原则:如何做好 API 接口设计?相关推荐

  1. API接口设计最佳实践

    目录 目录 前言 API接口设计 Token设计 API接口设计原则 1.明确协议规范 2.统一接口路径规范 3.统一接口版本管理 4.为你的接口设定调用门槛 5.接口返回规范 6.接口安全规范 7. ...

  2. API接口设计之RESTful软件架构风格

    说到API接口设计有的喜欢用Web Service,有的喜欢用WCF,当然也有还在用最原始的ashx,aspx页面的.无论采用什么方式能很好的满足业务需求就ok,但是不同的方式在扩展性.易用性,可维护 ...

  3. API接口设计,需要注意这4点

    原文 | http://www.woshipm.com/pd/2772820.html 原则上API接口设计一般出现在开发的详细设计中,但是随着诸多公司建立开放平台,产品经理也逐渐需要能理解API接口 ...

  4. 优秀的API接口设计原则及方法

    一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...

  5. API接口设计要考虑的几个重要原则和方法总结

    转载:https://ask.zkbhj.com/?/article/254 2020博客地址汇总 2019年博客汇总 这里想和大家讨论的是在后台接口设计过程中,还有哪些方面需要考虑,以及还有哪些优秀 ...

  6. API接口设计 注意问题

    摘要: 总结一下API接口开发过程中的注意事项 1.跨平台性 所谓跨平台是指我们的接口要能够支持不同的终端,比如Android.iOS.windowsphone以及桌面软件.网站等.如:不同的终端每页 ...

  7. [API接口设计] RESTful 规范

    RESTful是目前最流行的API设计规范,它是用于Web数据接口的设计:REST它是 Representational State Transfer的简称,中文的含义是: "表征状态转移& ...

  8. api接口设计相关总结

    写过不少接口,不过一直没有去总结,网上搜了一下,大同小异,此文根据以下几个链接整理修改: https://segmentfault.com/a/1190000004051246 http://blog ...

  9. php后台对接ios,安卓,API接口设计和实践完全攻略,涨薪必备技能

    2016年12月29日13:45:27  关于接口设计要说的东西很多,可能写一个系列都可以,vsd图都得画很多张,但是由于个人时间和精力有限,所有有些东西后面再补充 说道接口设计第一反应就是restf ...

最新文章

  1. 《深入理解ES6》笔记——块级作用域绑定(1)
  2. 基于Linux的socket编程模板
  3. 网络语言对作文教学的冲击
  4. 直观理解深度学习卷积部分
  5. php自动生成mysql的触发代码。
  6. php备份mysql页面_如何用PHP的页面备份、恢复Mysql数据库_php
  7. 经典手眼标定算法之Navy的OpenCV实现
  8. 【博客管理】博客资源汇总【置顶】
  9. 配置文件中的后缀dev、test和prod是什么意思
  10. 计算机类应届毕业生的求职历程
  11. apk安装时so库存放路径
  12. 绝密计划:我在阿里打黑工
  13. 在本地安装使用-LTP
  14. 【js】异步方法同步化
  15. 标牌标识导视指示系统设计制作工艺及材料知识
  16. 【一手测评】商务人士最优选择:微软Surface三款凡尔赛办公笔记本电脑
  17. WP 平衡球游戏开发教程(三) -DebugDraw(XNA渲染)
  18. 示波器中符号的意思是什么
  19. iOS 获取网络图片的尺寸
  20. rapidjson坑

热门文章

  1. 运营干货:虾皮店铺装修怎么做?
  2. 阿里云-SchedulerX分布式调度中心
  3. java哪个软件编程好学吗_java编程好学吗
  4. DPABI下载安装、测试及部分问题汇总
  5. Camera Tuning
  6. 一维表和交叉表的转化
  7. android仿qq分组列表效果
  8. opengl 旋转中心理解
  9. 阿里云王坚“骗”马云10个亿,被骂四年都忍了,最后还回4500亿
  10. 6Tops 加持3588 NVR方案