開發小記 - 用函數式編程優化代碼可讀性,減小一半行數

前言

本文主要是記錄一下用lambda 表達式優化代碼的經歷,篇幅不長,算是分享我以爲不錯的一個小技巧。java

話很少說,直接進入正題。redis

正文

咱們先來看這麼一段代碼:數據庫

@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個渠道的方法的時候,我就感受到了這塊代碼不是很優雅。緩存

總結出來兩點:app

  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("商戶號不能爲空");
            // }
            // 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的提供者」,好比在上文語境中就是對應渠道配置的提供者。相似的經常使用接口還有:

接口 參數 返回類型
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 lambda 不熟悉的同窗能夠本身學習下相關資料。

若是本文有幫助到你,但願能點個贊,這是對個人最大動力🤝🤝🤗🤗。
相關文章
相關標籤/搜索