最近在梳理支付逻辑,发现里面有很多需要优化的地方,本文主要聊一聊如何重构那些令人头秃的if...else。
先来看一下目前系统中的支付方式判断代码(简化了业务逻辑):
//支付接口
public interface IPay {
void pay();
}
//支付宝方式
@Service
public class AliPay implements IPay {
@Override
public void pay() {
System.out.println("go to ali pay");
}
}
//微信方式
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("go to weixin pay");
}
}
//招商银行方式
@Service
public class ZhaoshangPay implements IPay {
@Override
public void pay() {
System.out.println("go to zhaoshang pay");
}
}
//发起支付,选择支付方式
@Service
public class PayService {
@Autowired
private AliPay aliPay;
@Autowired
private WeixinPay weixinPay;
@Autowired
private ZhaoshangPay zhaoshangPay;
public void gotoPay(String code){
if("ali".equals(code)){
aliPay.pay();
}else if("weixin".equals(code)){
weixinPay.pay();
}else if("zhaoshang".equals(code)){
zhaoshangPay.pay();
}else{
System.out.println("the pay type is not in service");
}
}
}
可以看到支付类PayService的gotoPay方法就是根据传入的code来决定最终调用哪种支付方式。目前项目代码就是这样,没出什么问题,但是如果后续接入京东支付、百度支付、云闪付等方式,那么就需要修改gotoPay方法的逻辑,新增新的if...else分支结构,进而导致逻辑越来越复杂。显然这里违反了软件设计模式六大原则中的开闭原则(对扩展开放,对修改封闭,即增加新功能时尽量修改旧的代码)和单一职责原则(一个类所承担的责任最好单一,不要太过于负责)。
因此接下来笔者将结合自身经历和一些好的方法来尝试对其进行改造。
其实使用switch结构,本质上和使用if...else结构差别不大,只是看起来更加清晰明了:
@Service
public class PayService2 {
@Autowired
private AliPay aliPay;
@Autowired
private WeixinPay weixinPay;
@Autowired
private ZhaoshangPay zhaoshangPay;
public void gotoPay(String code){
switch (code){
case "ali":
aliPay.pay();
break;
case "weixin":
weixinPay.pay();
break;
case "zhaoshang":
zhaoshangPay.pay();
break;
default:
System.out.println("the pay type is not in service");
}
}
}
由于支付代码payCode和支付类之间不存在绑定关系,因此前面我们需要通过传入的payCode来判断实际应当调用的支付方式。也就是说,如果我们定义了支付代码payCode和支付类之间的绑定关系,那样就可以直接调用实际支付方式。
毫无疑问,我们需要获取到当前程序的ApplicationContext
并从中获取到对应的Bean实例,由于ApplicationContext
事件机制实际上是采用了观察者模式,因此我们可以通过ApplicationEvent
类和ApplicationListener
接口,进而实现ApplicationContext
事件处理。
请注意,如果Spring容器中存在ApplicationListener
子类,那么只要ApplicationContext
发布ApplicationEvent
,ApplicationListener
子类将会自动触发,当然这种触发必须通过程序来显式触发。
Spring中存在一些内置事件,其实就是当完成某种操作时就会发出某些事件动作,通过监听这些动作,开发者就能定义和实现不同的业务逻辑。举个例子,当Spring容器中所有的Bean都初始化完成并被成功装载时,ContextRefreshedEvent
事件将会被触发,那么实现ApplicationListener<ContextRefreshedEvent>
接口的对象就知道这个事件被触发了,之后就可以书写对应的逻辑。关于这一块的内容,这里不细说,此处使用到了ContextRefreshedEvent
事件。
这里以注解为例来演示如何绑定两者之间的关系。
第一步,定义一个注解,属性值为value,这个就是payCode的值:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PayCode {
String value();
}
第二步,在所有的支付类上添加该注解:
@PayCode(value="ali")
@Service
public class AliPay implements IPay {
@Override
public void pay() {
System.out.println("go to ali pay");
}
}
@PayCode(value="weixin")
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("go to weixin pay");
}
}
@PayCode(value="zhaoshang")
@Service
public class ZhaoshangPay implements IPay {
@Override
public void pay() {
System.out.println("go to zhaoshang pay");
}
}
第三步,新建PayService3支付类,需要实现ApplicationListener<ContextRefreshedEvent>
接口,注意它需要重写onApplicationEvent
方法,该方法的逻辑是获取ApplicationContext
对象并从中得到PayCode的子类,之后遍历这些子类并构建一个Map对象,key为支付代码payCode,value则为IPay子类,后续我们就能通过code来获取对应的支付方式:
@Service
public class PayService3 implements ApplicationListener<ContextRefreshedEvent> {
private static Map<String,IPay> payMap = null;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ApplicationContext applicationContext = event.getApplicationContext();
//注意key为beanName, value为beanInstance
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(PayCode.class);
if(beans != null){
payMap = new HashMap<>();
beans.forEach((key,value)->{
String payCodeValue = value.getClass().getAnnotation(PayCode.class).value();
payMap.put(payCodeValue,(IPay) value);
System.out.println(payCodeValue+">>>>>>>>>>>>>"+value);
});
}
}
public void gotoPay(String code){
payMap.get(code).pay();
}
}
可以看到使用这种方式后,可以直接通过传入的code来获取支付类实例,当需要新增支付方式时,只需在支付类上添加@PayCode
注解,并设置对应的支付代码payCode的值。使用这种方式时,传入的code的值可以没有业务含义,只要与@PayCode
注解中的value属性值能匹配即可。
责任链模式是指将请求的处理对象像一条长链一样组合起来,形成对象链。请求并不知道具体执行请求的对象是哪个,因此它解耦了请求与执行请求对象。
责任链模式在Spring框架中应用较为广泛,如Filter和AOP就是典型的例子,这里笔者简化一下相关逻辑。
第一步,新建PayHandler抽象类,里面定义一个PayHandler类型的next属性,表示处理完对应逻辑后应当返回的对象,即返回处理的对象本身,这样就构成了一个链式调用:
public abstract class PayHandler {
protected PayHandler next;
public PayHandler getNext() {
return next;
}
public void setNext(PayHandler next) {
this.next = next;
}
public abstract void pay(String payCode);
}
第二步,定义所有的支付类,这些支付类需要继承PayHandler类并重写其中的pay方法,pay方法则是实际的执行逻辑,如果满足则执行对应逻辑,否则返回该对象:
@Service
public class AliPayHandler extends PayHandler{
@Override
public void pay(String payCode) {
if("ali".equals(payCode)){
System.out.println("go to ali pay");
}else{
getNext().pay(payCode);
}
}
}
@Service
public class WeixinPayHandler extends PayHandler{
@Override
public void pay(String payCode) {
if("weixin".equals(payCode)){
System.out.println("go to weixin pay");
}else{
getNext().pay(payCode);
}
}
}
@Service
public class ZhaoshangHandler extends PayHandler{
@Override
public void pay(String payCode) {
if("zhaoshang".equals(payCode)){
System.out.println("go to zhaoshang pay");
}else{
getNext().pay(payCode);
}
}
}
第三步,定义支付链类,需要实现 ApplicationContextAware
和InitializingBean
接口,其中实现ApplicationContextAware
接口是为了获取ApplicationContext
对象,然后从中获取对应的信息:
@Service
public class PayHandlerChain implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private PayHandler payHandler;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, PayHandler> beansOfTypeMap = applicationContext.getBeansOfType(PayHandler.class);
if(beansOfTypeMap == null || beansOfTypeMap.size() == 0){
return;
}
List<PayHandler> payHandlers = new ArrayList<>(beansOfTypeMap.values());
for (int i=0;i< payHandlers.size();i++){
PayHandler payHandler = payHandlers.get(i);
if(i!= payHandlers.size()-1){
payHandler.setNext(payHandlers.get(i+1));
}
}
payHandler = payHandlers.get(0);
}
public void handlerPay(String code){
payHandler.pay(code);
}
}
责任链模式这种在解决if...else结构中非常有效,但是实现起来较为困难,对开发人员的能力要求较高。
模板方法这种方式灵感来源于Spring AOP的源码,查看一下其中DefaultAdvisorAdapterRegistry#wrap()
方法的源码:
public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
if (adviceObject instanceof Advisor) {
return (Advisor)adviceObject;
} else if (!(adviceObject instanceof Advice)) {
throw new UnknownAdviceTypeException(adviceObject);
} else {
Advice advice = (Advice)adviceObject;
if (advice instanceof MethodInterceptor) {
return new DefaultPointcutAdvisor(advice);
} else {
Iterator var3 = this.adapters.iterator();
AdvisorAdapter adapter;
do {
if (!var3.hasNext()) {
throw new UnknownAdviceTypeException(advice);
}
adapter = (AdvisorAdapter)var3.next();
} while(!adapter.supportsAdvice(advice));
return new DefaultPointcutAdvisor(advice);
}
}
}
可以看到它遍历了adapters对象,然后调用adapter.supportsAdvice(advice)
方法来判断是否支持对应的通知。AdvisorAdapter
是一个接口,源码如下:
public interface AdvisorAdapter {
boolean supportsAdvice(Advice advice);
MethodInterceptor getInterceptor(Advisor advisor);
}
该接口中的supportsAdvice用于判断当前通知是否支持此种通知,即判断通知的类型,如果支持则调用getInterceptor方法获取对应的方法拦截器。该接口有三个实现类,查看一下这三个类中supportsAdvice方法的实现逻辑:
public boolean supportsAdvice(Advice advice) {
return advice instanceof AfterReturningAdvice;
}
public boolean supportsAdvice(Advice advice) {
return advice instanceof MethodBeforeAdvice;
}
public boolean supportsAdvice(Advice advice) {
return advice instanceof ThrowsAdvice;
}
也就是说我们也可以采取类似的方法,通过定义一个接口或者抽象类,然后在里面新建一个support方法,里面根据传入的code值来判断是否是自己处理,如果是自己处理则走支付逻辑。
第一步,新建一个IPay接口,里面定义两个方法,其中support方法根据传入的code值来判断是否需要自己处理;pay方法则是需要自己处理时的逻辑:
public interface IPay {
boolean support(String code);
void pay();
}
第二步,定义所有的支付类,这些支付类需要实现IPay接口,并重写其中的两个方法:
@Service
public class AliPay implements IPay {
@Override
public void pay() {
System.out.println("go to ali pay");
}
@Override
public boolean support(String code) {
return "ali".equals(code);
}
}
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("go to weixin pay");
}
@Override
public boolean support(String code) {
return "weixin".equals(code);
}
}
@Service
public class ZhaoshangPay implements IPay {
@Override
public void pay() {
System.out.println("go to zhaoshang pay");
}
@Override
public boolean support(String code) {
return "zhaoshang".equals(code);
}
}
第三步,定义支付类PayService5,需要实现 ApplicationContextAware
和InitializingBean
接口,其中实现ApplicationContextAware
接口是为了获取ApplicationContext
对象,然后从中获取对应的信息:
@Service
public class PayService5 implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private List<IPay> payLists = null;
@Override
public void afterPropertiesSet() throws Exception {
if(null == payLists){
payLists = new ArrayList<>();
Map<String, IPay> beans = applicationContext.getBeansOfType(IPay.class);
beans.forEach((key,value)-> payLists.add(value));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void gotoPay(String code){
payLists.forEach(iPay -> {
if(iPay.support(code)){
iPay.pay();
}
});
}
}
可以看到这里我们先将实现了IPay接口的支付类都初始化到一个list集合中,之后在调用该支付接口的时候遍历这个集合,根据传入的code值来判断是否是自己处理,如果是自己处理则走支付逻辑。
策略+工厂模式这种比较适用于code有具体业务含义的场景,其中策略模式定义了一组算法,将它们一个个的封装起来,并且可互相替换;工厂模式则用于封装和管理对象的创建。
第一步,新建一个IPay接口,里面定义一个支付方法:
public interface IPay {
void pay();
}
第二步,定义支付策略工厂类PayStrategyFactory
,里面定义一个全局的map,后续将所有实现IPay接口的实现类都注册到map中,之后在调用的时候通过PayStrategyFactory
类根据传入的code来从map中获取支付类的实例:
public class PayStrategyFactory {
private static Map<String,IPay> PAY_REGISTERS = new HashMap<>();
public static void register(String code,IPay iPay){
if(null != code && !"".equals(code)){
PAY_REGISTERS.put(code,iPay);
}
}
public static IPay get(String code){
return PAY_REGISTERS.get(code);
}
}
第三步,定义所有的支付类,这些支付类需要实现IPay接口,并重写其中的pay方法,以及定义init方法用于将定义的Bean注册到支付策略工厂中:
@Service
public class AliPay implements IPay {
@Override
public void pay() {
System.out.println("go to ali pay");
}
@PostConstruct
public void init(){
PayStrategyFactory.register("ali",this);
}
}
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("go to weixin pay");
}
@PostConstruct
public void init(){
PayStrategyFactory.register("weixin",this);
}
}
@Service
public class ZhaoshangPay implements IPay {
@Override
public void pay() {
System.out.println("go to zhaoshang pay");
}
@PostConstruct
public void init(){
PayStrategyFactory.register("zhaoshang",this);
}
}
第四步,新建支付类PayService6,它的功能较为单一,即通过传入的code从PayStrategyFactory
支付策略工厂中获取支付类实例:
@Service
public class PayService6 {
public void gotoPay(String code){
PayStrategyFactory.get(code).pay();
}
}
动态拼接名称这一方法主要针对code有具体的业务含义,在实际工作中用的也是较为频繁。我们可以假设支付类的bean名称命名格式为“code+后缀”,如aliPay、weixinPay和zhaoShangPay等,那么就可以直接从ApplicationContext
中获取支付实例,由于Spring容器中的Bean默认是单例的,因此放在一个map中是不有性能问题。
新建一个名为PayService4的类,它需要实现ApplicationContextAware
接口:
@Service
public class PayService4 implements ApplicationContextAware {
private ApplicationContext applicationContext;
private static final String SUFFIX = "Pay";
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void gotoPay(String payCode){
((IPay)applicationContext.getBean(getBeanCompleteName(payCode))).pay();
}
public String getBeanCompleteName(String payCode){
return payCode + SUFFIX;
}
}
实际开发过程中使用if...else分支的场景非常多,下面再举几个不同的例子进行分析。
如果满足某个条件才执行某个方法,否则就直接退出时,通常会采用如下方式:
public String update(Integer id){
if(null != id){
return "业务操作";
}
return "";
}
此时可以完全考虑提前将不满足条件的逻辑执行完,即提前return回去:
public String update(Integer id){
if(null == id){
return "";
}
return "业务操作";
}
在根据传入的id来执行删除或者修改操作的时候,通常会采用如下方式:
public String updateOrDelete(Integer id,Integer type){
if(type == 1){
return "更新成功";
}
return "删除成功";
}
此时完全可以使用三目元算符进行代替:
public String updateOrDelete(Integer id,Integer type){
return type==1? "更新成功":"删除成功";
}
如果你在进行某些逻辑处理的时候需要判断某些传入的参数不能为空,此时通俗的做法如下:
public String update(Integer id,Integer type){
if(null == id){
return "id不能为空";
}
if(null == type){
return "type不能为空";
}
return "后续逻辑";
}
可以考虑使用Spring提供的断言来进行判断,这在很多源码中都是有使用的例子:
public String update(Integer id,Integer type){
Assert.notNull(id,"id不能为空");
Assert.notNull(type,"type不能为空");
return "后续逻辑";
}
在实际开发过程中,出现大量的if...else极有可能是因为非空判断:
public String update(Integer id){
if(null == id){
return "";
}
return "业务操作";
}
此时可考虑使用Java8提供的Optional类来替换:
public String update(Integer id){
Optional<Integer> optional = Optional.of(id);
return optional.isPresent()? "业务逻辑":"";
}
表驱动方法是一种让你不需要使用很多的逻辑语句,就能从表中查找信息的方法。简单起见,这里我们使用一个map来模拟表,即从map中查找信息,进而省去不必要的逻辑语句:
if(type.equals(key1)){
doSomething1(id);
}else if(type.equals(key2)){
doSomething2(id);
}else if(type.equals(key3)){
doSomething3(id);
}
使用表驱动方法后,代码可优化为如下所示:
//定义一个map
Map<?, Function<?> function> somethingMaps = new HashMap<>();
// 初始化
somethingMaps .put(value1, (id) -> { doSomething1(id)});
somethingMaps .put(value2, (id) -> { doSomething2(id)});
somethingMaps .put(value3, (id) -> { doSomething3(id)});
// 省去null值判断
somethingMaps.get(type).apply(id);
此处我们使用了Lambda表达式和函数式接口,这样代码看起来非常简洁。
当需要根据订单的状态(一般是数字)来给用户返回相应的提示信息,此时一般的伪代码如下:
public String showToast(Integer code){
if(code==-1){
return "订单已取消!";
}else if(code==0){
return "订单创建成功,待买家支付!";
}else if(code==1){
return "买家已支付,等待卖家接单!";
}else if(code==2){
return "卖家已接单,等待买家提车!";
}else if(code==3){
return "买家申请提车,等待卖家发车!";
}else if(code==4){
return "卖家已发车,等待买家验车!";
}else if(code==5){
return "买家已验车,等待卖家交接手续!";
}else if(code==6){
return "卖家已交接手续,等待买家收车!";
}else if(code==7){
return "买家已确认收车,订单完成!";
}else{
return "订单创建失败!";
}
}
针对这种场景,可以考虑使用枚举类来代替,枚举其实也是一种表驱动方法:
public enum OrderStatusEnum {
CANCEL(-1,"订单已取消!"),
WAITING_BUYER_PAY(0,"订单创建成功,待买家支付!"),
WAITING_SELLER_CONFIRMED(1,"买家已支付,等待卖家接单!"),
WAITING_BUYER_PICKING(2,"卖家已接单,等待买家提车!"),
WAITING_SELLER_SENDING(3,"买家申请提车,等待卖家发车!"),
WAITING_BUYER_CHECKING(4,"卖家已发车,等待买家验车!"),
WAITING_SELLER_EXCHANGE(5,"买家已验车,等待卖家交接手续!"),
WAITING_BUYER_CONFIRMED(6,"卖家已交接手续,等待买家收车!"),
FINISHED(7,"买家已确认收车,订单完成!"),
FAILED(100,"订单创建失败!");
private Integer code;
private String msg;
OrderStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public String getMsg() {
return msg;
}
public static OrderStatusEnum getOrderStatusEnum(Integer code){
return Arrays.stream(OrderStatusEnum.values()).filter(orderStatusEnum -> orderStatusEnum.code.equals(code))
.findFirst().orElse(null);
}
}
之后的调用方法如下所示:
public String showToast(Integer code){
return OrderStatusEnum.getOrderStatusEnum(code).getMsg();
}
这样如果新增了新的状态,只需在枚举类中新增对应的枚举信息即可。
这些方法更多的是为开发者在解决if...else分支的时候提供了新的思考,因此可结合实际情况进行选择。