策略模式:助你消除丑陋的 if else 多分支代码

Posted codeboyzhou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了策略模式:助你消除丑陋的 if else 多分支代码相关的知识,希望对你有一定的参考价值。

开发场景举例

让我们以一个实际开发场景来切入这篇文章的正题。现在,假设需要开发这样一个需求:购物车商品结算时需要根据用户会员等级进行打折。

我们假设用户会员等级被分为几个档次:青铜、白银、黄金、钻石、王者,对应折扣分别为:九折、八折、七折、六折、五折。

那么,我们很容易想到的一种实现方式,就是像下面这样的代码:

/**
 * 计算用户最终应支付的订单总金额
 *
 * @param originalMoney 打折前用户应支付的订单总金额
 * @param userVipLevel 用户会员等级
 *
 * @return 用户最终应支付的订单总金额
 */
public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    if (userVipLevel == UserVipLevelEnum.BRONZE) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    } else if (userVipLevel == UserVipLevelEnum.SILVER) {
        return originalMoney.multiply(BigDecimal.valueOf(0.8));
    } else if (userVipLevel == UserVipLevelEnum.GOLD) {
        return originalMoney.multiply(BigDecimal.valueOf(0.7));
    } else if (userVipLevel == UserVipLevelEnum.DIAMOND) {
        return originalMoney.multiply(BigDecimal.valueOf(0.6));
    } else if (userVipLevel == UserVipLevelEnum.SUPER_VIP) {
        return originalMoney.multiply(BigDecimal.valueOf(0.5));
    }
    return originalMoney;
}

这段代码乍一看没有什么问题,也可以满足需求。但是我们不妨稍微思考一下,此刻这段代码看起来比较简单,是因为需求本身很简单,只是需要一个打折的运算。如果后期的需求继续增加,需要我们根据用户会员等级的不同做更多的区别性操作,那么这个 needPay 方法可能会很快变得臃肿;再比如,后期的用户会员又多了一些其它的等级,那么 if else 的分支也将随之增多,代码阅读起来也会让人很眼花。

初步优化思路

针对可能会增加的后期需求,假如需要我们根据用户会员等级的不同做更多的区别性操作,也许可以考虑把各个操作按用户会员等级抽取成不同的方法,就像下面的代码这样:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    if (userVipLevel == UserVipLevelEnum.BRONZE) {
        return bronzeUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.SILVER) {
        return silverUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.GOLD) {
        return goldUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.DIAMOND) {
        return diamondUserNeedPay(originalMoney);
    } else if (userVipLevel == UserVipLevelEnum.SUPER_VIP) {
        return superVipUserNeedPay(originalMoney);
    }
    return originalMoney;
}

这算是一个比较简单的优化思路,也比较容易想到,它从一定程度上解决了 needPay 方法可能会臃肿的问题,但是很明显,这段代码还是没有解决 if else 分支过多的问题。那么我们在这里再进一步思考一下,其实可以使用设计模式当中的策略模式来解决 if else 分支过多的问题。

使用策略模式

策略模式的概念我这里就不过多描述了,这篇文章主要以代码为切入点,力求理解起来更加直观。

搭建策略模式框架

使用策略模式需要我们先定义一个 Java 接口,这个接口用来描述某种要实现的策略,比如对应本文举的例子就是用户支付策略。除此之外,还需要定义一个该接口需要规范的统一行为,即此处的用户支付行为。接口的定义大致像下面的代码这样:

/**
 * 用户支付策略统一接口规范
 */
public interface UserPaymentStrategy {
    /**
     * 计算用户最终应支付的订单总金额
     *
     * @param originalMoney 用户应支付的原始订单总金额
     *
     * @return 用户最终应支付的订单总金额
     */
    BigDecimal needPay(BigDecimal originalMoney);
}

有了接口定义,自然就应该有对应的接口实现,而实现这个策略接口的过程,其实就是在定义每一种不同策略的实现方式,比如此处我们以青铜、白银、黄金会员为例,分别实现这三类用户的具体支付策略,外加一种默认的用户支付策略。代码大致像下面这样:

/**
 * 青铜用户支付策略
 */
public class BronzeUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    }
}
/**
 * 白银用户支付策略
 */
public class SilverUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.8));
    }
}
/**
 * 黄金用户支付策略
 */
public class GoldUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.7));
    }
}
/**
 * 默认的用户支付策略
 */
public class DefaultUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney;
    }
}

策略模式简单调用

有了具体的策略实现,那么实际使用时的代码该怎么写呢?我们先来看一种简单直接的使用方式,直接在业务代码中做策略选择:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    UserPaymentStrategy userPaymentStrategy = new DefaultUserPaymentStrategy();

    if (userVipLevel == UserVipLevelEnum.BRONZE) {
        userPaymentStrategy = new BronzeUserPaymentStrategy();
    } else if (userVipLevel == UserVipLevelEnum.SILVER) {
        userPaymentStrategy = new SilverUserPaymentStrategy();
    } else if (userVipLevel == UserVipLevelEnum.GOLD) {
        userPaymentStrategy = new GoldUserPaymentStrategy();
    }

    return userPaymentStrategy.needPay(originalMoney);
}

更优雅的调用方式

上述的简单直接调用策略的方式,看起来已经使用到了策略,但实际上并没有消除掉 if else 的多分支代码。在实际调用策略模式的过程中,我们其实还需要结合策略工厂来封装策略的选择过程,以隐藏 if else 分支细节。这里所说的策略工厂,其实就是实现一个选择策略的简单工厂模式

/**
 * 用户支付策略工厂
 */
public class UserPaymentStrategyFactory {
    /**
     * 根据用户会员等级选择合适的用户支付策略
     * 
     * @param userVipLevel 用户会员等级
     *                     
     * @return 对应的用户支付策略
     */
    public static UserPaymentStrategy getUserPaymentStrategy(UserVipLevelEnum userVipLevel) {
        if (userVipLevel == UserVipLevelEnum.BRONZE) {
            return new BronzeUserPaymentStrategy();
        } else if (userVipLevel == UserVipLevelEnum.SILVER) {
            return new SilverUserPaymentStrategy();
        } else if (userVipLevel == UserVipLevelEnum.GOLD) {
            return new GoldUserPaymentStrategy();
        }
        return new DefaultUserPaymentStrategy();
    }
}

然后业务代码在具体调用策略代码时就可以像下面这样使用:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    return UserPaymentStrategyFactory.getUserPaymentStrategy(userVipLevel).needPay(originalMoney);
}

这样一来,对于最初的业务方法 needPay 就已经隐藏了很多实现细节,业务层代码看起来会更干净优雅,想要修改某种用户的支付策略,只需要到对应用户的支付策略类中修改对应的实现,而不用担心其它的策略会怎么样。

策略模式优化

彻底消除 if else 分支

上面的代码已经实现了一个基本的策略模式,但是从更加严格的角度来讲,在策略选择工厂里面,其实还是存在着 if else 分支代码。所以我们能否想一个方法来优化下这段代码呢?那么,要想不按照用户会员等级来做 if 判断,就得提前知道用户会员等级和用户支付策略的对应关系,一一对应?可以考虑用 Map<UserVipLevelEnum, UserPaymentStrategy> 来解决,具体代码大致像下面这样:

/**
 * 用户支付策略工厂
 */
public class UserPaymentStrategyFactory {
    /**
     * 存储用户会员等级和用户支付策略的对应关系
     */
    private static Map<UserVipLevelEnum, UserPaymentStrategy> userPaymentStrategyMap;

    // userPaymentStrategyMap 静态初始化
    static {
        userPaymentStrategyMap = new HashMap<>(UserVipLevelEnum.values().length);
        userPaymentStrategyMap.put(UserVipLevelEnum.DEFAULT, new DefaultUserPaymentStrategy());
        userPaymentStrategyMap.put(UserVipLevelEnum.BRONZE, new BronzeUserPaymentStrategy());
        userPaymentStrategyMap.put(UserVipLevelEnum.SILVER, new SilverUserPaymentStrategy());
        userPaymentStrategyMap.put(UserVipLevelEnum.GOLD, new GoldUserPaymentStrategy());
    }
    
