问题描述
有一个在线博客系统,系统提供了一个API,前端只需要传递参数:(开始日期,结束日期),然后就会返回一个这样的JSON:{日期1:新文章数量,日期2:新文章数量...}
。现在来了一个新的需求:用户需要查看当天,本周,本月,最近半年或者一年新发布的文章的数量。现在需要设计后端API供前端调用,那么这个API应该如何设计呢?
一开始,我想到了3种可能的方案:
- 直接使用之前的API,前端根据天,周,月等单位换算成时间区间,去后端查询出每天的新文章数量,然后在前端累加。
- 设计5个API,然后每个API处理不同的单位。
- 设计一个API,然后有一个枚举类型的参数表示5种不同的情况。
我的第一感觉是:方案1是最简单的,方案2看起来好像也可以,方案3感觉有点复杂了。我到底应该选择哪一种方案呢,每种方案的利弊是什么呢?
方案一
该方案很简单而且看起来很灵活,后端提供一个API,既可以用来获取每天新文章具体数目,又可以用来计算该区间内新总和,那么该方案有什么问题吗?
我觉得这个方案最大的问题就是暴露了领域知识在前端,这里体现出来的就是前端人员需要计算本周的区间,本月的区间,本年的时间区间。当然这个知识很简单,前端人员肯定都知道怎么换算。但是这确实不应该由前端来处理,为什么呢?
- 我个人觉得前端人员的职责主要就是单纯的调用后端的API,然后将数据展示出来。前端人员只需要知道哪些API是来干什么的以及调用的顺序即可。
- 单位的转换确实应该由后端完成。单位的概念也属于领域的知识,本例子中的年月日比较简单,但如果是(点,刻,字)这种时间单位呢?后端处理数据,数据的单位转换就应当交给后端完成。
举个详细的例子来说明由后端处理的好处:查询2017年9月的新文章数量。如果后端来做2017年9月的查询,那么就有这几种很好的实现:
- 换算成区间,然后使用之前API的代码查询并对结果求和,之后将结果缓存起来。
- 后端可以基于时间列创建日期列(如果数据库是MySQL可以使用Virtual columns),然后在日期列上创建索引。甚至查询结果也可以缓存起来。
方案二和方案三
方案二和方案三都没有方案一的问题。之所以现在要将这两个放在一起说,是因为这两个的关系有点类似于面向对象设计里面的FlagArgument 问题。其建议不要提供一个唯一的API,然后通过额外的参数表示不同的行为,而是推荐提供多个表示不同行为的API。
Martin Fowler给出的为什么不要使用FlagArgument的原因如下:
class Concert... public Booking book (Customer aCustomer, boolean isPremium) {...}
VS
class Concert... public Booking regularBook(Customer aCustomer) {...} public Booking premiumBook(Customer aCustomer) {...}
My reasoning here is that the separate methods communicate more clearly what my intention is when I make the call. Instead of having to remember the meaning of the flag variable when I see
book(martin, false)
I can easily readregularBook(martin)
.
从可读性和可维护性说起,假如regularBook的处理逻辑需要修改,那么第二种方式可以更好的定位到所有使用了reqularBook逻辑的地方,第一种方式则比较麻烦。但是这种情况并不是绝对的,在编程语言中的关键字参数或者枚举就可以绕过这个问题:
- 比如在Python中,我们可以使用关键字参数,调用方式大概如下
ins.book(customer,isPremium=True)
- 使用或者使用枚举:
ins.book(customer,PriceType.Premium)
现在讨论其扩展性,假如新增了一种价格类型,第一种方式需要将isPremium变成一个可以表示3种情况的枚举,而第二种方式则需要增加一个API,如果情况很多,那么第二种方式将会有大量的API产生。大量API主要会带来什么问题?我觉得主要看调用该API的人是谁,如果是后端自己用的API那么没什么问题,但是如果要给前端调用就有问题了:假如前端不关心价格类型。
举个例子:如果是第一种方式的API,后端需要告诉告诉前端:Hi Jay,我写了一个预定的API,到时候你传递用户编号和价格类型过来给我就可以了。如果是第二种方式则是:Hi Jay,我写了一系列用于预定的API,所有API你都需要传递用户编号过来,如果是XX价格类型,你就调用XXX API,如果是YY价格类型,你就调用YYY API,如果是...。
如果前端又需要关心价格类型,那么仍然可以采用第一种方式,因为访问后端的API时可以提供命名参数,如:book?price_type=premium
。
因此在博客系统案例中,我最终选择了方案三,设计的API如下:
GET /recent?range=1m
range值范围:1d 1w 1m 6m 1y...
网友评论