美文网首页互联网科技老男孩的成长之路Java架构技术进阶
开发小记:用函数式编程优化代码可读性,减少一半行数

开发小记:用函数式编程优化代码可读性,减少一半行数

作者: java菲菲 | 来源:发表于2020-03-06 16:41 被阅读0次

前言

本文主要是记录一下用lambda 表达式优化代码的经历,篇幅不长,算是分享我觉得不错的一个小技巧。
话不多说,直接进入正题。
正文
我们先来看这么一段代码:

@Component
public class ConfigCacheHelper {

    private final RedisHelper redisHelper;

    private final IChannelConfigMapper iChannelConfigMapper;

    @Autowired
    public ConfigCacheHelper(RedisHelper redisHelper, IChannelConfigMapper iChannelConfigMapper) {
        this.redisHelper = redisHelper;
        this.iChannelConfigMapper = iChannelConfigMapper;
    }

    public AaaChannelConfig getAaaChannelConfig(String merchantId){
        if (StringUtils.isEmpty(merchantId)){
            throw new IllegalArgumentException("商户号不能为空");
        }
        Object obj = redisHelper.hget(RedisKey.CHANEL_CONFIG, RedisKey.AAA_CHANNEL);
        AaaChannelConfig config;
        if (obj == null){
            config = iChannelConfigMapper.selectAaaChannelConfig(merchantId);
        }
        else {
            Map<String, AaaChannelConfig> map = (Map<String, AaaChannelConfig>)obj;
            config = map.get(merchantId);
        }

        return Objects.requireNonNull(config, "获取Aaa渠道配置为空");
    }

    public BbbChannelConfig getBbbChannelConfig(String merchantId){
        if (StringUtils.isEmpty(merchantId)){
            throw new IllegalArgumentException("商户号不能为空");
        }
        Object obj = redisHelper.hget(RedisKey.CHANEL_CONFIG, RedisKey.BBB_CHANNEL);
        BbbChannelConfig config;
        if (obj == null){
            config = iChannelConfigMapper.selectBbbChannelConfig(merchantId);
        }
        else {
            Map<String, BbbChannelConfig> map = (Map<String, BbbChannelConfig>)obj;
            config = map.get(merchantId);
        }

        return Objects.requireNonNull(config, "获取Bbb渠道配置为空");
    }

    public CccChannelConfig getCccChannelConfig(String merchantId, String posId, String operatorId){
        if (StringUtils.isEmpty(merchantId)){
            throw new IllegalArgumentException("商户号不能为空");
        }
        Object obj = redisHelper.hget(RedisKey.CHANEL_CONFIG, RedisKey.CCC_CHANNEL);
        CccChannelConfig config;
        if (obj == null){
            config = iChannelConfigMapper.selectCccChannelConfig(merchantId, posId, operatorId);
        }
        else {
            Map<String, CccChannelConfig> map = (Map<String, CccChannelConfig>)obj;
            config = map.get(String.format("%s_%s_%s", merchantId, posId, operatorId));
        }

        return Objects.requireNonNull(config, "获取Ccc渠道配置为空");
    }

    // ... 此处再省略N个渠道的config

}

俺是做支付的,这段代码的逻辑很简单,就是获取某个支付渠道的商户配置,缓存取不到就去数据库取。
在IDEA乍一看,我倒没看出什么问题,代码检查插件也没有报什么warning,但是当我在这个类里面新增获取第N个渠道的方法的时候,我就感觉到了这块代码不是很优雅。
总结出来两点:

多余的StringUtils.isEmpty(merchantId)

        if (StringUtils.isEmpty(merchantId)){
            throw new IllegalArgumentException("商户号不能为空");
        }

理由有二:

判断字符串为空应该是调用者的责任
外部的业务逻辑早就确保merchantId 不可能为空字符串,没必要再判断

