乐趣区

关于后端:优秀的后端应该有哪些开发习惯

前言
毕业快三年了,前后也待过几家公司,碰到各种各样的共事。见识过各种各样的代码,优良的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优良的后端 Java 开发应该有哪些好的开发习惯。
拆分正当的目录构造
受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,而后无限度增加,到最初你就会发现一个 service 文件夹上面有几十上百个 Service 类,基本没法分清业务模块。正确的做法是在写 service 下层新建一个 modules 文件夹,在 moudles 文件夹下依据不同业务建设不同的包,在这些包上面写具体的 service、controller、entity、enums 包或者持续拆分。

等当前开发版本迭代,如果某个包能够持续拆畛域就持续往下拆,能够很分明的一览我的项目业务模块。后续拆微服务也简略。
封装办法形参
当你的办法形参过多时请封装一个对象进去 …… 上面是一个反面教材,谁特么教你这样写代码的!
public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,

               String androidId, String imei, String gaId,
               String gcmPushToken, String instanceId) {}

复制代码
写个对象进去
public class CustomerDeviceRequest {

private Long customerId;
// 省略属性......

}
复制代码
为什么要这么写?比方你这办法是用来查问的,万一当前加个查问条件是不是要批改办法?每次加每次都要改办法参数列表。封装个对象,当前无论加多少查问条件都只须要在对象外面加字段就行。而且要害是看起来代码也很难受啊!
封装业务逻辑
如果你看过“屎山”你就会有粗浅的感触,这特么一个办法能写几千行代码,还无任何规定可言 …… 往往负责的人会说,这个业务太简单,没有方法改善,实际上这都是懒的借口。不论业务再简单,咱们都可能用正当的设计、封装去晋升代码可读性。上面贴两段高级开发(伪装本人是高级开发)写的代码
@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {

ChildOrder childOrder = this.generateOrder(shop);
childOrder.setOrderId(orderId);
// 订单起源 APP/ 微信小程序
childOrder.setSource(userService.getOrderSource());
// 校验优惠券
orderAdjustmentService.validate(shop.getOrderAdjustments());
// 订单商品
orderProductService.add(childOrder, shop);
// 订单附件
orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
// 解决订单地址信息
processAddress(childOrder, shop);
// 最初插入订单
childOrderMapper.insert(childOrder);
this.updateSkuInventory(shop, childOrder);
// 发送订单创立事件
applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
return childOrder;

}
复制代码
@Transactional
public void clearBills(Long customerId) {

// 获取清理须要的账单、deposit 等信息
ClearContext context = getClearContext(customerId);
// 校验金额非法
checkAmount(context);
// 判断是否可用优惠券,返回可抵扣金额
CouponDeductibleResponse deductibleResponse = couponDeducted(context);
// 清理所有账单
DepositClearResponse response = clearBills(context);
// 更新 l_pay_deposit
lPayDepositService.clear(context.getDeposit(), response);
// 发送还款对账音讯
repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
// 更新账户余额
accountService.clear(context, response);
// 解决清理的优惠券,被用掉或者解绑
couponService.clear(deductibleResponse);
// 保留券抵扣记录
clearCouponDeductService.add(context, deductibleResponse);

}
复制代码
这段两代码外面其实业务很简单,外部预计激进干了五万件事件,然而不同程度的人写进去就齐全不同,不得不赞一下这个正文,这个业务的拆分和办法的封装。一个大业务外面有多个小业务,不同的业务调用不同的 service 办法即可,后续接手的人即便没有流程图等相干文档也能疾速了解这里的业务,而很多高级开发写进去的业务办法就是上一行代码是 A 业务的,下一行代码是 B 业务的,在上面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得十分凌乱,代码还多。
判断汇合类型不为空的正确形式
很多人喜爱写这样的代码去判断汇合
if (list == null || list.size() == 0) {
return null;
}
复制代码
当然你硬要这么写也没什么问题 …… 然而不感觉好受么,当初框架中轻易一个 jar 包都有汇合工具类,比方 org.springframework.util.CollectionUtils、com.baomidou.mybatisplus.core.toolkit.CollectionUtils。
当前请这么写
if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}
复制代码
汇合类型返回值不要 return null
当你的业务办法返回值是汇合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查问,如果没查问到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,少数场景下对于对象也是如此。
映射数据库的属性尽量不要用根本类型
咱们都晓得 int/long 等根本数据类型作为成员变量默认值是 0。当初风行应用 mybatisplus、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的我的项目外面实体类外面全都是根本数据类型。当场裂开 ……
封装判断条件
public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()

      && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
      && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
