欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

API风云录

程序员文章站 2022-04-14 10:44:45
...

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

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

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

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

 

 

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来管理:

UserManagementUtil.getInstance().getUser(evt);
 

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

……

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

 

--------------------------------------------------------------------------------

 

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

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

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

 

 

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模型最初设计的相关接口有:

 

 

queryUserInfoByName(String name) 
queryUserInfoByAccountNumber(String accountNumber) 

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

 

queryUserInfo(Map queryCriteria) 

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

 

 

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

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

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

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

 

最后我用一张Google API的截图结束本文:


API风云录
            
    
    博客分类: API Design API 
 

 

文章系本人原创,转载请注明作者和出处

  • API风云录
            
    
    博客分类: API Design API 
  • 大小: 110.6 KB
相关标签: API