如何写好日常代码以及一些常见的坑
1、方法命名潜在的坑
1.1、案例伪代码示例
@Data
public class Order {
private String customerName;
private BigDecimal totalAmount;
private String statusCode;
private static final String payingStatus = "paying";
/**
* 是否支付中
*
* OrderInfo#verifyOrderPaymentStatus
* @return
*/
public boolean isPaying(){
return statusCode.equals(payingStatus);
}
}
public static void main(String[] args) {
Order order = new Order();
// 这里会报空指针异常
System.out.println(JSON.toJSONString(order));
}
1.2、问题和改进建议
在三层架构下,我们通常将原来很多本该属于对象的行为的方法写在Service上,这种方式相比DDD我们称之为贫血模型,但是随着DDD越来越流行,大家都开始接受DDD充血模型去写代码,充血模型其实就是回归到面向对象(OOP),将原来属于对象Bean的行为方法写回到该对象去,但是对象Bean通常作为打印关键业务日志的主要数据对象,这里对于行为方法命名就要小心,除了本身Bean的getter、setter方法外,其它带逻辑性的行为不要用 get\set\is 开头,否则打印日志json序列化(JSON.toJSONString(...))默认会执行get\set\is 开头的方法,业务逻辑方法通常在特定场景下确保字段都有数据的情况下被调用,但是在打印日志时可能这些字段存在为null的情况,会导致报空指针,如果一定要这么命名,建议在方法上注解JSON序列化忽略标记上去,而且不同的JSON框架忽略注解都不同,这种方式可以避免但是会使代码变得很不优雅。
2、查询类API最佳实践
2.1、案例伪代码示例
@RestController
public class QueryApiController {
@GetMapping("/getOrderByCustomerCode")
public R getOrderByCustomerCode(@RequestParam("customerCode") String customerCode) {
//业务查询操作
return R.success();
}
@GetMapping("/getOrderByOrderNo")
public R getOrderByOrderNo(@RequestParam("orderNo") String orderNo) {
//业务查询操作
return R.success();
}
}
2.2、问题和改进建议
前面控制器针对客户编码、订单号分别创建了两个对外暴露的api,这种做法容易使代码查询逻辑分散,随着项目不断迭代,整个控制器api清单会冗余臃肿,容易产生冗余代码,维护和后期迭代坑比较多,而且对客户端也不友好,最佳做法应该合并起来统一1个条件查询api,入参改为对象的方式,改进后代码示例:
//改为条件查询,收缩api开放数量,减少维护和管理入口
@PostMapping("/getByCondition")
public R getByCondition(@RequestBody OrderReq req) {
//业务查询操作
return R.success();
}
3、通过拆分子方法编排流程
3.1、案例伪代码示例(面条式代码)
public class OrderHeadService {
@Autowired
private OrderHeadRepository orderHeadRepository;
@Autowired
private OrderLineRepository orderLineRepository;
@Autowired
private IntegralRepository integralRepository;
@Autowired
private StockRepository stockRepository;
@Autowired
private CapitalRepository capitalRepository;
/**
* 创建订单
* @param orderHead
*/
public void createOrder(OrderHead orderHead){
//校验订单头参数
if (orderHead == null){
throw new RuntimeException("参数订单头对象不能为空");
}
if (CollectionUtils.isEmpty(orderHead.getOrderLineList())){
throw new RuntimeException("订单明细不能为空");
}
if (StringUtils.isEmpty(orderHead.getCustomerCode())){
throw new RuntimeException("客户编码不能为空");
}
if (StringUtils.isEmpty(orderHead.getMobile())){
throw new RuntimeException("联系电话不能为空");
}
if (StringUtils.isEmpty(orderHead.getReceiveAddress())){
throw new RuntimeException("收货地址不能为空");
}
//校验订单行参数
orderHead.getOrderLineList().forEach(orderLine -> {
if (orderLine == null){
throw new RuntimeException("订单行对象不能为空");
}
if (StringUtils.isEmpty(orderLine.getLineNo())){
throw new RuntimeException("客户编码不能为空");
}
if (StringUtils.isEmpty(orderLine.getSkuCode())){
throw new RuntimeException("sku不能为空");
}
if (orderLine.getQuantity() == null || orderLine.getQuantity() < 1){
throw new RuntimeException("数量不能为空");
}
if (orderLine.getUnitPrice() == null){
throw new RuntimeException("单价不能为空");
}
});
//占用库存
orderHead.getOrderLineList().forEach(orderLine -> {
int stockQuantity = stockRepository.queryStock(orderLine.getSkuCode());
if (stockQuantity < orderLine.getQuantity()){
throw new RuntimeException("库存不足 sku" + orderLine.getSkuCode());
}
stockRepository.preoccupyStock(orderLine.getSkuCode(), orderLine.getQuantity());
});
//占用资金
CapitalHead capitalHead = capitalRepository.queryCustomerCapital(orderHead.getCustomerCode());
if (capitalHead.getAvailableAmount() < orderHead.getTotalAmount()){
throw new RuntimeException("可用资金不足 sku" + capitalHead.getAvailableAmount());
}
capitalRepository.preoccupyCapital(orderHead.getCustomerCode(), orderHead.getTotalAmount());
//保存订单信息
orderHeadRepository.saveOrderHead(orderHead);
orderLineRepository.saveOrderLine(orderHead.getOrderLineList());
//添加积分
BigDecimal addIntegralCondition = new BigDecimal(2000);
if (orderHead.getTotalAmount().compareTo(addIntegralCondition) > 0){
IntegralHead integralHead = new IntegralHead();
integralHead.setIntegral(orderHead.getTotalAmount());
integralRepository.addIntegral(integralHead);
}
}
}
3.2、问题和改进建议
上述这种代码风格我们称之为面条式代码风格,流程性的方法全部堆在一块,如果注释写的好一些的勉强能看出相关的流程节点代码关系,如果注释写的不好的,别人看类似的代码必须全部代码看完并且所有变量操作点都记下来,然后才明白哪些跟哪些是一个逻辑块,可读性非常差,甚至有些代码变量交叉公用的,对后期维护和改动也风险较大,每次改动都会改到整个大流程,涉及逻辑都有覆盖测试一遍,测试工作量较大,应该按流程节点进行代码拆分,通过子方法或者设计模式进行解耦隔离,改进后伪代码示例:
public class OrderHeadService {
@Autowired
private OrderHeadRepository orderHeadRepository;
@Autowired
private OrderLineRepository orderLineRepository;
@Autowired
private IntegralRepository integralRepository;
@Autowired
private StockRepository stockRepository;
@Autowired
private CapitalRepository capitalRepository;
/**
* 创建订单
*
* @param orderHead
*/
public void createOrder(OrderHead orderHead) {
//检查订单信息
this.checkOrder(orderHead);
//校验、占用库存、乐观锁控制并发
this.preoccupyStock(orderHead);
//占用资金
this.preoccupyCapital(orderHead);
//保存订单信息
this.saveOrder(orderHead);
//添加积分
this.addIntegral(orderHead);
}
}
4、优化if...else一些小技巧
4.1、案例伪代码示例
public void handleXXX(String arg) {
if ("a".equals(arg)) {
//一堆业务逻辑
System.out.println("this is a");
} else if ("b".equals(arg)) {
//一堆业务逻辑
System.out.println("this is b");
} else if ("c".equals(arg)) {
//一堆业务逻辑
System.out.println("this is c");
} else if ("c".equals(arg)) {
//一堆业务逻辑
System.out.println("this is d");
}
}
4.2、问题和改进建议
当每个if或者else块存在大量业务逻辑代码时,如果采用ifelse,可读性差、单个方法代码行数普通较大,扩展性不好,每次改动都要改到ifelse的代码块,容易改出其它bug,同时不符合开闭原则,这种情况应该优先考虑通过策略模式或者提前return的方式减少else部分,下面是通过策略模式改进后的伪代码示例:
public interface Strategy {
void handleXXX();
boolean isSupport(String arg);
}
public class StrategyA implements Strategy {
@Override
public void handleXXX() {
//一堆业务逻辑
System.out.println("this is a");
}
@Override
public boolean isSupport(String arg) {
return "a".equals(arg);
}
}
public class StrategyB implements Strategy {
@Override
public void handleXXX() {
//一堆业务逻辑
System.out.println("this is b");
}
@Override
public boolean isSupport(String arg) {
return "b".equals(arg);
}
}
//改进后
public void handleXXX(String arg) {
//可以改为SpringIOC方式
List<Strategy> strategieList = new ArrayList<>();
strategieList.add(new StrategyA());
strategieList.add(new StrategyB());
for (Strategy strategy : strategieList) {
if (strategy.isSupport(arg)) {
strategy.handleXXX();
return;
}
}
throw new RuntimeException("找不到对应的策略");
}
5、MQ消息推送不可回滚
5.1、案例伪代码示例
// 案例1
@Transactional(rollbackFor = Exception.class)
public void handlexxx1(Order order) {
//发送订单状态
MqClient.send(order);
//修改订单发货数量
OrderService.update(order);
//推送SAP
OrderService.sendSap(order);
}
// 案例2
@Transactional(rollbackFor = Exception.class)
public void handlexxx2(List<Order> orderList) {
for (Order order : orderList) {
//修改订单发货数量
OrderService.update(order);
//推送SAP
OrderService.sendSap(order);
//发送订单状态
MqClient.send(order);
}
}
5.2、问题和改进建议
案例1和案例2都存在相同的问题,就是当执行过程发生任何异常时,本地事务是可以正常回滚的,但是MQ发送是不会回滚的,这就导致分布式环境下数据不一致,改进后代码如下:
// 案例1改进后代码示例:
public void handlexxx11(Order order) {
//先落盘,成功提交事务,再推送mq
this.handlexxx111(order);
//发送订单状态 mq尽可能放到最后
MqClient.send(order);
}
@Transactional(rollbackFor = Exception.class)
public void handlexxx111(Order order){
//修改订单发货数量
OrderService.update(order);
//推送SAP
OrderService.sendSap(order);
}
// 案例2改进后的伪代码示例:
//改进后
public void handlexxx22(List<Order> orderList) {
//先落盘事务代码
this.handlexxx222(orderList);
//再mq类推送
for (Order order : orderList) {
//发送订单状态
MqClient.send(order);
}
}
@Transactional(rollbackFor = Exception.class)
public void handlexxx222(List<Order> orderList){
for (Order order : orderList) {
//修改订单发货数量
OrderService.update(order);
//推送SAP
OrderService.sendSap(order);
}
}
6、简单聊聊OOP和DDD
OOP是(Object Orientend Programming)面向对象编程的英文首字母缩写,我们在学习Java最开始入门教程就已经开始接触,但是随着我们进入企业工作后,在企业级开发中,用的最普遍的就是三层架构,在三层架构中,通常把对象Bean定义为对标数据库表的一个Model数据承载体,Model只有普通的对应表字段的java属性和基于JavaBean规范的getter\setter方法,把本来应该数据对象的业务行为抽象方法全部提取到一个叫Service的类中,这种方式在企业级开发数十年来基本没变过,这就导致5年甚至10年的资深程序员在遵循MVC模式下,基本已经忘记的OOP这个强大的面向对象抽象编程的设计,当然三层架构(控制层Controller、业务逻辑层Service、数据访问层DAO)确实可以确保项目调用链、数据传输链、代码边界上大家在共识上是一致的。
最近DDD开始盛行,DDD是(Domain Driven Design)领域驱动设计的英文首字母缩写,DDD提倡的设计思想和历来三层架构模式有很多是完全相反的,例如DDD强调代码设计上应该能够真实反映世界业务形态,例如订单对象的状态变更,在三层架构中我们通常会通过一个类似叫OrderServiceImpl的服务类去实现,伪代码如下:
public class OrderServiceImpl implements OrderService {
public void updateOrderStatus(String orderNo, String statusCode){
// 查询订单
Order order = OrderDao.findOrder(orderNo);
if (order == null){
throw new RuntimeException("订单不存在 + orderNo");
}
// 省略.....其它业务逻辑,例如是否可以更新
// 修改状态
order.setStatusCode(statusCode);
// 更新订单
orderDao.update(order);
}
}
但是如果按照DDD的思想我们订单状态变更属于订单对象本身的行为方法,我们应该将其定义在订单对象上,而不是Service中,伪代码如下:
public class Order{
public void updateStatus(String statusCode){
// 省略.....其它业务逻辑,例如是否可以更新
this.statusCode = statusCode;
}
}
事实上DDD核心思想和设计原则就是回归到最原始的OOP的面向对象的设计。面向对象对开发要求较高,事实上很多开发都不能很好地将真实世界业务逻辑抽象到程序设计中去,反观三层架构抛弃面向对象地抽象过程,简单将程序逻辑按Ctroller、Service、Dao来分层,这种方式对设计成本较低,容易实现,所以这也是为什么三层架构方式会盛行10几年之久的原因。
7、Spring事务失效六大场景
参看本人龚总号的另一篇文章
Spring @Transactional注解事务六大失效场景
---------- 正文结束 ----------
长按扫码关注微信公众号
Java软件编程之家