Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

API 风云录

Posted on 02/27/201110/08/2024 by 四火

1 好吧,我承认我是标题党,还是让我们从一个故事开始吧。

项目的业务逻辑层需要被设计成一个具备易扩展的模式,对外提供了大小相异的 API。项目组人人头脑风暴,最后在各位的努力下,克服苦难,业务逻辑层被封装起来,一组最初的 API 被提供出来:

1、现有 Service 逻辑已经疏于管理,欠缺重构,变成了不易控制的逻辑层,接口众多,鱼龙混杂,难以规整出清晰、可用的接口给第三方(例如下游定制团队),怎么办? 
Web 应用有个特点,当你对代码的管理缺乏控制而搞不定时,可以在其上封装一层,这是一个通用的解决办法,也是一柄双刃剑。 
正如某同事“ 我们都是工程商人” 所述,心底里可能我们愿意追求代码的曼妙和清晰,但是大多数软件项目都应建立在“ 赚钱” 的前提之上。 
于是大家各抒己见,最终将原有 Service 之上的 Facade 改头换面,重新整顿了一把,变成了 XXXManagementUtil。

2、有人考虑扩展方便,将这些 API 门面类中的方法参数,全部使用 Event 封装起来,这样的好处在于添加参数的情况下不必修改任何接口方法:

1
2
3
class UserManagementUtil{  
    public UserInfo getUser(GetUserEvt);  
}

3、Event 里面的参数哪些可选、哪些必选?应当满足怎样的取值规则?于是在 Event 的接口中引入了 verify 方法,子类中重写了 toString 方法,并利用 Spring 的 AOP 机制,在 API 调用时进行参数校验并打印日志。

4、API 的接口应该根据什么来划分呢?
按照模型驱动来划分吧,有声音说,比如 UserManagementUtil、SongManagementUtil。
可是更多的声音说:业务中有 Song、Music 等等二十多种内容类型,不觉得太庞大了吗?还是把 Song、Music 等发布的内容涉及到的 API 都归结到 ContentManagementUtil 和 ContentExtManagementUtil 里面吧,不要细分了。

5、API 的接口粒度应该怎样控制呢?
有同事表示,提供简易的接口,就如同 Windows 提供的 API 一样,那么,我们提供基于模型的 CRUD 方法吧,这样方法既原子、纯净,通过外部调用者一定的组合,又能满足外部调用的功能。

6、怎样让 API 便于外部调用呢?
一开始要求外部使用 Spring 注入的方式来使用 API 的建议遭到了一些反对,我们不是要让调用方用得灵活方便、降低定制难度对吗?
又有一个声音说:把方法都变成 static 吧?于是又遭来一些反对的声音,static 方法可能带来 API 中资源依赖和资源初始化的问题。
最后 API 外部的调用变成了下面这样,而 API 内依然由 Spring 来管理:

1
UserManagementUtil.getInstance().getUser(evt);

7、怎样让 API 便于定制人员理解呢?
需要强化 API 的 JavaDoc,其中需要包含足够的方法功能和流程、参数以及返回值的说明。

……

于是大家大干一场,API 渐渐新鲜出炉了,一切看起来是那么美好。

——————————————————————————–

不过数日之后,许多人渐渐开始发现,看起来那么美好的事情实际上好像也并不那么美好:

1、考虑到外部接口调用功能和性能的问题,通用和简单的接口已经完全无法满足业务需要,多次 API 调用可能意味着多次与数据资源交互,如果能把多次交互合并成一次(例如底层使用一次数据库连表查询实现),性能就可以大大提升。于是一些功能庞大的接口开始出现,并且愈来愈不受控制了。

2、方法参数 Event 的境遇如何呢?也不好。调用者并不十分清楚 Event 中哪些参数是必选的,那么,就把所有参数都传进去吧!于是 API 以外的 Action 层面到处是这样丑陋的代码:

1
2
3
4
5
6
GetUserEvt event = new GetUserEvt();  
event.setAccountName("13000000000");  
event.setAddress("xxx");  
event.setType(xx);  
event.setAge(xx);  
……(此处省略 N 行)

3、verify 方法呢?toString 方法呢? 
随着项目的进行,这些都变得不可控了,写一个简单根据 ID 来获取模型 POJO 的方法,就要写一个 Evt,还有一堆冗长的 verify 和 toString 方法,开发人员变得不那么情愿,这两个方法就写得越来越简陋了。 
更糟的是,这里用到的 Spring 的 AOP 方式还在项目中被发现为性能瓶颈,于是有更多的人开始怀疑最初这个决定的正确性了。

4、ContentManagementUtil 和 ContentExtManagementUtil 变得非常庞大,困惑越来越多,一些方法也不知该往哪放了,比如 getContentsByCategory 方法,到底是放到 ContentManagementUtil 里呢,还是放到 CategoryManagementUtil 里呢?

5、JavaDoc 变成了真正定制的瓶颈,定制人员不断表示无法读懂 JavaDoc,不知道该怎样调用 API;而开发人员呢,则不断抱怨 JavaDoc 工作量巨大,要把一个接口的 JavaDoc 写清楚,需要描述接口内部流程、参数名称、参数含义、使用场景、不同场景下需要哪些参数、返回对象含义、异常类型、异常返回码可能的取值、调用示例…… 一句话,变得无比困难。

6、需求变化频繁,当接口版本更新时,接口调用者发现,糟糕,原来的方法调用变得不可用了,但是是哪个参数不正确或缺失造成的呢?也没法看出来。

……

 

——————————————————————————–

 

这就是一个简简单单的 API 风云录,一段 API 诞生和撞到新秀墙的困惑史,一切看起来都很自然,也许你感到些许熟悉,我就说到这里,如果有一些感触,这些真实的记录就变得有价值。

