50

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

 4 years ago
source link: https://ricstudio.top/archives/note_lambda_beatify_code
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

本文主要是记录一下用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个渠道的方法的时候,我就感觉到了这块代码不是很优雅。

我总结出来两点:

  1. 多余的 StringUtils.isEmpty(merchantId)

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

    理由有二:

    • 判断字符串为空应该是调用者的责任
    • 外部的业务逻辑早就确保merchantId 不可能为空字符串,没必要再判断
  2. getXXXChannelConfig 逻辑可以提取成如下

    public AaaChannelConfig getAaaChannelConfig(String merchantId){
            // if (StringUtils.isEmpty(merchantId)){
            //     throw new IllegalArgumentException("商户号不能为空");
            // }
            // :one:
            Object obj = redisHelper.hget(RedisKey.CHANEL_CONFIG, {渠道键值});
            AaaChannelConfig config;
            if (obj == null){
                // :two:
                // selectBbbChannelConfig()
                // selectCccChannelConfig(merchantId, posId, operatorId)
                config = iChannelConfigMapper.{取某个渠道配置的方法}(...);
            }
            else {
                Map<String, AaaChannelConfig> map = (Map<String, AaaChannelConfig>)obj;
                // :three:
                // 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的提供者”,比如在上文语境中就是对应渠道配置的提供者。类似的常用接口还有:

接口 参数 返回类型 Predicate<T> T boolean Consumer<T> T void Function<T,R> T R Supplier<T> None T UnaryOperator<T> T T

优化结果

这样优化之后,提升了代码的可读性,在实现相同功能的前提下,比原来减少了一半的代码量。(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 函数式不熟悉的同学可以自己学习下相关资料。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力 。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK