先把 Joshua Bloch 大神的 API PDF 放在这里膜拜下:“How to Design a Good API and Why it Matters.pdf”
总述###
在设计和实现通用订单搜索API的过程中,收获了一点关于API设计的得与失。总结下,希望能给后面的工作带来有益的帮助。
什么是好的API ?
简洁、清晰、易懂、易使用。 语义行为与选项分离。
- Easy to learn
- Easy to use, even without documentation
- Hard to misuse
- Easy to read and maintain code that uses it
- Sufficiently powerful to satisfy requirements
- Easy to extend
- Appropriate to audience
得:
从交易订单搜索能力化角度思考API,充分考虑了组合性,做到足够灵活,同时兼顾可读性。能够在较少改动和发布 trade-manage 的基础上,实现多样化的需求。
失:
在过于注重灵活性功能的情况下,有一些地方做的不够好,给使用方带来很多困扰。
本文主要讨论其得与失,希望能够给后来者带来有益的启发。
好的地方###
能力化####
强调从能力的角度而不是业务场景角度来思考API设计,考虑足够灵活性。 比如
-
定义交易订单搜索能力需要的参数,而不是依赖页面传过来的参数。通过定义的参数来组合搜索能力。
-
虽然搜索页面只需要传一个订单类型,但是后台允许传多个订单类型,为后续扩展性留下空间。
实现清晰####
基于自身定义的订单搜索参数,后台实现也非常简洁,而不需要做各种参数的解析和适配。对于搜索能力的稳定性和减少BUG的发生,是非常有益的。
一体两面,总有正面和反面的地方。由于通用订单搜索API 特别强调基础层的能力化和稳定性,在基础层与业务层之间缺乏一道友好的适配层,给业务方使用也带来了不少曲折。
详细的文档####
其实通用订单搜索API 的文档也做了不少工作。 接口入参 ,代码示例, 搜索入参的分类,包括代码里 java doc 其实也写得很清楚。 为什么业务方还来咨询呢? 一个原因,确实 API 设计有失当之处(考虑了最主要的PC列表页面搜索场景),对于业务方的简单需求(比如按照下单人ID)显得过于复杂了; 另一个原因,我认为开发人员对于陌生事物还是缺乏必要的耐心。当然我其实也有这个毛病。 如果说文档没有,代码里 java doc API 也缺失,过来咨询是没问题的,但是文档 ,javadoc 注释写的很明白了,花2分钟稍微扫一下,能够学到更多东西,就不会为了一个小需求来咨询了。
但是文档确实也有写的不够的地方。
比如返回的错误信息,文档没有写明。于是联调的时候,业务方遇到“非法的调用源”,一脸懵逼。需要有一些错误提示文档 ; 比如业务方遇到其他的错误,也不知道是什么原因,只能找我们来查。
比如没有突出常用的需要的东西。往往业务方内部依赖订单搜索列表的时候,基于应用场景,可能更多依赖于 订单状态,订单类型,粉丝ID,下单人ID,维权状态, 这些可以单独抽离一个更 brief 的订单搜索接口,并辅以文档,也许就不会遭遇不知道用哪个参数的问题了。 换句话说, 原来过于依赖能力化的角度思考接口,缺失的一块是, 基于应用场景的角度来思考接口。
不好的地方###
盲增参数####
先说下背景。当时是因为老搜索接口不太灵活,且参数与前端页面耦合比较紧,因此决定设计一个新的通用订单搜索接口,以提供更加灵活的搜索能力。
第一个失,就是不加思索地把老搜索接口里的参数全部拷贝过来。 比如说 毫无卵用的 订单标记,被废弃还让业务方传错的 pageSize, pageNo 。
经验: 由于新接口上线和接入通常有一个过程,其实不必要立即支持所有可能的搜索请求,而是先支持最常用的部分,然后根据情况扩展能力。 API 参数通常是越严格越少更好,公开了就收不回来了。
-
设计新接口入参时,可以参考老接口入参,但一定要慎之又慎,只取必需的,严格把控每个参数的必要性。
-
参数一定要仅仅针对接口服务而言,不要为了图方便把所有入参都混杂到一个类里,导致API语义不清,给业务方使用带来困惑。
继承滥用####
第二个失,过于追求复用代码和语义分离,继承层次比较深。
追求代码复用是好习惯,但不能过度。尤其 API 设计,应该以“清晰易使用”为重。 如下是个反例:
经验: 继承一般是为了实现不同的语义分离,比如基础通用参数与业务参数, 但强烈建议最多两层,继承层次绝不要超过两层。
public class PaginationParam extends BaseParam { }
public class GeneralOrderSearchParam extends PaginationParam {
protected String index = "trade_es_index";
/**
* 订单查询对象
*/
protected OrderSearchParam orderSearchParam;
// ...
}
public class OrderSearchParam extends BaseParam { }
使用体验不佳####
第三个失,没有充分考虑业务方的使用体验。
层次感不强
所有的搜索参数都平铺到一个 OrderSearchParam 的字段里,没有层次感, 业务方往往只要根据一个字段搜索,却要在繁多的搜索字段里搜索。
构造入参不方便
缺乏方便的构造搜索入参的方法,业务方需要写很无聊的 set , set , set 来构造搜索入参,代码会比较难看。 可以考虑使用 Builder 模式来构造常用参数。这个方法也可以解决 层次感不强 的问题。
经验: 应该对搜索字段进行区分对待,重点突出, 可以将最常用的搜索字段聚合成一个子搜索对象,有关联语义的搜索字段聚合成一个子搜索对象。 让业务方能够快速找到所需要的。通过提供易懂的 API 示例,也可以帮助业务方更好地使用 API。
常用搜索重复构建
由于设计参数时,参数取值过于原子化,强调灵活性与可组合性,需要业务方根据搜索场景自行组装参数,当多个业务方来对接某个常用搜索场景时,每个业务方都需要写相同的代码。
if (someCond) {
setOrderTypeDesc(Arrays.asList(xEnum.name(),yEnum.name(),zEnum.name()))
}
如果后续设计有变更,每个业务方都必须修改一遍。因此,更好的做法是,若组合语义占了常用80%的情形,那么应该清晰定义这些组合语义,内部做映射消化掉,而不是委托给外部。
参数混杂####
为了图方便,直接在原来的参数里加字段,而不是创造新的参数对象,导致原有的参数对象混杂不同的功能,给业务方使用带来困惑。
比如原来有个实时详情接口的入参 OrderQueryOptions , 非实时详情接口,多了个 withDeliveryInfo 可选参数,虽然放在原来的参数对象会省不少事,可是会让接口语义不清。对应实时接口来说,它根本不需要这个参数;对于非实时接口,又混杂了实时接口的入参。 两边都不讨好。
经验: 严格对接口入参进行控制,与接口服务无关的参数不允许加入。实现细节相关的东西不加入API,不公开。
暴漏内部细节的扩展方式####
为了具有更好的可扩展能力,减少发布,设计了一个入参 extendKeywords 。当要搜索的字段不在给定搜索入参时,可以直接指定 ES 里对应的字段及操作符来搜索。 虽然灵活了些,却将业务方与底层实现紧紧绑在一起。如果以后要改用其他的搜索引擎,这种就是非常难以摆脱的困扰了。对于API设计与实现来说,都是不好的决策。
小结###
API 是服务提供的接口抽象。 一旦公开,收回来会非常困难而且费力。因此,设计API 要精思细虑,严格把关每个入参字段、继承层次不要超过两次、充分考虑使用者体验、避免参数混杂、避免暴漏实现细节等问题。
API 好书推荐:《软件框架设计的艺术》,英文书名是:《 Practical API Design: Confessions of a Java Framework Architect 》