API风云录

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

项目的业务逻辑层需要被设计成一个具备易扩展的模式,对外提供了大小相异的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来说明,如果接口简单,完全可以只需要很少的说明,成为自注释的。

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

分享到:

2 comments

  1. 关于接口设计…

    也是来自之前的邮件,时时提醒吧。 保持良好接口机制的同时,还要注意避免歧义。 iPad组正在做的消息…

  2. [...] 向后兼容。当然,这一点也可以归纳到前文提到的一致性里面去。我曾经拿JDKHashTable举了一个例子,它的containsValue和contains方法其实是一样的,造成这种情况的一个原因就是为了保持向后兼容。 [...]

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Preview on Feedage: