如何优雅消除重复代码

如何优雅消除重复代码

在消除重复代码之前,我们首先要考虑重复代码的特性是什么。有的同学会说,重复代码不就是一模一样的代码散落在不同的地方吗。当然这么说也对,只不过不够全面,重复代码不只是不同文件中,相同的代码,还有业务流程相似,但是并不完全相同,这类代码统称重复代码。

  1. 代码结构完全相同
    比如读取配置文件的这段代码,工程中好几个地方都在用,代码都是相同的。那么我们可以把不同地方读取配置文件的这段代码,放到一个工具类中。这样今后使用读取配置文件的时候,直接调用工具类读取配置文件方法即可,这也是工作中最常见的使用方式。

  2. 代码结构逻辑相似
    在项目中,我们经常遇到代码并不是完全相同,但是代码逻辑却是惊人的相似。比如电商营销活动中,通常邀请以邀请用户红包领取活动,但是对于新老用户红包赠予规则是不同的。同时也会根据邀请用户的数量的不同给予不同的红包优惠。但是无论新老用户都会经历根据用户类型获取红包计算规则,根据规则计算减免的红包,最后付款的时候减去红包数额这样一个业务逻辑。虽然表面看上去代码并不相同,但是实际上逻辑基本是一样的,因此也属于重复代码。

下面分享常用优化技巧

统一参数校验

当我们进行项目开发的时候,会编写类的实现方法,不可避免的会进行参数校验和业务规则校验,因此会在实现方法中写一下判断参数是否有效,返回结果是否有效的代码。

public OrderDTO queryOrderById(String id) {j
    if(StringUtils.isEmpty(id)) {
        return null;
    }
    
    OrderDTO order = orderBizService.queryOrder(id);
    if(Objects.isNull(Order)) {
        return null;
    }
    ...
}

public List queryUsersByType(List type) {
    if(StringUtils.isEmpty(id)) {
        return null;
    }
    
    ...
}

这种参数校验方式,很多人会用@valid注解进行参数有效性判断,但是还不够方便,它只能对一些参数进行校验,不能对业务逻辑结果进行有效校验。那么如何消除这些if...else....呢?

因此我会统一定义一个Assert进行参数或者业务响应结果进行校验。当然也可以使用Spring框架提供的Assert抽象类进行判断,但是它抛出的异常是IllegalArgumentException,习惯抛出自定义全局统一异常信息,这样可以通过全局异常类进行处理,因此我们首先定义一个业务断言类,主要针对biz层出现的参数以及业务结果进行断言,这样可以避免重复写if...else...判断代码。

public class Assert {

    public static void notEmpty(String param) {
        if(StringUtils.isEmpty(param)) {
            throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "param is empty or null");
        }
    }

    public static void notNull(Object o) {
        if (Objects.isNull(o)) {
            throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "object is null");
        }
    }

    public static void notEmpty(Collection collection) {
        if(CollectionUtils.isEmpty(collection)) {
            throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "collection is empty or null");
        }
        
    }
    
}

优化后的代码,看着是不是很爽

public OrderDTO queryOrderById(String id) {
    Assert.notEmpty(id);
    OrderDTO order = orderBizService.queryOrder(id);
    Assert.notNull(order);
    ...
}

public List queryUsersByType(List type) {
    Assert.notEmpty(type);
    
    
    ...
}

统一异常处理

以下Controller代码在项目中是否很常见?很多工程中Controller层中都充斥着大量的,try{}catch{}逻辑处理,相当于每个接口实现都要进行异常处理,看起来非常冗余麻烦。实际上我们可以定义统一异常处理,避免重复异常捕获。

@GetMapping("list")
public ResponseResult getOrderList(@RequestParam("id")String userId) {
    try {
        OrderVO orderVo = orderBizService.queryOrder(userId);    
        return ResponseResultBuilder.buildSuccessResponese(orderDTO);
    } catch (BizException be) {
        // 捕捉业务异常
        return ResponseResultBuilder.buildErrorResponse(be.getCode, be.getMessage());
    } catch (Exception e) {
        // 捕捉兜底异常
        return ResponseResultBuilder.buildErrorResponse(e.getMessage());
    }
}

那么我们应该这样优化处理这些重复异常捕获代码呢?首先我们要定义一个统一异常处理器,通过他来对Controller异常进行统一异常处理,包括捕获异常和异常信息等。这样就不用在每个接口实现进行try{}catch{}处理了。示例代码只是简单的说明实现方法,在项目落地的时候,大家可以定义处理更多的异常信息

@ControllerAdvice
@ResponseBody
public class UnifiedException {

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(BizException.class)
    @ResponseBody
    public ResponseResult handlerBizException(BizException bizexception) {
            return ResponseResultBuilder.buildErrorResponseResult(bizexception.getCode(), bizexception.getMessage());
    }


    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult handlerException(Exception ex) {
        return ResponseResultBuilder.buildErrorResponseResult(ex.getMessage());
    }   
}