    /**
     * 根据用户会员等级选择合适的用户支付策略
     *
     * @param userVipLevel 用户会员等级
     *
     * @return 对应的用户支付策略
     */
    public static UserPaymentStrategy getUserPaymentStrategy(UserVipLevelEnum userVipLevel) {
        return userPaymentStrategyMap.get(userVipLevel);
    }
}

实现策略的自动注册

上面的代码已经借助 Map 消除了之前大段的 if else 分支代码,但是细想一下,还会发现一个小问题,就是当我们需要新增一种支付策略的时候,必须得进入策略工厂来修改现有工厂类的代码。那么,能不能做到新增策略但不需要修改策略工厂类的代码呢?答案是可以的。

怎么做呢?这里提出一种策略注册的思想,大致的思路如下:

  1. 先由策略工厂类提供一个方法,该方法用于进行策略注册,每一种具体的策略实现都必须调用该方法将自己注册进策略工厂,即放进 Map 中;
  2. 同时,在最初的支付策略接口 UserPaymentStrategy 中新增一种行为,就是将策略对象自身注册到策略工厂的方法 register()
  3. 在策略工厂类中再提供一个静态方法,通过反射获取到 UserPaymentStrategy 接口的所有实现类,并依次调用它们的 register() 方法;
  4. 最后,在策略工厂类的初始化静态代码块中调用自动注册所有策略的方法,完成所有支付策略的对象注册。

这样一来,就可以达到新增支付策略时并不需要修改策略工厂类的目的。上述步骤中所提到的代码大致像下面这样:

public class UserPaymentStrategyFactory {
    /**
     * 存储用户会员等级和用户支付策略的对应关系
     */
    private static Map<UserVipLevelEnum, UserPaymentStrategy> userPaymentStrategyMap;

    // userPaymentStrategyMap 静态初始化
    static {
        userPaymentStrategyMap = new HashMap<>(UserVipLevelEnum.values().length);
        autoRegisterAllPaymentStrategies();
    }

    /**
     * 注册具体支付策略到策略工厂
     * 
     * @param userVipLevel 用户会员等级
     * @param paymentStrategy 具体支付策略对象
     */
    public static void registerPaymentStrategy(UserVipLevelEnum userVipLevel, UserPaymentStrategy paymentStrategy) {
        userPaymentStrategyMap.put(userVipLevel, paymentStrategy);
    }

    /**
     * 自动注册所有的支付策略
     */
    public static void autoRegisterAllPaymentStrategies() {
        // 此处用到了 java.util.ServiceLoader 类来获取 UserPaymentStrategy 接口的所有实现类
        // 该类的具体使用方式可以参考网络上其它相关的资源,这里不再赘述
        ServiceLoader.load(UserPaymentStrategy.class).forEach(UserPaymentStrategy::register);
    }
}

注意:上述代码中用到了 java.util.ServiceLoader 这个类来获取 UserPaymentStrategy 接口的所有实现类,该类的具体使用方式可以参考网络上其它相关的资源,这里不再赘述。当然,如果项目是依托于 Spring 开发框架,那么可以利用 Spring 的容器来获取所有的实现类。

支付策略接口 UserPaymentStrategy 所需要做出的改动如下,新增 register() 方法:

public interface UserPaymentStrategy {

    BigDecimal needPay(BigDecimal originalMoney);

    /**
     * 策略将自身对象注册到策略工厂
     */
    void register();
}

青铜用户支付策略实现类为例,将它自己的实例对象注册到策略工厂当中,大致的代码像下面这样:

public class BronzeUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    }

    @Override
    public void register() {
        UserPaymentStrategyFactory.registerPaymentStrategy(UserVipLevelEnum.BRONZE, this);
    }
}

使用注解进一步优化