//...
return;

}
复制代码
这段代码的可读性很差,这 if 外面谁晓得干啥的?咱们用面向对象的思维去给 loanApp 这个对象外面封装个办法不就行了么?
public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {

//...
return;

}
复制代码
LoanApp 这个类中封装一个办法,简略来说就是这个逻辑判断细节不该呈现在业务办法中。
/**

  • 贷款单是否实现
    */

public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()

      && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
      && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();

}
复制代码
管制办法复杂度
举荐一款 IDEA 插件 CodeMetrics,它能显示出办法的复杂度,它是对办法中的表达式进行计算,布尔表达式,if/else 分支,循环等。

点击能够查看哪些代码减少了办法的复杂度,能够适当进行参考,毕竟咱们通常写的是业务代码,在保障失常工作的前提下最重要的是要让他人可能疾速看懂。当你的办法复杂度超过 10 就要思考是否能够优化了。
应用 @ConfigurationProperties 代替 @Value
之前竟然还看到有文章举荐应用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的益处。

在我的项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性能够疾速导航到配置类。写配置时也能主动补全、联想到正文。须要额定引入一个依赖 org.springframework.boot:spring-boot-configuration-processor。

@ConfigurationProperties 反对 NACOS 配置主动刷新,应用 @Value 须要在 BEAN 下面应用 @RefreshScope 注解能力实现主动刷新

@ConfigurationProperties 能够联合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产失落配置等问题。

@ConfigurationProperties 能够注入多个属性,@Value 只能一个一个写

@ConfigurationProperties 能够反对简单类型,无论嵌套多少层,都能够正确映射成对象

相比之下我不明确为什么那么多人不违心承受新的货色,裂开 …… 你能够看下所有的 springboot-starter 外面用的都是 @ConfigurationProperties 来接配置属性。
举荐应用 lombok
当然这是一个有争议的问题,我的习惯是应用它省去 getter、setter、toString 等等。
不要在 AService 调用 BMapper
咱们肯定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能间接调用其余的 Mapper,那特么还要其余 Service 干嘛?老我的项目还有从 controller 调用 mapper 的,把控制器当 service 来解决了。。。
尽量少写工具类
为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包外面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。本人写容易错还要加载多余的类。
不要包裹 OpenFeign 接口返回值
搞不懂为什么那么多人喜爱把接口的返回值用 Response 包装起来 …… 加个 code、message、success 字段,而后每次调用方就变成这样
CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}
复制代码
这样就相当于

在 coupon-api 抛出异样
在 coupon-api 拦挡异样,批改 Response.code
在调用方判断 response.code 如果是 FAIELD 再把异样抛出去 ……

你间接在服务提供方抛异样不就行了么。。。而且这样一包装 HTTP 申请永远都是 200,没法做重试和监控。当然这个问题波及到接口响应体该如何设计,目前网上大多是三种流派

接口响应状态一律 200
接口响应状态听从 HTTP 实在状态
佛系开发,领导怎么说就怎么做

不承受反驳,我举荐应用 HTTP 标准状态。特定场景包含参数校验失败等一律应用 400 给前端弹 toast。下篇文章会论述一律 200 的害处。
OpenFeign 接口不倡议打成 jar
见过很多应用 OpenFeign 的接口是这样用的,将 OpenFeign 接口写在服务提供方,打成 jar。比方服务 A 调用 B,在 B 我的项目独自开一个 module 写接口定义,打出一个 jar 包让 A 引入依赖。
让咱们来感受一下调用一个 Feign 接口实现的步骤:

在 B 服务中写 Controller 实现
在 B 服务中定义 OpenFeign 接口定义
在 B 服务中批改 jar 版本 +1,打一个 jar 包到本地仓库
在 A 服务中批改依赖 jar 版本,刷新 maven/gradle

