一个查询API引发的对前后端职责的思考

问题描述

有一个在线博客系统,系统提供了一个API,前端只需要传递参数:(开始日期,结束日期),然后就会返回一个这样的JSON:{日期1:新文章数量,日期2:新文章数量...}。现在来了一个新的需求:用户需要查看当天,本周,本月,最近半年或者一年新发布的文章的数量。现在需要设计后端API供前端调用,那么这个API应该如何设计呢?

一开始,我想到了3种可能的方案:

  1. 直接使用之前的API,前端根据天,周,月等单位换算成时间区间,去后端查询出每天的新文章数量,然后在前端累加。
  2. 设计5个API,然后每个API处理不同的单位。
  3. 设计一个API,然后有一个枚举类型的参数表示5种不同的情况。

我的第一感觉是:方案1是最简单的,方案2看起来好像也可以,方案3感觉有点复杂了。我到底应该选择哪一种方案呢,每种方案的利弊是什么呢?

方案一

该方案很简单而且看起来很灵活,后端提供一个API,既可以用来获取每天新文章具体数目,又可以用来计算该区间内新总和,那么该方案有什么问题吗?

我觉得这个方案最大的问题就是暴露了领域知识在前端,这里体现出来的就是前端人员需要计算本周的区间,本月的区间,本年的时间区间。当然这个知识很简单,前端人员肯定都知道怎么换算。但是这确实不应该由前端来处理,为什么呢?

  1. 我个人觉得前端人员的职责主要就是单纯的调用后端的API,然后将数据展示出来。前端人员只需要知道哪些API是来干什么的以及调用的顺序即可。
  2. 单位的转换确实应该由后端完成。单位的概念也属于领域的知识,本例子中的年月日比较简单,但如果是(点,刻,字)这种时间单位呢?后端处理数据,数据的单位转换就应当交给后端完成。

举个详细的例子来说明由后端处理的好处:查询2017年9月的新文章数量。如果后端来做2017年9月的查询,那么就有这几种很好的实现:

  1. 换算成区间,然后使用之前API的代码查询并对结果求和,之后将结果缓存起来。
  2. 后端可以基于时间列创建日期列(如果数据库是MySQL可以使用Virtual columns),然后在日期列上创建索引。甚至查询结果也可以缓存起来。

方案二和方案三

方案二和方案三都没有方案一的问题。之所以现在要将这两个放在一起说,是因为这两个的关系有点类似于面向对象设计里面的FlagArgument 问题。其建议不要提供一个唯一的API,然后通过额外的参数表示不同的行为,而是推荐提供多个表示不同行为的API。

Martin Fowler讨论FlagArgument时使用了下面的例子:

// 1
class Concert...
public Booking book (Customer aCustomer, boolean isPremium) {...}
//2
class Concert...
public Booking regularBook(Customer aCustomer) {...}
public Booking premiumBook(Customer aCustomer) {...}

他给出了不要使用FlagArgument的主要原因:

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 read regularBook(martin).

从可读性和可维护性说起,假如regularBook的处理逻辑需要修改,那么第二种方式可以更好的定位到所有使用了reqularBook逻辑的地方,第一种方式则比较麻烦。但是这种情况并不是绝对的,在编程语言中的关键字参数或者枚举就可以绕过这个问题:

  1. 比如在Python中,我们可以使用关键字参数,调用方式大概如下ins.book(customer,isPremium=True)
  2. 使用或者使用枚举: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...

参考资料

https://softwareengineering.stackexchange.com/questions/359452/how-do-i-design-a-backend-api-with-a-different-query-range

https://softwareengineering.stackexchange.com/questions/147977/is-it-wrong-to-use-a-boolean-parameter-to-determine-behavior

https://martinfowler.com/bliki/FlagArgument.html

分享到