如何提高业务代码的可读性

背景

​ 最近看了一些系统的历史代码,直观感受是很乱,不知道原有逻辑再做什么,也不知道新逻辑写在什么地方合适,来来回回还是自己加一个先对独立的逻辑实现需求,这样就更加混乱了。单看某个模块其实代码结构上是有很多用心设计的样子的,所以一直想弄清楚系统为什么变复杂了。目前有了一点阶段性的结论,先整理一下。

​ 这里我不再考虑一些概念性的东西,像封装、继承、多态这些,或者是一些基本原则。这些所有人都能很好的说出来并且应用,我从最表面的代码结构来讨论一下。首先系统的代码维护,这是一个工程,我们的目标很明确就是持续迭代。迭代一个系统最基本的是“看得懂”,然后才是“怎么写”。而编写好的代码的判断指标之一就是“可读性”,也就是后人如何“看得懂”。所以本文主要就来讨论业务系统中的代码可读性。最后顺便考虑一下最近说的单元测试。

业务系统的可读性

​ 想要让大家更好的读懂业务代码,我们首先要弄清楚大家的关注点是什么。

​ 我在举个例子,以我现在正在开发的电梯估价接口来说。如果后续有人要在电梯上开发一个新功能,评估改动的时候肯定会有如下的流程:

​ 其实可以发现阅读业务代码的时候是有一个关注度的。不同的层级关注的东西不一样,那么顺应这个阅读流程的代码可读性就会很强。

代码举例

​ 我们来看下这个application层的代码,这是一个Service中的代码。