乍一看不麻烦是吧?然而你要晓得咱们开发中常常会呈现丢参数、缺响应属性等状况,一旦有任何小问题,都要从新走一遍上述流程。。。。
倡议将 OpenFeign 接口定义在生产端 A,B 只须要提供一个接口实现即可。所不好的中央无非是 XxxRequest、XxxResponse 类冗余了一份,但其实并没有什么问题,因为对于 Feign 来说申请和响应的 BO 类并不需要字段完全一致,它的解码器会智能的解析响应并封装到你的 XxxResponse 接管类中。

你这么了解就明确了,这个类 XxxRequest、XxxResponse 等,仅仅是你的 A 服务为了映射申请后果而本地自定义的一个映射数据结构,这个映射数据结构和 B 服务能够说是没关系的。所以你当然应该放在 A 这里。

你很纠结无非是你感觉这个货色仿佛是能够复用的,所以纠结放 A 还是放 B,以及是不是要抽出来做个公共依赖。我很久以前也很纠结这个货色,然而踩了太多坑当前我的想法就变了,高内聚低耦合实质的意义,就是把和一个服务(组件,利用,包,等等等等)相干的代码全部包在一起,不要和外界有牵扯,你有牵扯就会引发批改时的依赖天堂。

写有意义的办法正文
这种正文你写进去是怕前面接手的人瞎么 …… 你在前面写字段参数的意义啊 ……
/**

  • 申请电话验证
    *
  • @param credentialNum
  • @param callback
  • @param param
  • @return phoneVerifyResult
    */

public void method(String credentialNum,String callback,String param,String phoneVerifyResult){

}

复制代码
要么就别写,要么就在前面加上形容 …… 写这样的正文被 IDEA 报一堆正告看着不好受?
和前端交互的 DTO 对象命名
什么 VO、BO、DTO、PO 我倒真是感觉没有那么大必要分那么具体,至多咱们在和前端交互的时候类名要起的适合,不要间接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要非凡解决。
举荐的做法是承受前端申请的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:承受保留更新订单信息的实体类能够定义为 OrderRequest,订单查问响应定义为 OrderResponse,订单的查问条件申请定义为 OrderQueryRequest。
不要跨服务循环拜访数据库
跨服务查问时,如果有批量数据查问的场景,间接写一个批量的 Feign 查问接口,不要像上面这样
list.foreach(id -> {

UserResponse user = userClient.findById(id);

});
复制代码
因为每一次 OpenFeign 的申请都是一个 Http 申请、一个数据库 IO 操作,还要通过各种框架内的拦截器、解码器等等,全都是损耗。
间接定义一个批量查问接口
@PostMapping(“/user/batch-info”)
List<UserResponse> batchInfo(@RequestBody List<Long> userIds);
复制代码
这就完结了吗?并没有,如果你遇到这种 userIds 的数量十分大,在 2000 以上,那么你在实现方不能在数据库中间接用 in() 去查问。在实现方要拆分这个 useIds。有索引的状况下 in() 1000 个元素以下通常问题不大
public List<XxxResponse> list(List<Long> userIds) {
List<List<Long>> partition = Lists.partition(userIds, 500); // 拆分 List

List<XxxResponse> list = new ArrayList<>();
partition.forEach(item -> list.addAll(xxxMapper.list(item)));
return list;
}
复制代码
尽量别让 IDEA 报警
我是很恶感看到 IDEA 代码窗口一串正告的,十分好受。因为有正告就代表代码还能够优化,或者说存在问题。
前几天捕获了一个团队外部的小 bug,其实原本和我没有关系,然而共事都在一头雾水的看里面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到汇合中就变成了 Integer,而后 stepId 点下来一看是 long 类型,在汇合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。
你看如果重视到正告,鼠标移过去看一眼提醒就分明了,少了一个生产 bug。
尽可能应用新技术组件
我感觉这是一个程序员应该具备的素养 …… 反正我是喜爱用新的技术组件,因为新的技术组件呈现必然是解决旧技术组件的有余,而且作为一个技术人员咱们应该要与时俱进~~ 当然前提是要做好筹备工作,不能无脑降级。举个最简略的例子,Java 17 都进去了,新我的项目当初还有人用 Date 来解决日期工夫 …… 都什么年代了你还在用 Date
结语
本篇文章简略介绍我日常开发的习惯,当然仅是作者本人的见解。临时只想到这几点,当前发现其余的会更新。
如果这篇文章对你有帮忙,记得点赞加关注!你的反对就是我持续创作的能源!

退出移动版