函数式编程正简化你的代码

2,387 阅读6分钟

函数式编程正简化你的代码

前言

白天在做可视化大屏的接口优化,一共六个子页面对应着六个数据请求接口,现只对其中业务相似的五个接口进行讨论。五个接口总共涉及到三个参数:(1)areaCode—地区编码 (2)areaLevel — 地区层级镇级:2 村级:3 网格级别:4 (3)typeList — 人员类型逗号拼接字符串。

Snipaste_2023-03-02_23-50-04.png

项目经理要求这个大屏的速度要快一些,按理说是应该先优化SQL的,但是这套数据库里面的表结构很乱,包括索引也是很乱的。为了保持和另一个系统查出的数据一致,这个大屏的SQL我都很多是从另一个系统中拷贝来的。既然是可视化大屏,那其实是可以考虑将查询缓慢的数据放入Redis缓存中的,在大领导来参观前,多点一点,之后向领导演示时候就不会卡顿了。

这边简单展示Controller层的以下五个接口:

/**
 * 所说的五个接口大致样式
 */
@RestController
@RequestMapping("/important-population")
@Validated
public class ImportantPopulationController extends BaseController {

    @Autowired
    private Service serice;
    
    /**
     * 人群统计
     */
    @GetMapping("/population-area-statistic")
    public R populationAreaStatistic(@RequestParam String areaCode,
                                     @RequestParam Integer areaLevel,
                                     String typeList) {
        List<PieChartWithCode> result = service.populationAreaStatistic(areaCode, areaLevel, typeList);
        return R.data(result);

    /**
     * 人口统计
     */
    @GetMapping("/age-gender-statistic")
    public R ageGenderStatistic(@RequestParam String areaCode,
                                @RequestParam Integer areaLevel,
                                String typeList) {
        BarChart result = service.ageGenderStatistic(areaCode, areaLevel, typeList);
        return R.data(result);
    }

    /**
     * 风险等级统计
     */
    @GetMapping("/risk-level-statistic")
    public R riskLevelStatistic(@RequestParam String areaCode,
                                @RequestParam Integer areaLevel,
                                String typeList) {
        List<PieChart> result = service.riskLevelStatistic(areaCode, areaLevel, typeList);
        return R.data(result);
    }

    /**
     * 地区排名
     */
    @GetMapping("/area-rank")
    public R areaRank(@RequestParam String areaCode,
                      @RequestParam  Integer areaLevel,
                      String typeList) {
        List<PieChartWithCode> result = service.areaRank(areaCode, areaLevel, typeList);
        return R.data(result);
    }

    /**
     * 重点人员占比
     */
    @GetMapping("/area-ratio")
    public R areaRatio(@RequestParam String areaCode,
                       @RequestParam  Integer areaLevel,
                       String typeList) {
        List<PieChartWithCode> result = service.areaRatio(areaCode, areaLevel, typeList);
        return R.data(result);
    }
}

问题

接口数据直接放Redis缓存还是较为简单的,系统中已经封装好了相应注解 @RedisCache

/**
 * 事件占比分析
 */
@GetMapping("/case-source-statistic")
@RedisCache(key = "important-population::case-source-statistic", fieldKey = "#year+'-'+#areaCode+'-'+#typeList", expired = 60 * 60 * 24)
public R caseSourceStatistic(@RequestParam String year,
                             @RequestParam String areaCode,
                             String typeList) {
}

但是就当前业务来说,数据都存Redis是没有必要的,只有在areaLevel = 2(即查询所有镇级别的数据是才会卡顿,其他村级和网格级的数据情况几乎是不卡顿的)。所以考虑单独处理Redis缓存的存取情况,最简单的方法就是Controller层下的每个方法都写一遍:判断是否查询镇级别数据,根据判断结果决定是否进行Redis存取。但是既然有这么多的共同点,本能的直觉告诉我,一定是能够简化这边的代码逻辑的。

思考

找共同点

五个接口内部都是简单的一个service方法的调用,且参数一致。最重要的相同点是,除了他们都有个service方法调用,我们要对他们做相同的Redis存取逻辑。

不同点

返回类型不同,有List类型也有自定义的类型的。

想法

是否我们能将五个接口重复的进行Redis存取逻辑处理的代码抽取出来,而不关心最后到底是调用哪个sevice方法去查询结果的。在函数式编程中,函数是能够作为参数传递的。那么假如将抽取的那部分重复代码命名为redisHandle,那只要我们对函数redisHandle传入不同的service调用(即函数方法),就能实现redisHandle中动态调用service方法。

实现

选择函数式接口

Java JDK8中自带很多函数式接口Function、BiFunction等,但是这边是三个参数,需要使用 @FunctionalInterface自定义函数式接口

