目录
前言
实际工作中大部分时间都是在写业务逻辑,一般都是三层架构,表示层(Controller)接收客户端请求,并对入参做检验,业务逻辑层(Service)负责处理业务逻辑,一般开发都是在这一层中写具体的业务逻辑。数据访问层(Dao)是直接和数据库交互的,用于查数据给业务逻辑层,或者是将业务逻辑层处理后的数据写入数据库。
简单的增删改查接口不用多说,基本上写好一部分,剩下的都是复制粘贴然后稍微修改一下就行了,有些甚至可以用工具来直接生成。不过,系统中还是会存在一些相对复杂的接口,一般都是系统的核心功能,其中的业务逻辑就复杂多了,代码量少说也得大几百行才能够实现功能,这种代码往往就是“屎山”了。往往后续接手的人在不熟悉整体业务逻辑的情况下,再加上代码量大,代码编写不规范的话,维护起来相当痛苦。
经历过维护“屎山”的痛苦,到自己开发“屎山”,自然希望在自己能够遵循代码规范,开发出一座相对好维护,可读性更高的“屎山”,让自己多积点阴德,多攒点人品。这里分享下写“屎山”代码的心路历程
需求场景
开发一个 api 调用服务,让用户自定义要调用 api 的各种配置,例如 api 的 url 地址,请求方式,query 参数名,header 参数名,body 参数名(表单或者json),api 返参等等,配置完成后,即可提供给其他系统模块进行调用。
api 提供一个调用的接口,其他系统模块调用时按照 api 配置的输入参数名称,传入对应的输入参数值,返回 api 调用模块配置的返参。
调用接口的整体逻辑可以抽象成以下几个步骤
- 获取 api 配置信息
- 构造请求参数
- 构造请求头
- 构造请求体
- 校验必填字段
- 调用 api
- 校验 api 返回值
- 返回 api 返回值
将业务逻辑分解后,就可以直接开始写代码了。
常规写法
将所有的业务逻辑都放在一个方法中处理。
public class ApiService {
public Object call(Input input) {
//获取 api 配置信息
//构造请求参数
//构造请求头
//构造请求体
//校验必填字段
//调用 api
//校验 api 返回值
//返回 api 返回值
}
}
优点:
- 业务逻辑分解合理,再加上每一段业务有对应的注释的话,可读性还是很好的。(理想情况下,实际上可能就没有注释,也没有拆分业务逻辑,全都堆在一起了)
- 业务逻辑都在一个方法里,省去了各种变量的传参,开发一时爽。
缺点:
- 业务处理复杂导致一个方法代码行数太多了(大几百行甚至上千行),而一个显示屏幕一般也就显示几十行,给看代码的人增加了负担。
- 代码行数多,无法直接一眼了解整体的业务流程,必须看完整个方法才能了解整个业务流程。
- 可能出现一个变量横跨几百行的情况,不好维护。
拆分方法
分解业务逻辑后,将每段的业务逻辑都用一个 private 方法封装起来。
public class ApiService {
public Object call(Input input) {
//方法的出入参根据实际业务各不相同,这里只是简单表示下拆分方法去分解复杂业务逻辑
Object info = getBaseInfo(param ...);
Object query = buildQuery(param ...);
Object header = buildHeader(param ...);
Object body = buildBody(param ...);
checkRequiredField(param ...);
Object result = invoke(param ...);
checkReturnResult(param ...);
return result;
}
private Object getBaseInfo(Param param ...) { ... }
private Object buildQuery(Param param ...) { ... }
private Object buildHeader(Param param ...) { ... }
private Object buildBody(Param param ...) { ... }
private boolean checkRequiredField(Param param ...) { ... }
private Object invoke(Param param ...) { ... }
private boolean checkReturnResult(Param param ...) { ... }
}
优点:
- 将业务逻辑拆分到方法后,就能从最外层的方法上一眼了解整个业务逻辑了,而不用去通读整个方法,可读性更好。
- 大方法拆分成小方法,每个方法职责越小,越好维护。
缺点:
- 分解业务逻辑到不同的小方法后,不同的方法之间如果需要用到同一个变量,则需要通过入参来传递该变量,业务复杂的情况下,共享变量会越多,方法入参也就变得非常多。
- Java 的方法只能有一个返回值,但是有可能会出现一个方法需要返回多个值到外层整体方法,然后将返回值作为其他方法的入参,处理起来非常不优雅。
- 业务越复杂,拆分的 private 方法越多,在整个 Service 类中的占比越大,这些 private 方法对于 Service 类中的其他 public 方法是不相干的,但是却还是在同一个 Service 类下,显得类非常臃肿。
领域对象
为了解决上面所有的痛点,可以抽象出一个专门处理这个业务逻辑的领域对象
public class ApiService {
public Object call(Input input) {
//方法的出入参根据实际业务各不相同,这里只是简单表示下拆分方法去分解复杂业务逻辑
ApiDo apiDo = ApiDo.build(input);
return apiDo.getBaseInfo(param ...)
.buildQuery()
.buildHeader()
.buildBody()
.checkRequiredFields(param ...)
.invoke()
.checkReturnResult(param ...)
.getResult();
}
}
public class ApiDo {
Input input;
//业务处理中用到的业务变量
Object field1;
Object field2;
Object result;
public static build(Input input) {
this.input = input;
}
public ApiDo getBaseInfo(param ...) {
//处理业务逻辑
return this;
}
public ApiDo buildQuery() {
//处理业务逻辑
return this;
}
public ApiDo buildHeader() {
//处理业务逻辑
return this;
}
public ApiDo buildBody() {
//处理业务逻辑
return this;
}
public ApiDo checkRequiredFields(param ...) {
//处理业务逻辑
return this;
}
public ApiDo invoke() {
//处理业务逻辑
return this;
}
public ApiDo checkReturnResult(param ...) {
//处理业务逻辑
return this;
}
public ApiDo getResult() {
//处理业务逻辑
return result;
}
}
使用领域对象,可以同时拥有上面写法的共同优点,同时又可以避免掉它们的缺点,相对来说,更加地面向对象编程。
- 抽象出了领域对象,使得 Service 层更加精简,不再需要臃肿的 private 方法。
- Service 层方法就能够一眼了解整体的业务逻辑。
- 方法的链式调用,天然具有很好的可读性。
- 可以使用对象的成员变量来保存不同方法共同需要的业务变量,精简方法的入参。
总结
针对复杂的业务逻辑,可以通过抽象出一个领域对象来处理,分解业务逻辑到领域对象的方法中, 使用建造者模式创建领域对象,然后链式调用方法,这样处理,能使代码的可读性更高,也更加地面向对象编程。提高代码可读性,积阴德,攒人品。