getXXXChannelConfig逻辑可以提取成如下
    public AaaChannelConfig getAaaChannelConfig(String merchantId){
        // if (StringUtils.isEmpty(merchantId)){
        //     throw new IllegalArgumentException("商户号不能为空");
        // }
        // 1️⃣
        Object obj = redisHelper.hget(RedisKey.CHANEL_CONFIG, {渠道键值});
        AaaChannelConfig config;
        if (obj == null){
            // 2️⃣
            // selectBbbChannelConfig()
            // selectCccChannelConfig(merchantId, posId, operatorId)
            config = iChannelConfigMapper.{取某个渠道配置的方法}(...);
        }
        else {
            Map<String, AaaChannelConfig> map = (Map<String, AaaChannelConfig>)obj;
            // 3️⃣
            // config = map.get(String.format("%s_%s_%s", merchantId, posId, operatorId));
            // config = map.get(merchantId);
            config = map.get({渠道配置Map的键值});
        }

        return Objects.requireNonNull(config, "获取Aaa渠道配置为空");
    }

第一点很简单,不讲了。主要来讲下第二点,从上面的分析中,就可以抽取出3个变量:

  • 渠道键值
  • 去数据库中取配置的函数 - 配置的提供者
  • 渠道配置Map的键值

这样子我们就可以把代码改造成下面这样:

    private <T> T getChannelConfig(String configKey, String configMapInnerKey,
                                   Supplier<T> daoSupplier) {
        Object obj = redisHelper.hget(RedisKey.CHANNELPROXY_HKEY, configKey);
        T config;
        if (obj == null){
            config = daoSupplier.get();
        } else {
            Map<String, T> map = (Map<String, T>)obj;
            config = map.get(configMapInnerKey);
        }
        return Objects.requireNonNull(config, "获取渠道配置为空, 渠道值:" + configKey);
    }
    public AaaChannelConfig getAaaChannelConfig(String merchantId){
        return getChannelConfig(
            RedisKey.AAA_CHANNEL, merchantId,
            () -> iChannelConfigMapper.selectAaaChannelConfig(merchantId)
        );
    }
    public BbbChannelConfig getBbbChannelConfig(String merchantId){
        return getChannelConfig(
            RedisKey.BBB_CHANNEL, merchantId,
            () -> iChannelConfigMapper.selectBbbChannelConfig(merchantId)
        );
    }
    public CccChannelConfig getCccChannelConfig(String merchantId, String posId, String operatorId){
        return getChannelConfig(
            RedisKey.CCC_CHANNEL, String.format("%s_%s_%s", merchantId, posId, operatorId),
            () -> iChannelConfigMapper.selectCccChannelConfig(merchantId, posId, operatorId)
        );
    }

这里简单提一下Supplier,这是java.util.function中提供的函数式接口,用来支持Java 中的函数式编程。从语义上理解就是“T的提供者”,比如在上文语境中就是对应渠道配置的提供者。类似的常用接口还有:

image.png

优化结果
这样优化之后,提升了代码的可读性,在实现相同功能的前提下,比原来减少了一半的代码量。(233 -> 137)
如何抛出我想要的异常?
这里想要再提一个场景,因为之前有碰到过,就是如何让你的Function抛出异常?
还是拿上述代码为例,

    private <T> T getChannelConfig(String configKey, String configMapInnerKey,
                                   Supplier<T> daoSupplier) {
        Object obj = redisHelper.hget(RedisKey.CHANNELPROXY_HKEY, configKey);
        T config;
        if (obj == null){
            // 假设我想让这个方法抛出一个自定义的BizException 业务异常,怎么办?
            config = daoSupplier.get();
        } else {
            Map<String, T> map = (Map<String, T>)obj;
            config = map.get(configMapInnerKey);
        }
        return Objects.requireNonNull(config, "获取渠道配置为空, 渠道值:" + configKey);
    }