优化后的Controller如下所示,大量的try...catch...不见了,代码结构变得更加清晰直接。

@GetMapping("list")
public ResponseResult getOrderList(@RequestParam("id")String userId) {
        List orderVo = orderBizService.queryOrder(userId);    
        return ResponseResultBuilder.buildSuccessResponese(orderVo);
}

优雅的属性拷贝

在我们实际项目开发中我们开发的微服务项目都是分层的,有的按照DDD领域模型分为四层。

无论分为三层或者四层都要涉及不同层级间的调用,每个层级都有直接的数据模型,比如biz层是dto,domain层是model,repo层是po.因此必然会涉及到数据模型对象之间的相关转换。在一些场景下模型之间的字段很多都是一样的,有的甚至是完全一模一样。

比如将DTO转化为业务模型Model,实际上他们之间很多的字段都是一样的,所以经常会出现以下的这种代码,会出现大量的属性赋值 的操作来达到模型转换的需求。实际上我们可以通过一些工具包或者工具类进行属性的拷贝,避免出现大量的重复赋值代码。

public class TaskConverter {

    public static TaskDTO taskModel2DTO(TaskModel taskModel) {
        TaskDTO taskDTO = new TaskDTO();
        taskDTO.setId(taskModel.getId());
        taskDTO.setName(taskModel.getName());
        taskDTO.setType(taskModel.getType());
        taskDTO.setContent(taskModel.getContent());
        taskDTO.setStartTime(taskModel.getStartTime());
        taskDTO.setEndTime(taskModel.getEndTime());
        return taskDTO;

    }
}

使用BeanUtils的进行属性赋值,很明显不再有那又长又没有感情的一条又一条的属性赋值语句了,整个任务数据模型对象的转换代码看上去立马舒服很多。

    public class TaskConverter {
        
        public static TaskDTO taskModel2DTO(TaskModel taskModel) {
            TaskDTO taskDTO = new TaskDTO();
            BeanUtils.copyProperties(taskModel, taskDTO);
            return taskDTO;
        }

    }

当然很多人会说,BeanUtils会存在深拷贝的问题。但是在一些浅拷贝的场景下使用起来还是比较方便的。另外还有Mapstruct工具,大家也可以试用一下。

核心能力抽象

假设我们有这样的业务场景,系统中根据不同的用户类型计算商品结算金额,大致计算逻辑分为三个步骤,分别是计算商品总价,计算不同用户对应的优惠金额,最后计算出结算金额。我们先看原有的计算逻辑

普通用户结算逻辑:

    public Class NormalUserSettlement {
        
         //省略代码
        ...

        public Bigdecimal calculate(String userId) {
            //计算商品总价格
            List goods = shoppingService.queryGoodsById(userId);
            
            Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
              
            //计算优惠
            Bigdecimal discount = total.multiply(new Bigdecimal(0.1));          
                
            //计算应付金额
            Bigdecimal payPrice = total - dicount;
            return payPrice;
        }
         //省略代码
        ...
    }

VIP用户结算逻辑:


    public Class VIPUserSettlement {
        
        //省略代码
        ...

        
       public Bigdecimal calculate(String userId) {
            //计算商品总价格
            List goods = shoppingService.queryGoodsById(userId);
            
            Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
              
            //计算优惠
            Bigdecimal discount = total.multiply(new Bigdecimal(0.2));          
                
            //计算应付金额
            Bigdecimal payPrice = total - dicount;   
            return payPrice;
        }
         //省略代码
        ...
    }

黑卡用户结算逻辑:

    public Class VIPUserSettlement {
        
        //省略代码
        ...

        
       public Bigdecimal calculate(String userId) {
            //计算商品总价格
            List goods = shoppingService.queryGoodsById(userId);
            
            Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
              
            //计算优惠
            Bigdecimal discount = total.multiply(new Bigdecimal(0.2));          
                
            //计算应付金额
            Bigdecimal payPrice = total - dicount;       
            return payPrice;   
        }    
         //省略代码
        ...
    }

在这样的场景中,我们发现计算商品总价格和计算结算金额逻辑是一样的,唯一不同的是每个用户类型对应的优惠金额是不同的。因此我们可以把逻辑相同的共性部分提取到父类AbstractSettleMent中,然后把可变的部分有子类进行扩展实现。这样各个子类之关系自己的优惠逻辑即可,重复的代码都被抽象复用,大大减少重复代码。

    public abstract class AbstractSettlement {
        
       //省略代码
        ...
            
        public abstact   Bigdecimal calculateDiscount();

        
       public Bigdecimal calculate(String userId) {
            //计算商品总价格
            List goods = shoppingService.queryGoodsById(userId);
            
            Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
              
            //计算优惠
            Bigdecimal discount = calculateDiscount();          
                
            //计算应付金额
            Bigdecimal payPrice = total - dicount;     
            return payPrice; 
        
        }
        
         //省略代码
        ...
    }