上面的代码看似已经比较完美了,既消除了 if else 又保护了策略工厂类,但是仔细看仿佛又引入了新的问题:比如策略实现类中的 register() 方法,其实相似性是非常高的。在我们编码的过程当中,相似性非常高的代码往往是一种警示,提示你需要对它们做更进一步的抽象。那么,上面的代码还能再进行怎样的优化呢?我们可以考虑使用注解来进一步降低代码的耦合度,达到更进一步的优化。

先创建一个注解类,用于标识用户会员等级:

/**
 * 用户会员等级注解
 */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserVipLevel {
    UserVipLevelEnum value() default UserVipLevelEnum.DEFAULT;
}

接着,可以删掉 UserPaymentStrategy 接口及其实现类当中的 register() 方法了,取而代之的是,在各个策略实现类上加上上面这个用户会员等级注解,以青铜用户支付策略的实现类为例:

@UserVipLevel(UserVipLevelEnum.BRONZE)
public class BronzeUserPaymentStrategy implements UserPaymentStrategy {
    @Override
    public BigDecimal needPay(BigDecimal originalMoney) {
        return originalMoney.multiply(BigDecimal.valueOf(0.9));
    }
}

最后,需要对策略工厂类的自动注册所有策略方法做一点修改,具体的实现可以参考最终修改好的策略工厂代码:

public class UserPaymentStrategyFactory {
    /**
     * 存储用户会员等级和用户支付策略的对应关系
     */
    private static Map<UserVipLevelEnum, UserPaymentStrategy> userPaymentStrategyMap;

    // 自动注册所有的支付策略
    static {
        userPaymentStrategyMap = new HashMap<>(UserVipLevelEnum.values().length);
        autoRegisterAllPaymentStrategies();
    }

    // 之前这里的 registerPaymentStrategy() 方法也已经不再需要了

    /**
     * 自动注册所有的支付策略
     */
    public static void autoRegisterAllPaymentStrategies() {
        // 此处用到了 java.util.ServiceLoader 类来获取 UserPaymentStrategy 接口的所有实现类
        // 该类的具体使用方式可以参考网络上其它相关的资源,这里不再赘述
        ServiceLoader.load(UserPaymentStrategy.class).forEach(paymentStrategy -> {
            // 获取用户会员等级注解
            UserVipLevel userVipLevel = paymentStrategy.getClass().getAnnotation(UserVipLevel.class);
            userPaymentStrategyMap.put(userVipLevel.value(), paymentStrategy);
        });
    }

    /**
     * 根据用户会员等级选择合适的用户支付策略
     *
     * @param userVipLevel 用户会员等级
     *
     * @return 对应的用户支付策略
     */
    public static UserPaymentStrategy getUserPaymentStrategy(UserVipLevelEnum userVipLevel) {
        return userPaymentStrategyMap.get(userVipLevel);
    }
}

至此,整个策略模式的实现代码又少了一点,也更优雅了一点,再新增新的支付策略还是无需修改策略工厂。而且你或许也注意到了,这整个过程中也都没有再修改过业务代码对策略模式的调用过程,还是当初那一行最简洁的调用方式:

public BigDecimal needPay(BigDecimal originalMoney, UserVipLevelEnum userVipLevel) {
    return UserPaymentStrategyFactory.getUserPaymentStrategy(userVipLevel).needPay(originalMoney);
}

总结

  1. 必要时使用 Map 建立对象映射关系,可以避免 if else 多分支操作。
  2. 想通过接口调用到所有实现类的某个方法,可以考虑是否能在相关代码中使用反射技术。
  3. 能用到反射的地方就可以用到注解,二者结合,往往可以达到更好的降低代码耦合效果。

以上全部代码均已 push 到我个人的代码仓库,欢迎点击查阅

以上是关于策略模式:助你消除丑陋的 if else 多分支代码的主要内容,如果未能解决你的问题,请参考以下文章

策略模式+工厂方法消除if...else

使用责任链模式消除if分支实践

Java嵌套if else优化

优雅的替换if-else语句

优雅的替换if-else语句

Spring 实现策略模式--自定义注解方式解耦if...else