public ValuatePriceElevatorEntity productCenterElevatorValuatePrice(ValuateDemandEntity valuateDemandEntity) {
				//异步流程最大时间 product.center.elevator.valuate.price.async.timeout
        //估价开始前先记录时间,这样如果整个接口耗时过大,就不等异步流程返回了。有利于控制整体耗时。所以这是一个兜底的超时控制
        long timeOut = System.currentTimeMillis()+ApolloConfigUtil.getLongProperty(ApolloConstants.PRODUCT_CENTER_ELEVATOR_VALUATE_PRICE_ASYNC_TIMEOUT,4000L);

        List<String> goodsSkuList = operatePlatformService.getElevatorValuatePriceGoodsSkuConfig();
        valuateDemandEntity.setGoodsSkuList(goodsSkuList);

        //查询用户首单信息
        CompletableFuture<PassengerFirstOrderInfoVO> passengerFirstOrderInfoVOCompletableFuture = CompletableFutureUtils.supplyAsyncSimple(() -> {
            return userInfoService.queryPassengerFirstOrderInfo(valuateDemandEntity.getUserNewId());
        }, ThreadPools.getProductCenterElevatorValuatePricePoolInstance());
        //用户选择偏好
        CompletableFuture<PassengerUserPreferencesInfoVO> passengerUserPreferencesInfoVOFuture = CompletableFutureUtils.supplyAsyncSimple(() -> {
            return userInfoService.queryPassengerUserPreferencesInfo(valuateDemandEntity.getUserNewId());
        }, ThreadPools.getProductCenterElevatorValuatePricePoolInstance());

        valuateDemandEntity.fillDefaultValueIfNull();
        //估价模块
        List<SkuPriceInfo> skuPriceInfoList = asyncValuate(valuateDemandEntity);
        ValuatePriceElevatorEntity valuatePriceElevatorEntity = ValuatePriceElevatorFactory.createElevatorEntity(valuateDemandEntity,skuPriceInfoList);

        //获取地图使用什么业务线的配置
        ElevatorValuatePricePathPlanBizConfig elevatorValuatePricePathPlanBizConfig = operatePlatformService.getElevatorValuatePricePathPlanBizConfig(
                valuateDemandEntity.getStartPosition().getCityCode(),
                valuateDemandEntity.getUserNewId());
        valuatePriceElevatorEntity.fillPathPlanByBizCode(elevatorValuatePricePathPlanBizConfig.getPathPlanBizCodeEnum());
        valuatePriceElevatorEntity.fillDemandInfo();

        //异步查询举手率
        List<String> skuCodeList = valuatePriceElevatorEntity.parseAllSkuCodeList();
        CompletableFuture<List<TaxiBottomSkuHandUpRateInfo>> taxiBottomSkuHandUprRateCompletableFuture = CompletableFutureUtils.supplyAsyncSimple(() -> {
            return userInfoService.getTaxiBottomSkuHandUprRate(
                    valuateDemandEntity.getStartPosition().getCityCode(),
                    Long.valueOf(valuateDemandEntity.getDistance()),
                    skuCodeList);
        }, ThreadPools.getProductCenterElevatorValuatePricePoolInstance());
        //价敏系数
        CompletableFuture<String> elasticityScoreCompletableFuture = CompletableFutureUtils.supplyAsyncSimple(() -> {
            return extractElasticityScore(valuateDemandEntity, new ArrayList<>(skuPriceInfoList));
        }, ThreadPools.getProductCenterElevatorValuatePricePoolInstance());

        //品类过滤配置
        ElevatorValuatePriceSkuFilterConfig elevatorValuatePriceSkuFilterConfig = operatePlatformService.getElevatorValuatePriceSkuFilterConfig(
                valuateDemandEntity.getStartPosition().getCityCode(),
                valuateDemandEntity.getDistance(),
                valuateDemandEntity.getStartPlanStartTime(),
                valuateDemandEntity.getInstantType(),
                valuateDemandEntity.isCrossCity(),
                valuateDemandEntity.getUserNewId());
        if (elevatorValuatePriceSkuFilterConfig!=null) {
            valuatePriceElevatorEntity.filterSkuBySkuList(elevatorValuatePriceSkuFilterConfig.getFilterSkuList());
        }

        //品类卡片配置
        Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap = operatePlatformService.getElevatorValuatePriceSkuCardConfigMap(
                valuateDemandEntity.getUserNewId(),
                valuateDemandEntity.getStartPosition().getCityCode(),
                valuatePriceElevatorEntity.parseAllSkuCodeList());

        //运力不可选状态,归类到不可选模块
        ProductCenterValuatePriceTimeConfig productCenterValuatePriceTimeConfig = operatePlatformService.getProductCenterValuatePriceTimeConfig(
                valuateDemandEntity.getUserNewId(),
                valuateDemandEntity.getStartPosition().getCityCode(),
                valuateDemandEntity.getEndPosition().getCityCode());
        valuatePriceElevatorEntity.groupSkuForUnavailable(productCenterValuatePriceTimeConfig,elevatorValuatePriceSkuCardConfigMap);

        //附近无车聚类
        ElevatorValuatePriceSkuNoCarConfig elevatorValuatePriceSkuNoCarConfig = operatePlatformService.getElevatorValuatePriceSkuNoCarConfig(
                valuateDemandEntity.getStartPosition().getCityCode(),
                valuateDemandEntity.getDistance(),
                valuateDemandEntity.getStartPlanStartTime(),
                valuateDemandEntity.getUserNewId());
        if (elevatorValuatePriceSkuNoCarConfig!=null) {
            List<TaxiBottomSkuHandUpRateInfo> taxiBottomSkuHandUprRate = CompletableFutureUtils.getTimeOutForTime(taxiBottomSkuHandUprRateCompletableFuture, new ArrayList<>(), timeOut);
            valuatePriceElevatorEntity.groupSkuForNoCar(elevatorValuatePriceSkuNoCarConfig,taxiBottomSkuHandUprRate,elevatorValuatePriceSkuCardConfigMap);
        }
        //品类正常聚类成电梯,包含盒子逻辑
        PassengerFirstOrderInfoVO passengerFirstOrderInfoVO = CompletableFutureUtils.getTimeOutForTime(passengerFirstOrderInfoVOCompletableFuture, PassengerFirstOrderInfoVO.defaultVO, timeOut);
        Boolean phFirstOrder = passengerFirstOrderInfoVO.getPhFirstOrder();
        List<ElevatorValuatePriceSkuGroupConfig> elevatorValuatePriceSkuGroupConfigList = operatePlatformService.getElevatorValuatePriceSkuGroupConfig(
                valuateDemandEntity.getStartPosition().getCityCode(),
                valuateDemandEntity.getDistance(),
                valuateDemandEntity.getStartPlanStartTime(),
                valuateDemandEntity.getInstantType(),
                phFirstOrder,
                valuateDemandEntity.isCrossCity(),
                valuateDemandEntity.getAdSource(),
                valuateDemandEntity.getUserNewId());
        if (CollectionUtils.isNotEmpty(elevatorValuatePriceSkuGroupConfigList)) {
            valuatePriceElevatorEntity.groupAllSkuAndSkuBox(elevatorValuatePriceSkuGroupConfigList,elevatorValuatePriceSkuCardConfigMap);
        }
        //剩余的品类聚类成其他模块
        valuatePriceElevatorEntity.groupOtherSku(elevatorValuatePriceSkuCardConfigMap);

        //将电梯进行排序
        valuatePriceElevatorEntity.sort();
}