我刚开始想了蛮久的,后面发现这实际上是属于Java 基础方面的知识。
我们传入的参数daoSuppier实际上相当于是一个函数式接口Supplier的匿名内部实现类而已(当然底层实现是不一样的,在某种意义上比匿名内部类好很多,无论是性能,可读性还是使用趋势)

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

接口可以声明抛出某个异常,它的实现可以不抛出异常,反之呢,如果它的实现抛出了受检异常,这个接口就必须显式声明抛出这个异常。
所以这种情况下,我们如果想我们的Supplier抛出我们想要的异常,那么就需要自己声明一个Functional Interface,

    public interface DaoSupplier<T> {

        /**
         * Gets a result.
         *
         * @return a result
         */
        T get() throws BizException;
    }

再次改造后的代码:

    private <T> T getChannelConfig(String configKey, String configMapInnerKey,
                                   DaoSupplier<T> daoSupplier) throws BizException {
        Object obj = redisHelper.hget(RedisKey.CHANNELPROXY_HKEY, configKey);
        T config;
        if (obj == null){
            config = daoSupplier.get();
        } else {
            Map<String, T> map = (Map<String, T>)obj;
            config = map.get(configMapInnerKey);
        }
        return Objects.requireNonNull(config, "获取渠道配置为空, 渠道值:" + configKey);
    }
    
    public AaaChannelConfig getAaaPayChannelConfig(String merchantId) throws BizException {
        return getChannelConfig(
                RedisKey.AAA_CHANNEL, merchantId,
                () -> Optional.ofNullable(iChannelConfigMapper.selectAliPayChannelConfig(appId))
                        .orElseThrow(BizException::new)
        );
    }

结语

本文并没有引入很多的Java Lambda的原理性介绍、API介绍,因为本身就是个人的开发小记,偏重于实践,引入太多的知识性介绍反而偏离了本意。希望Java lambda 不熟悉的同学可以自己学习下相关资料。

作者:Richard_Yi
链接:https://juejin.im/post/5e61a0bdf265da572e4f3492

相关文章

  • 开发小记:用函数式编程优化代码可读性,减少一半行数

    美团一面凉凉:MySQL+Java+Redis+算法+网络+Linux等一个都讲不清 秋招JAVA面试总结:Jav...

  • 函数式编程

    一 函数式编程的好处 1. 代码简洁,开发快速 函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度...

  • JavaScript之函数式编程

    文章更新中... 函数式编程用简短、紧凑的代码完成工作,代码风格可读性强,更突出所解决的问题。 命令式编程和声明式...

  • JavaScript代码优化

    js代码优化 1️⃣函数变量使用驼峰式,提高函数名的可读性2️⃣使用&& 减少if判断,应用场景是&&前使用boo...

  • RxJava使用指南

    前言 RxJava的开发模式越来越受开发者欢迎,响应式的编程不仅可以减少代码量,还增加了代码的可读性,让开发维护再...

  • iOS Developer 初级面试常见问题总结

    iOS 开发 RunLoop Cell 图片异步加载优化 iOS 函数式编程的实现 && 响应式编程概念 内存恶鬼...

  • 编程技能

    基于接口设计与编程 Java代码优化 精练代码:一次 Java 函数式编程的重构之旅 软件开发之技能梳理 一句话的...

  • RectiveCocoa的学习(一)

    RectiveCocoa 的代码风格 函数式编程 响应式编程 ReactiveCocoa在iOS开发时的作用有:替...

  • 浅谈JavaScript函数式编程思想

    一。函数式编程概述 首先我们需要函数式编程,主要是因为函数式编程的可读性和可维护性都较好,下图是《JavaScri...

  • 函数式编程感想

    用函数式申明的编程风格取代计算机命令式风格,代码更简洁易懂。 用递归取代迭代,用不可变取代可变。当然语言对迭代有优化。

网友评论

    本文标题:开发小记:用函数式编程优化代码可读性,减少一半行数

    本文链接:https://www.haomeiwen.com/subject/hbcwrhtx.html