[图片上传失败...(image-4e3295-1666598499307)]

自定义注解和AOP

能够将那些与业务无关,缺为共同模块共同调用的逻辑或者责任(例如事务记录,权限管理,日志管理,接口鉴权)封装起来,便于减少系统的重复代码,降低代码的耦合度,并有利于未来的扩展和维护。针对这种场景 我们可以使用AOP同时结合自定义注解实现接口的切面编程,在需要进行通用逻辑处理的接口或者类中增加对应的注解即可。

假设有这样的业务场景,需要计算指定某些接口的耗时情况,一般的做法是在每个接口中都加上计算接口耗时的逻辑,这样各个接口中就会有这样重复计算耗时的逻辑,重复代码就这样产生了。那么通过自定义注解和AOP的方式可以轻松地解决代码重复的问题。首先定义一个注解,用于需要统计接口耗时的接口方法上。

    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TimeCost {

    }

定义切面实现类

  @Aspect 
  @Component
  public class CostTimeAspect {

      @Pointcut(value = "@annotation(com.mufeng.eshop.anotation.CostTime)") 
      public void costTime(){ }
      
      @Around("runTime()") 
      public Object costTimeAround(ProceedingJoinPoint joinPoint) {
          Object obj = null;
    try {
     long beginTime = System.currentTimeMillis();
     obj = joinPoint.proceed();
     //获取方法名称
     String method = joinPoint.getSignature().getName();
     //获取类名称
     String class = joinPoint.getSignature().getDeclaringTypeName();
                          //计算耗时
                          long cost = System.currentTimeMillis() - beginTime;
     log.info("类:[{}],方法:[{}] 接口耗时:[{}]", class, method, cost + "毫秒");
    } catch (Throwable throwable) {
     throwable.printStackTrace();
    }
    return obj;
      }
  }

优化前的代码

