Press "Enter" to skip to content

设计模式-策略模式

简介

策略模式(Strategy Pattern)属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
其主要目的是通过定义相似的算法,替换if else 语句写法,并且可以随时相互替换。

策略模式主要由这三个角色组成,环境角色(Context)、抽象策略角色(Strategy)和具体策略角色(ConcreteStrategy)。

环境角色(Context):持有一个策略类的引用,提供给客户端使用。
抽象策略角色(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略角色(ConcreteStrategy):包装了相关的算法或行为。

示例一

例如:在网购中,我在支付的时候,可以根据实际情况来选择不同的支付方式(微信支付、支付宝、银行卡支付等等),这些支付方式即是不同的策略。我们通常会看到如下的实现代码:

Order order = 订单信息
if (payType == 微信支付) {
    微信支付流程
} else if (payType == 支付宝) {
    支付宝支付流程
} else if (payType == 银行卡) {
    银行卡支付流程
} else {
    暂不支持的支付方式
}

如上代码,虽然写起来简单,但违反了面向对象的 2 个基本原则:

单一职责原则:一个类只有1个发生变化的原因之后修改任何逻辑,当前方法都会被修改;
开闭原则:对扩展开放,对修改关闭,当我们需要增加、减少某种支付方式(积分支付/组合支付),或者增加优惠券等功能时,不可避免的要修改该段代码。

特别是当 if-else 块中的代码量比较大时,后续的扩展和维护会变得非常复杂且容易出错。

策略模式是解决过多 if-else(或者 switch-case) 代码块的方法之一,提高代码的可维护性、可扩展性和可读性。下面我将从策略的定义、创建和使用这三个方面以上述网购支付为示例来分别进行说明。

策略的定义

策略接口的定义,通常包含两个方法:获取策略类型的方法和处理策略业务逻辑的方法。

/**
 * 第三方支付
 */
public interface Payment {

    /**
     * 获取支付方式
     * 
     * @return 响应,支付方式
     */
    PayTypeEnum getPayType();

    /**
     * 支付调用
     * 
     * @param order 订单信息
     * @return 响应,支付结果
     */
    PayResult pay(Order order);

}

策略接口的实现,每种支付类都实现了上述接口(基于接口而非实现编程),这样我们可以灵活的替换不同的支付方式。下边示例代码展示了每种支付方式的实现:

/**
 * 微信支付
 */
@Component
public class WxPayment implements Payment {

    @Override
    public PayTypeEnum getPayType() {
        return PayTypeEnum.WX;
    }

    @Override
    public PayResult pay(Order order) {
        调用微信支付
        if (成功) {
            return PayResult.SUCCESS;
        } else {
            return PayResult.FAIL;
        }
    }

}
/**
 * 支付宝支付
 */
@Component
public class AlipayPayment implements Payment {

    @Override
    public PayTypeEnum getPayType() {
        return PayTypeEnum.ALIPAY;
    }

    @Override
    public PayResult pay(Order order) {
        调用支付宝支付
        if (成功) {
            return PayResult.SUCCESS;
        } else {
            return PayResult.FAIL;
        }
    }

}
/**
 * 银行卡支付
 */
@Component
public class BankCardPayment implements Payment {

    @Override
    public PayTypeEnum getPayType() {
        return PayTypeEnum.BANK_CARD;
    }

    @Override
    public PayResult pay(Order order) {
        调用银行卡支付
        if (成功) {
            return PayResult.SUCCESS;
        } else {
            return PayResult.FAIL;
        }
    }

}

策略的创建

策略模式包含一组同类的策略,在使用时我们通常通过类型来判断创建哪种策略来进行使用。我们可以使用工厂模式来创建策略,以屏蔽策略的创建细节。如下代码所示:

public class PaymentFactory {
    private static final Map<PayTypeEnum, Payment> payStrategies = new HashMap<>();

    static {
        payStrategies.put(PayTypeEnum.WX, new WxPayment());
        payStrategies.put(PayTypeEnum.ALIPAY, new AlipayPayment());
        payStrategies.put(PayTypeEnum.BANK_CARD, new BankCardPayment());
    }

    public static Payment getPayment(PayTypeEnum payType) {
        if (payType == null) {
            throw new IllegalArgumentException("pay type is empty.");
        }
        if (!payStrategies.containsKey(payType)) {
            throw new IllegalArgumentException("pay type not supported.");
        }
        return payStrategies.get(payType);
    }

}

或者使用 Spring 创建:

@Component
public class PaymentFactory implements InitializingBean, ApplicationContextAware {
    private static final Map<PayTypeEnum, Payment> payStrategies = new HashMap<>();

    private ApplicationContext appContext;

    public static Payment getPayment(PayTypeEnum payType) {
        if (payType == null) {
            throw new IllegalArgumentException("pay type is empty.");
        }
        if (!payStrategies.containsKey(payType)) {
            throw new IllegalArgumentException("pay type not supported.");
        }
        return payStrategies.get(payType);
    }

    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
        appContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() {
        // 将 Spring 容器中所有的 Payment 接口实现类注册到 payStrategies
        appContext.getBeansOfType(Payment.class)
                  .values()
                  .forEach(payment -> payStrategies.put(payment.getPayType(), payment));
    }
}

注意:以上两种创建方式,都是无状态的,即不包含成员变量,它们可以被共享使用。如果策略类是有状态的,需要根据业务场景每次创建新的策略对象,那么我们可以在工厂方法中,每次生成新的策略对象,而不是使用已经提前缓存好的策略对象。如下代码所示:

public class PaymentFactory {
    public static Payment getPayment(PayTypeEnum payType) {
        if (payType == null) {
            throw new IllegalArgumentException("pay type is empty.");
        }
        if (payType == PayTypeEnum.WX) {
            return new WxPayment();
        }
        if (payType == PayTypeEnum.ALIPAY) {
            return new AlipayPayment();
        }
        if (payType == PayTypeEnum.BANK_CARD) {
            return new BankCardPayment();
        }
        throw new IllegalArgumentException("pay type not supported.");
    }

}

策略的使用

通常我们事先并不知道会使用哪个策略,在程序运行时根据配置、用户输入、计算结果等来决定到底使用哪种策略。例如,前边支付方式的例子,我们会根据用户的选择来决定使用哪种支付方式。使用策略模式的代码实现如下:

Order order = 订单信息
PayResult payResult = PaymentFactory.getPayment(payType).pay(order);
if (payResult == PayResult.SUCCESS) {
    System.out.println("支付成功");
} else if (payType == 支付宝) {
    System.out.println("支付失败");
}

综上代码中,接口类只负责业务策略的定义,每个策略的具体实现单独放在实现类中,工厂类 Factory 只负责获取具体实现类,而具体调用代码则负责业务逻辑的编排。这些实现用到了面向接口而非实现编程,满足了职责单一、开闭原则,从而达到了功能上的高内聚低耦合、提高了可维护性、扩展性以及代码的可读性。

示例二

这里为了方便理解,我们就拿刚学习Java的时候使用计算方法来说吧。

在使用计算器进行计算的时候,会经常用到加减乘除方法。如果我们想得到两个数字相加的和,我们需要用到“+”符号,得到相减的差,需要用到“-”符号等等。虽然我们可以通过字符串比较使用if/else写成通用方法,但是计算的符号每次增加,我们就不得不加在原先的方法中进行增加相应的代码,如果后续计算方法增加、修改或删除,那么会使后续的维护变得困难。

但是在这些方法中,我们发现其基本方法是固定的,这时我们就可以通过策略模式来进行开发,可以有效避免通过if/else来进行判断,即使后续增加其他的计算规则也可灵活进行调整。

首先定义一个抽象策略角色,并拥有一个计算的方法。

interface CalculateStrategy {
   int doOperation(int num1, int num2);
}

然后再定义加减乘除这些具体策略角色并实现方法。

class OperationAdd implements CalculateStrategy {
   @Override
   public int doOperation(int num1, int num2) {
   	return num1 + num2;
   }
}

class OperationSub implements CalculateStrategy {
   @Override
   public int doOperation(int num1, int num2) {
   	return num1 - num2;
   }
}

class OperationMul implements CalculateStrategy {
   @Override
   public int doOperation(int num1, int num2) {
   	return num1 * num2;
   }
}

class OperationDiv implements CalculateStrategy {
   @Override
   public int doOperation(int num1, int num2) {
   	return num1 / num2;
   }
}

最后在定义一个环境角色,提供一个计算的接口供客户端使用。

class  CalculatorContext {
	private CalculateStrategy strategy;

	public CalculatorContext(CalculateStrategy strategy) {
		this.strategy = strategy;
	}

	public int executeStrategy(int num1, int num2) {
		return strategy.doOperation(num1, num2);
	}
}

编写好之后,那么我们来进行测试。

public static void main(String[] args) {
  		   int a=4,b=2;
		  CalculatorContext context = new CalculatorContext(new OperationAdd());    
	      System.out.println("a + b = "+context.executeStrategy(a, b));
	 
	      CalculatorContext context2 = new CalculatorContext(new OperationSub());      
	      System.out.println("a - b = "+context2.executeStrategy(a, b));
	 
	      CalculatorContext context3 = new CalculatorContext(new OperationMul());    
	      System.out.println("a * b = "+context3.executeStrategy(a, b));
	
	      CalculatorContext context4 = new CalculatorContext(new OperationDiv());    
	      System.out.println("a / b = "+context4.executeStrategy(a, b));
}

输出结果

a + b = 6
a - b = 2
a * b = 8
a / b = 2

策略模式优点

扩展性好,可以在不修改对象结构的情况下,为新的算法进行添加新的类进行实现;
灵活性好,可以对算法进行自由切换。

策略模式缺点

使用策略类变多,会增加系统的复杂度,客户端必须知道所有的策略类才能进行调用;

使用场景

如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为;
一个系统需要动态地在几种算法中选择一种;
如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注