    @FunctionalInterface
    interface Function3 <A, B, C, R> {
        R apply (A a, B b, C c);
    }

具体实现

初步实现redisHandle方法

其中redisUtils是对redisTemplate的再封装

redisUtils.get(String key) :直接从redis获取数据

redisUtils.get(String key, Class clazz):从redis获取数据,按指定类型用fastjson解析

由于返回类型不同,使用泛型T

/**
 * function:具体的service方法调用
 * areaCode:地区编码
 * areaLevel:地区层级 镇级:2
 * typeList:人员类型
 */
private <T> T redisHandle(Function3 function, String areaCode, Integer areaLevel, String typeList) {
    T result;
    // redis中存取所用的key
    String redisKey = areaCode + "::" + areaLevel + "::" + typeList;
    // 只对地区层级为镇级的数据进行Redis缓存
    if (areaLevel.equals(2)) {
        // redisUtils从Redis中读取已被缓存的数据
        result = (T) redisUtils.get(redisKey);
        if (result == null) {
            // 动态调用具体方法
            result = (T) function.apply(areaCode, areaLevel, typeList);
            redisUtils.set(redisKey);
        }
    } else {
        result = (T) function.apply(areaCode, areaLevel, typeList);
    }
    return result;
}
/**
 * 只列出一个接口是怎么调用的
 *
 * 地区人口统计
 */
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
                                 @RequestParam Integer areaLevel,
                                 String typeList) {

    List<PieChartWithCode> baseInfoList = redisHandle((a, b, c) ->
            populationImageService.populationAreaStatistic((String) a, (Integer) b, (List<String>) c),
            areaCode, areaLevel, typeList);

    return R.data(baseInfoList);
}

fastjson类型解析出错

以上的方法基本实现了对公共代码的抽取,但是从redis读取数据时候会出现fastjson类型解析异常,redisUtils.get()方法需要传入要解析的目标类型。以下提供了返回类型为List类型和自定义类型BarChart的两种情况的代码。

/**
 * function:具体的service方法调用
 * areaCode:地区编码
 * areaLevel:地区层级 镇级:2
 * typeList:人员类型
 * returnClass: 返回类型
 */
private <T> T redisHandle(Function3 function, String areaCode, Integer areaLevel, String typeList, Class returnClass) {
    T result;
    // redis中存取所用的key
    String redisKey = areaCode + "::" + areaLevel + "::" + typeList;
    // 只对地区层级为镇级的数据进行Redis缓存
    if (areaLevel.equals(2)) {
        // redisUtils从Redis中读取已被缓存的数据
        result = (T) redisUtils.get(redisKey, returnClass);
        if (result == null) {
            result = (T) function.apply(areaCode, areaLevel, typeList);
            redisUtils.set(redisKey);
        }
    } else {
        result = (T) function.apply(areaCode, areaLevel, typeList);
    }
    return result;
}
/**
 * 返回类型为List类型的调用
 *
 * 地区人口统计
 */
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
                                 @RequestParam Integer areaLevel,
                                 String typeList) {
    // 多添加了参数 "List.class"
    List<PieChartWithCode> resultList = redisHandle((a, b, c) ->
            populationImageService.populationAreaStatistic((String) a, (Integer) b, (List<String>) c),
            areaCode, areaLevel, typeList, List.class);

    return R.data(resultList);
}
/**
 * 返回类型为自定义类型BarChart的调用
 *
 * 性别年龄统计
 */
@GetMapping("/age-gender-statistic")
public R ageGenderStatistic(@RequestParam String areaCode,
                            @RequestParam Integer areaLevel,
                            String typeList) {
    BarChart barChart = getRedis((a, b, c) ->
            populationImageService.ageGenderStatistic((String) a, (Integer) b, (List<String>) c),
            areaCode, areaLevel, typeList, BarChart.class);

    return R.data(barChart);
}

接口的redis的key相同问题

以上代码虽然基本正确,但是还是没有满足业务需要,发现redis进行set操作时候并没有区分四个不同的接口。所以需要在redisHandle方法中添加一个参数businessKey(这边考虑用接口路径简单拼接),以取分出不同接口的数据。

/**
 * function:具体的service方法调用
 * areaCode:地区编码
 * areaLevel:地区层级 镇级:2
 * typeList:人员类型
 * returnClass: 返回类型
 * businessKey:redis业务键,用来区分不同接口的业务数据
 */
private <T> T redisHandle(Function3 function, String areaCode, Integer areaLevel, String typeList, Class returnClass, String businessKey) {
    T result;
    // redis中存取所用的key
    String redisKey = businessKey + "::" + areaCode + "::" + areaLevel + "::" + typeList;
    // 只对地区层级为镇级的数据进行Redis缓存
    if (areaLevel.equals(2)) {
        // redisUtils从Redis中读取已被缓存的数据
        result = (T) redisUtils.get(redisKey, returnClass);
        if (result == null) {
            result = (T) function.apply(areaCode, areaLevel, typeList);
            // 设置redis键能够区分是来自不同的业务接口的
            redisUtils.set(redisKey);
        }
    } else {
        result = (T) function.apply(areaCode, areaLevel, typeList);
    }
    return result;
}
/**
 * 传入businessKey参数
 *
 * 地区人口统计
 */
@GetMapping("/population-area-statistic")
public R populationAreaStatistic(@RequestParam String areaCode,
                                 @RequestParam Integer areaLevel,
                                 String typeList) {
    // 多添加了参数 "important-population::population-area-statistic",是由接口路径拼接的,能唯一标识该接口
    List<PieChartWithCode> baseInfoList = redisHandle((a, b, c) ->
            populationImageService.populationAreaStatistic((String) a, (Integer) b, (List<String>) c),
            areaCode, areaLevel, typeList, List.class, "important-population::population-area-statistic");

    return R.data(baseInfoList);
}

总结

之前函数式接口用的最多的也只是lambda表达式,这次是新的尝试。当然了,redisHandle方法的代码里idea给报了很多警告还没能解决。以后多多用函数式接口,让自己的代码既高效又美观。