这个逻辑其实挺长的,有小一百行了。但是仔细看一遍后是可以快速理解的。原因如下

代码反例

这里我再修改一下代码,大家看下对可读性的影响

public ValuatePriceElevatorEntity productCenterElevatorValuatePrice(ValuateDemandEntity valuateDemandEntity) {
  ...............
        //附近无车聚类
        ElevatorValuatePriceSkuNoCarConfig elevatorValuatePriceSkuNoCarConfig = operatePlatformService.getElevatorValuatePriceSkuNoCarConfig(valuateDemandEntity);
        if (elevatorValuatePriceSkuNoCarConfig!=null) {
            List<TaxiBottomSkuHandUpRateInfo> taxiBottomSkuHandUprRate = CompletableFutureUtils.getTimeOutForTime(taxiBottomSkuHandUprRateCompletableFuture, new ArrayList<>(), timeOut);
            valuatePriceElevatorEntity.groupSkuForNoCar(elevatorValuatePriceSkuNoCarConfig,taxiBottomSkuHandUprRate,elevatorValuatePriceSkuCardConfigMap);
        }
  			groupSku(ValuateDemandEntity valuateDemandEntity,Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap);
        //将电梯进行排序
        valuatePriceElevatorEntity.sort();
}


public ValuatePriceElevatorEntity groupSku(ValuatePriceElevatorEntity valuatePriceElevatorEntity,List<ElevatorValuatePriceSkuGroupConfig> elevatorValuatePriceSkuGroupConfigList,Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap) {
          groupAllSkuAndSkuBox(valuatePriceElevatorEntity,elevatorValuatePriceSkuGroupConfigList,elevatorValuatePriceSkuCardConfigMap);
        }
        //剩余的品类聚类成其他模块
        groupOtherSku(valuatePriceElevatorEntityelevatorValuatePriceSkuCardConfigMap);
}


