项目的业务逻辑层需要被设计成一个具备易扩展的模式,对外提供了大小相异的 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 来说明,如果接口简单,完全可以只需要很少的说明,成为自注释的。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
queryUserInfo(Map queryCriteria) — 这个 map 参数,参数的边界变得无限,怎么测试方法功能的边界?
这篇文章好早了。我觉得可以给每个属性配置 validation 的逻辑啊,这就可以测试边界了。