一般在完成接口设计之后,就可以开始编码了。而对于编码来说,制定编码规范是最简单且有效的整顿方法,只要开发团队遵守一些规则进行开发,就能很大程度上避免混乱。后端应用程序的编码规范一般考虑以下几个方面:
·目录结构,需明确目录逻辑;
·限制函数调用层级,以降低代码混乱度;
·抽离公共模块,提升代码复用度;
·错误机制。
注意:以下的具体命名或规则不是唯一,可以根据偏好和具体情况而定。本小节的例子都以Java作为开发语言,以Spring Boot作为基础框架。
1.目录结构
以Spring Boot为例,当初始化工程后,目录结构如图4.43。其中,需要关心的只有三部分,分别是构造工具配置文件、后端应用程序配置文件和代码。
在初始的Spring Boot工程里,代码部分除了Application.java和ServletInitializer.java两个引导文件以外,没有其他代码。由此可知,后端应用程序的代码结构其实是开放的,开发者可以按照个人偏好规划代码目录结构。不过,Spring相关框架(Spring Boot、Spring MVC等)提供了一种分层思想,大多数使用Spring相关框架的开发者都是按照这个分层思想进行开发的。因此,代码结构最好遵循Spring相关框架的分层思想。
Spring相关框架建议将代码分成三层:Controller层、Service层和Dao层,如图4.44所示。Controller层负责管理业务调度(调用Service层),也是接口函数的入口;Service层负责实现业务功能,一般会做一些算法逻辑和调用Dao层;Dao层负责与数据库进行交互。
提倡把URL的路径分成三层,即模块、子模块和具体资源。
在实际的Spring Boot工程目录结构里,模块指的是工程名(.war包的名字);子模块指的是Controller的标识,每个子模块应该有单独的Controller.java文件(例如图4.44中的xxController.java、yyController.java);具体资源指的是对应函数的标识(“具体资源标识”加“请求方式”才能唯一标识某个特定接口函数)。
以修改用户名的接口(URL:/users/settings/username,请求方式PUT)为例,接口对应的入口函数如代码4.21所示,其中,/settings为子模块标识,/username为具体资源标识,PUT标记的是请求方式。
代码4.21 修改用户名接口对应的入口函数
package com.example.users.controller;
import …
@Controller
@RequestMapping("/settings") //子模块标识
public class SettingsController {
…
//修改用户名接口的入口函数
//具体资源和请求方式标识
@RequestMapping(value="/username", method=RequestMethod.PUT)
@ResponseBody
public JSONObject UpdateUserName(@RequestBody String requestParam) {
…
}
//其他接口的入口函数,如修改密码(URL为/users/settings/password,请求方式为PUT)
@RequestMapping(value="/password", method=RequestMethod.PUT)
@ResponseBody
public JSONObject UpdatePassword(@RequestBody String requestParam) {
…
}
…
}
综上,基本的后端应用程序工程(以Spring Boot为基础框架)的目录结构及其目录逻辑如图4.45所示。
2.限制函数调用层级
一般情况下,采用Spring Boot作为基础框架的后端应用程序都会把代码部分按Controller、Service和Dao分层(如图4.45所示)。大部分人都认为,只要代码按照这三层分层,就不会出现太大的混乱。但实际上,仅凭这三个分层,并不能规整化代码。虽然这三个分层能让代码在整体上有一个流水处理的感觉(如图4.44所示),但是在实际编码中,这三层的约束和分工都相当模糊,以至于程序内部的调用关系很大概率会出现十分混乱的局面,如图4.46所示。
一个大型网站系统,后端应用程序需要随着网站的运营不断做调整,当出现以上这种混乱的调用关系时,程序的内部会变成一张“蜘蛛网”,后端应用程序的维护和扩展都变得十分艰难,即使是原来的开发人员,也需要花很长时间去梳理。而这种混乱,并不是完备的注释和文档就能解决的,因为即使是最完备的注释和文档也不可能描述所有的代码细节和调用关系。
说明:一个软件的质量,除了功能性、效率性和稳定性以外,更关键的是其维护和扩展的难度。而维护和扩展难度其实指的是代码编写逻辑,好的代码编写逻辑应该是结构明显、调用关系整洁的。
后端应用程序是多个接口的集合,也就是多个小程序的集合。而单个接口所需要实现的功能通常是简单的,对应的代码也是很简短的,不存在过于复杂的逻辑。也就是说,后端应用程序的复杂性不在于其功能,而在于其包含多个接口。因此,横向切分后端应用程序其实并不能很好地规整代码,而是应该先垂直切分每个接口代码(每个接口的代码完全独立),再横向切分每个接口的内部代码,
如图4.47所示。这样的话,就不会出现“蜘蛛网”式的调用关系,无论是增加接口还是修改接口代码,都会变得很简单,也不会存在修改了一个接口的代码却影响了五六个接口的情况。
垂直切分其实就是限制函数调用层级,一个接口在Controller层和Service层各只有一个专属函数(Dao层可以有多个函数,但是最好不要和其他接口产生关联),接口代码不允许调用其他接口的专属函数,即以一种垂直的方式完成接口功能,如图4.48所示。其中Controller.java、Service.java和Dao.java文件也应该尽量垂直对应。
注意:一些时候,共用函数是不可避免的,特别是Dao层,这些时候可以适当地冲破一下垂直调用的规则,但不能因为这些特殊情况而放宽整个编写代码的规则。
3.公共模块
在限制函数调用层级后,在便于代码修改和代码理解的同时,自然也会增加代码的冗余程度。因此,在限制函数调用层级后,需要抽离公共模块,以达到降低代码冗余度的目的。而对于公共模块的抽取,需要对接口做的“重复事情”有一个清晰的认识。在Controller层,接口代码会做一些前期工作,如用户权限认证、必要参数检查、可选参数填充等;在Service层,接口代码会做一些业务功能的操作,如数据库调用、第三方应用调用等。那么,对于这些“重复事情”,可以将其作为公共模块抽离出来,接口代码中可以通过传入不同的参数来使用这些模块功能,如图4.49所示。
在图4.49中,把数据库操作抽离成了一个公共模块,而在前面的介绍里,数据库操作应该放到Dao层。在中小型网站当中,数据库一般都是网站的唯一核心,在这些以数据库为核心的网站系统当中,后端应用程序其实就是作为前端使用数据库的桥梁,绝大多数的Service层代码也是为了操作数据库。因此,在中小型网站当中,把数据库操作放在Dao层确实会让后端应用程序在整体上有一个清晰的逻辑。
但是,在大型网站中,数据库就不一定是网站的唯一核心了,大型网站的后端应用程除了要操作数据库以外,还需要整合其他应用程序(如视频转码服务、非关系型数据库等)。从Service层的代码来看,数据库的使用只是其功能的一部分,而不是Service层的唯一目标。因此,在大型网站当中,把数据库操作抽离成其中一个公共模块更合理一些。
不过,数据库操作还是放在Dao层比较好,因为大多数的开发者还是习惯把数据库操作放在Dao层里。在抽离公共模块后,接口代码可以写成流水线的模式,从而可以让人一目了然:第一步做了什么,第二步做了什么。而且如果这些模块做得足够好的话,可以直接在多个项目中使用,让开发者更注重接口流程,而省去很多写冗余代码的时间。增加公共模块后,后端应用程序的目录结构如图4.50所示。
说明:抽离公共模块在实际编码中就是创建类或创建函数,具体方式和规则可根据团队偏好而定。
4.错误机制
错误机制是一个经常被忽略,但却特别重要的点。在4.3.1节接口设计中也强调,返回参数中需要带有处理结果代码及其描述。
在实现了公共模块抽离后,可以在调用模块后判断处理的结果,如果发生错误的话,就直接返回错误结果,不再进行后续步骤的处理,如图4.51所示。
错误机制在流水线式的流程中可以起到“保险丝”的作用,一旦上一个步骤发生错误,下一个步骤就不会被执行,这样能保证每个步骤的健康运行。错误提示也能初步定位问题发生的位置,可以省去很多排查的工作量。一般而言,错误提示不需要太具体,如“缺少必要参数”的错误提示,不需要精确到缺少了哪些参数。
说明:程序需要坚守“不信任原则”,对于后端应用程序而言,请求参数是不可信任的。在没有完备的错误机制前提下,错误的参数可能会产出一个看似正常的结果,如一个普通用户通过某接口获取到了全部用户信息(用户信息泄露)。