public ValuatePriceElevatorEntity groupAllSkuAndSkuBox(ValuatePriceElevatorEntity valuatePriceElevatorEntity,List<ElevatorValuatePriceSkuGroupConfig> elevatorValuatePriceSkuGroupConfigList,Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap) {
        //品类正常聚类成电梯,包含盒子逻辑
        List<ElevatorValuatePriceSkuGroupConfig> elevatorValuatePriceSkuGroupConfigList = operatePlatformService.getElevatorValuatePriceSkuGroupConfig(valuateDemandEntity);
        if (CollectionUtils.isNotEmpty(elevatorValuatePriceSkuGroupConfigList)) {
  //聚类 逻辑直接在service中实现
}
public ValuatePriceElevatorEntity groupOtherSku(ValuateDemandEntity valuateDemandEntity,Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap) {
  //剩余的品类聚类成其他模块
}
//operatePlatformService
public List<ElevatorValuatePriceSkuGroupConfig> getElevatorValuatePriceSkuGroupConfig(ValuateDemandEntity valuateDemandEntity){
  //mapStruct转换
  			FeatureGetParam featureGetParam = valuateDemandEntityToParam(valuateDemandEntity)
        Boolean phFirstOrder = aiBrainQuadFeatureFacade.getFirstOrderFeature(featureGetParam);
        List<ElevatorValuatePriceSkuGroupConfig> configList = PHRuleSDK.getInstance(USER_GROWTH)
                .getObjectList(Consts.OperationPlatform.ELEVATOR_VALUATE_SKU_GROUP_CONFIG,
                        ElevatorValuatePriceSkuGroupConfig.class,
                        cityCode,
                        new ArrayList<>(),
                        userNewId,
                        null);
        if(CollectionUtils.isEmpty(configList)){
            return null;
        }
        List<ElevatorValuatePriceSkuGroupConfig> configs = new ArrayList<>();
        for (ElevatorValuatePriceSkuGroupConfig config:configList){
            if (config.usable(distance,planStartTime,instantType,adSource,crossCity,phFirstOrder)){
                configs.add(config);
            }
        }
        return configs;
}

初步看下来productCenterElevatorValuatePrice中的逻辑一样简洁,但是评估需求改动的时候通常不敢下定论

修改后的代码或许再编写的时候不会有什么问题,每个功能点分了模块,都依赖entity。但是在后续评估改动范围和阅读的时候存在了很多困扰,另外随着迭代调用深度会加大,数据的使用范围会难以控制,且外部调用进一步隐藏难以发现。最后只能询问谁熟悉这块逻辑。其实熟悉的不是业务,而是代码结构和数据的作用范围,业务通常用语言能解释。但是代码中的一些细节和其导致的业务默认行为是难以理解的。

业务代码的单元测试

​ 因为目前在推进单元测试,所以这里顺便从代码结构的角度说一下单元测试的落地。我们分别从上述的例子1和2来编写单元测试看一下效果。

例子1单元测试


    @Test
    public void filterSkuBySkuListTest() {
        ValuatePriceElevatorEntity valuatePriceElevatorEntity = mock数据

        ElevatorValuatePriceSkuFilterConfig elevatorValuatePriceSkuFilterConfig = mock数据

        Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap = mock数据
        
       //执行单测
          valuatePriceElevatorEntity.groupAllSkuAndSkuBox(elevatorValuatePriceSkuGroupConfigList,elevatorValuatePriceSkuCardConfigMap);
      
      //校验结果valuatePriceElevatorEntity
    }

例子2单元测试


    @Test
    public void filterSkuBySkuListTest() {
        ValuatePriceElevatorEntity valuatePriceElevatorEntity = mock数据
          
        Map<String,ElevatorValuatePriceSkuCardConfig> elevatorValuatePriceSkuCardConfigMap = mock数据
          
          //groupAllSkuAndSkuBox中有外部依赖,需要mock
        MockOperatePlatformService mockOperatePlatformService = new MockOperatePlatformService();
        service.setOperatePlatformService(mockOperatePlatformService);
      
       //执行单测
       service.groupAllSkuAndSkuBox(valuatePriceElevatorEntityelevatorValuatePriceSkuCardConfigMap);
     
      
      //校验结果valuatePriceElevatorEntity
    }

class MockOperatePlatformService extends OperatePlatformService {
  //operatePlatformService
  public List<ElevatorValuatePriceSkuGroupConfig> getElevatorValuatePriceSkuGroupConfig(ValuateDemandEntity valuateDemandEntity){
    //mock数据
  }
}

​ 这里可以看到例子2的单元测试的成本是更高的,如果有大量的外部依赖,或者调用链路太深,单元测试难以执行。另外OperatePlatformService如果出现改动,对单元测试影响就更大,例子1依赖的是config实体,他的改动可能性远比service方法定义的改动低的多。

​ 业务中变动的逻辑主要还是功能点,复杂逻辑和业务细节也是功能点,所以单元测试也应该集中在功能点的测试。那么相对独立且明确数据依赖的功能点对单元测试是有利的。

总结

​ 其实总结一下,业务代码可读性的核心在于编码时考虑阅读业务代码的思路,把关注点在代码的第一层级中直接表现出来。代码的可读性要多考虑系统的目的,如果业务系统偏向中台或者平台,那这种流程式的代码结构是否适用有待讨论。而对于中间件或者工具,其标准也是不同的,本文主要讨论业务系统的可读性。

​ 另外关于设计模式对业务代码可读性的影响也需要进一步讨论,有时候为了扩展性导致核心业务流程被隐藏,是否是值得的。

​ 个人认为业务代码需要足够简单,流程化。然后将数据与使用数据的业务功能归到一起,对于后续功能的扩展之前修改对应代码即可,只要业务代码足够简洁明了,并不需要过多的扩展性设计。核心在于更清晰的表达业务,和限定数据使用范围。