@GetMapping("/list")
public ResponseResult> getOrderList(@RequestParam("id")String userId) {
        long beginTime = System.currentTimeMillis();
        List orderVo = orderBizService.queryOrder(userId);  
        log.info("getOrderList耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
        return ResponseResultBuilder.buildSuccessResponese(orderVo);
}


@GetMapping("/item")
public ResponseResult getOrderById(@RequestParam("id")String orderId) {
        long beginTime = System.currentTimeMillis();
        OrderVO orderVo = orderBizService.queryOrderById(orderId);
        log.info("getOrderById耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
        return ResponseResultBuilder.buildSuccessResponese(orderVo);
}

优化后的代码

    @GetMapping("/list")
    @TimeCost
    public ResponseResult> getOrderList(@RequestParam("id")String userId) {
            List orderVo = orderBizService.queryOrder(userId);    
            return ResponseResultBuilder.buildSuccessResponese(orderVo);
    }


    @GetMapping("/item")
    @TimeCost
    public ResponseResult getOrderList(@RequestParam("id")String orderId) {
            OrderVO orderVo = orderBizService.queryOrderById(orderId);    
            return ResponseResultBuilder.buildSuccessResponese(orderVo);
    }

引入规则引擎

大家在做业务开发的时候,可能会遇到这样的场景,业务中充斥着各种各样的规则判断,同时这些业务规则还可能经常发生变化。即便是我们用了策略模式等设计模式来优化代码结构,但是还是不能避免代码中出现大量的if...else...判断代码,一旦增加或者修改规则都需要在原来的业务规则代码中进行修改,维护起来非常不方便。

[图片上传失败...(image-b58048-1666598499307)]

假设设有这样的业务,销售人员的奖励根据实际的利润进行计算,不同的利润计算奖励的规则并不相同。使用规则引擎之前,可能会有这样的代码结构,需要根据实际利润所处的区间来计算最终的奖励金额,不同区间范围对应的返点规则是不一样的,因此会有很多的if...else...判断。另外规则有可能随着业务的发展还会经常变化,因此后期可能面临不断修改这部分的计算奖励的代码的情况。

    public double calculate(int profit) {
            if(profit < 1000) {
                return profit * 0.1;
            } else if(1000 < profit && profit< 2000) {
                return profit * 0.15;
            } else if(2000 < profit && profit < 3000) {
                return profit * 0.2;
            } 
            return  profit * 0.3;
        }

如果遇到这种业务场景,我们就可以考虑使用规则引擎。通过引入规则引擎,我们可以实现业务代码与业务规则相分离,将各种业务判断规则从原有的平台代码中抽离出来,以后规则的修改都在规则文件中直接修改就可以了,避免代码本身的变更,从而大大提升代码的扩展性。这里简单介绍下常用的规则引擎Drools是如何实现规则扩展管理的。

使用Drools之后:

使用规则引擎优化之后,所有的规则也就是所有的if...else...都会放在规则文件reward.drl中,因此代码中不会再有各种重复的if...else...代码,真正实现了业务规则与业务数据相分离。

   // 奖励规则
   package reward.rule
   import com.mufeng.eshop.biz.Reward
    
   // rule1:如果利润小于1000,则奖励计算规则为profit*0.1
   rule "reward_rule_1"
       when
           $reward: Reward(profit < 1000) 
       then
           $reward.setReward($reward.getProfit() * 0.1);
           System.out.println("匹配规则1,奖励为利润的1成");
   end
    
   // rule2:如果利润大于1000小于2000,则奖励计算规则为profit*0.15
   rule "reward_rule_2"
       when
           $reward: Reward(profit >= 1000 && profit < 2000)
       then
           $reward.setReward($reward.getProfit() * 0.15);
           System.out.println("匹配规则2,奖励为利润的1.5成");
   end
    
   // rule3:如果利润大于2000小于3000,则奖励计算规则为profit*0.2
   rule "reward_rule_3"
       when
           $order: Order(profit >= 2000 && profit < 3000)
       then
           $reward.setReward($reward.getProfit() * 0.2);
           System.out.println("匹配规则3,奖励为利润的2成");
   end
    
   //  rule4:如果利润大于等于3000,则奖励计算规则为profit*0.3
   rule "reward_rule_4"
       when
            $order: Order(profit >= 3000)
       then
           $reward.setReward($reward.getProfit() * 0.3);
           System.out.println("匹配规则4,奖励为利润的3成");
   end

在代码中只要将待判断的数据插入到规则引擎的工作内存中,然后执行规则就可以获取到最终的结果,是不是很方便的实现业务规则的解耦,在实际的Java代码中也不用看到各种if...else...判断。

定义规则引擎实现


    public class DroolsEngine {

        private KieHelper kieHelper;

        public DroolsEngine() {
            this.kieHelper = new KieHelper();
        }

        public void  executeRule(String rule, Object unit, boolean clear) {
            kieHelper.addContent(rule, ResourceType.DRL);
            KieSession kieSession = kieHelper.getKieContainer().newKieSession();
            //插入判断实体
            kieSession.insert(unit);
            //执行规则
            kieSession.fireAllRules();
            if (clear) {
                kieSession.dispose();
            }
        }
    }


  public class Profit {


      public double  calculateReward(Reward reward) {
         String rule = "classpath:rules/reward.drl";
         File rewardFile = new File(rule);
         String rewardDrl = FileUtils.readFile(rewardFile, "utf-8");
         DroolsEngine engine = new DroolsEngine();
         engine.executeRule(rewardDrl, reward, true);
         return  reward.getReward();    
         
      }
  }

通过引入Drools规则引擎,代码中不再有各种规则判断的重复的if...else...判断语句,而且如果后期要修改奖励规则,代码不用修改,直接更改规则即可,系统的扩展性以及可维护性进一步提升。

消除重复代码方法论

无论是提取公共逻辑作为工具类、使用AOP进行切面编程还是进行业务逻辑抽象,又或是借助规则引擎实现规则与业务分离。实际上他们本质是一样的,都是通过抽离或者抽象相似代码逻辑后进行统一处理将这些核心思想放入微服务内部就是就是在系统中消除重复业务逻辑。放在架构层面中来看就是与中台思想本质是相同的,将用户、支付、订单都会用到的服务抽象成中台。实际上就是一种混乱到有序的一直软件治理,万物归一的思想。

image.png

那么在日常的实际项目中我们应该怎么落地实践消除重复代码呢?这里总结了通过上述文章对于重复代码的处理,我们来试图来提炼消除重复代码的方法论。


image.png

Find:技术同学需要有一双可以发现重复代码的眼睛,能够将表面上的重复我代码以及隐藏的重复代码识别出来。重复代码不仅仅是表示长得一模一样的代码,那些核心业务逻辑一样实际也是一种重复代码。

Analysis:当我们找到了重复代码之后,就要考虑该如何进行优化了,如果只是工具类型的重复代码,那么直接提取作为一个工具类就可以了,也不用考虑太多。但是如果是涉及业务流程可能需要进一步的进行抽象。

Action:根据不同的重复代码的类型,我们需要制定不通过的优化重复代码的方案。根据不同的方案实现通过引入规则引擎还是模板方法进行抽象。

你可能感兴趣的:(如何优雅消除重复代码)