策略模式:助你消除丑陋的 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
分支代码,但是细想一下,还会发现一个小问题,就是当我们需要新增一种支付策略的时候,必须得进入策略工厂来修改现有工厂类的代码。那么,能不能做到新增策略但不需要修改策略工厂类的代码呢?答案是可以的。
怎么做呢?这里提出一种策略注册的思想,大致的思路如下:
- 先由策略工厂类提供一个方法,该方法用于进行策略注册,每一种具体的策略实现都必须调用该方法将自己注册进策略工厂,即放进
Map
中; - 同时,在最初的支付策略接口
UserPaymentStrategy
中新增一种行为,就是将策略对象自身注册到策略工厂的方法register()
; - 在策略工厂类中再提供一个静态方法,通过反射获取到
UserPaymentStrategy
接口的所有实现类,并依次调用它们的register()
方法; - 最后,在策略工厂类的初始化静态代码块中调用自动注册所有策略的方法,完成所有支付策略的对象注册。
这样一来,就可以达到新增支付策略时并不需要修改策略工厂类的目的。上述步骤中所提到的代码大致像下面这样:
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);
}
总结
- 必要时使用
Map
建立对象映射关系,可以避免if else
多分支操作。 - 想通过接口调用到所有实现类的某个方法,可以考虑是否能在相关代码中使用反射技术。
- 能用到反射的地方就可以用到注解,二者结合,往往可以达到更好的降低代码耦合效果。
!--more-->以上全部代码均已 push 到我个人的代码仓库,欢迎点击查阅。
以上是关于策略模式:助你消除丑陋的 if else 多分支代码的主要内容,如果未能解决你的问题,请参考以下文章