我说完了。

……

 

可是我怎么甘心就这么“ 说完了”?

这不是我的风格。

我要痛批一顿?要引出所谓“ 真正正确” 的做法?

当然不是!这样的事情还是留给专家学者去做吧。

 

——————————————————————————–

 

API 的设计是软件架构设计的细化和缩影,是一件持续的工作,一样没有银弹,一样没有一劳永逸的可能。它历来是一个难题,无论在最初看似多么“ 完美” 的规划和安排,最后都可能变成鸡肋;而且,看起来越强大、兼容性越好的设计,就越有可能打了水漂。

1、既然给不出一个完整和绝对正确的办法,API 从诞生之日起,就需要开发人员不断对其修整和维护,使之不断适应当前应用需要,从而避免其老化。软件的架构需要维护,一个再出色的架构师完成了他的设计,如果开发团队不能贯彻并把基本的架构思想传递下去,项目一样还会偏离预想的轨道(不一定是好或者坏的结果,但通常都是不合预期的),这一点,API 也一样,设计人员应当参与 API 一版版的发展和发布。 
对于一些业务性很强的 API,需要 API 编写的门槛会提高,需要开发人员理解 API 的原则,清楚细化了的要求。

2、设计一个易用、简单和清晰的 API 基本规则,在 API 发展的过程中,大小规模的重构不可避免且理所应当,基本规则就像软件架构一样,不会轻易变更,最初设定的规则越复杂,后续变更和成熟的过程越痛苦。重构本身就是版本发展的一部分,更多的特性应当在后续的重构中丰满,而不是在最初预留好一个准许“ 上帝功能” 都能便捷扩展的能力。

3、保持基础接口的兼容性。JDK 的 HashTable 有一个 containsValue 方法,还有一个 contains 方法,二者功能上完全一样,之所以搞这样两个完全一样的方法,正是由于历史原因造成的。JDK1.2 才正式引入 Java Collections Framework,抽象了 Map 接口,才有了 containsValue 方法,而之前的方法因为向下兼容的原因无法删除。我不能评估说这是一个好的兼容还是一个糟糕的妥协,但至少,JDK 都为了保持了基础接口的兼容性而做了这样一件看似不合理的事。 
有一种不激进的思路是,给一些将要废弃的接口置为 @deprecated,待若干个版本后可以选择删除。

4、给 API 设计人员以充分的信任。API 的设计不是民主选举,少数服从多数,把不可抗拒的要求和额外的需求陈述清楚,就不应过于干涉其组织的讨论。通常软件的设计都有这样一个惯性问题,不是最终采纳合理的方案、成熟的方案,而是采纳具备话语权的人的意见,或是经过民主式的妥协来完成设计。

5、严格控制接口数量。性能、可维护性,二者谁更重要?事实上这两者在很多情况下都是一组矛盾,平衡二者的关系才是设计者应当考虑的。如果把数个行为放置到一个接口中,当然可以提升性能,但是也增加了接口,增大了维护成本。尤其对于成熟的 API 来说,每增加一个接口都应是慎重的行为,如果项目组自我管理能力不够,就需要专人集中守护。

6、发布稳定和成熟的 API。业界有一句玩笑话叫“ 不要使用 3.0 版本以下的软件”,正是说的这个道理,经过少数几轮迭代的 API 还远不稳定,而且可能还有众多 bug,后续大规模的变更就会令 API 失去价值。如果由于不可抗因素,API 变更实在太大,考虑提纯 API 的功能,尽量简单的方法,将复杂的关系条件交给调用方,会减小需求变更带来的冲击。

举例来说,UserInfo 模型最初设计的相关接口有:

1
2
queryUserInfoByName(String name)  
queryUserInfoByAccountNumber(String accountNumber)

但倘若模型变更频繁,那么可以考虑设计这样的接口:

1
queryUserInfo(Map queryCriteria)

其中的参数 QueryCriteria 代表着了查询条件,比如这样调用:

1
queryUserInfo({name:"%abc%",accountNumber:"139%"})

(降低了可读性和调用的便捷程度,但提高了接口稳定性)

7、接口尽量独立,避免发布互相之间有依赖关系的接口。如果实在避免不了,最好让两个有依赖关系的接口放置在相近的地方,以便查看。

8、接口必须被完全理解,最好简洁易懂。如果接口复杂,那你可能寄望于详尽的 JavaDoc 来说明,如果接口简单,完全可以只需要很少的说明,成为自注释的。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

你可能也喜欢看:

  1. 关于接口设计,还有 Fluent Interface,这种有趣的接口设计风格
  2. 对象转换的问题
  3. 动手实现随机验证码
  4. 画圆画方的故事
  5. JavaScript 实现继承的几种方式

2 thoughts on “API 风云录”

  1. Anonymous says:
    02/09/2017 at 5:05 PM

    queryUserInfo(Map queryCriteria) — 这个 map 参数,参数的边界变得无限,怎么测试方法功能的边界?

    Reply
    1. 四火 says:
      02/12/2017 at 12:35 AM

      这篇文章好早了。我觉得可以给每个属性配置 validation 的逻辑啊,这就可以测试边界了。

      Reply

Leave a Reply to Anonymous Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Spark 互联网 亚马逊 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 眼界 程序员 管理 系统设计 缓存 编程范型 美股 英语 西雅图 设计 问题 面向对象 面试

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Machine Learning and Artificial Intelligence (6)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • panshenlian.com on 初涉 ML Workflow 系统:Kubeflow Pipelines、Flyte 和 Metaflow
  • panzhixiang on 关于近期求职的近况和思考
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
  • Anonymous on 我裸辞了
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme
Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接