设计模式--策略模式
概念
策略模式 是一种 行为设计模式;当在处理一个业务时,有多种处理方式,并且需要在运行时决定使哪一种具体实现时,就会使用 策略模式
特点
策略模式体现了面向对象程序设计中非常重要的两个原则:
- 封装变化的概念
- 编程中使用接口,而不是使用具体的实现类(面向接口编程)
组成
策略模式 由 抽象策略角色、具体策略角色、环境角色 构成
-
抽象策略角色(
PaymentStrategy
):它为所支持的算法声明了 抽象方法,是所有 具体策略角色 的 父类,它可以是 抽象类,可以是 具体类,可以是 接口 -
具体策略角色(
CreditPaymentStrategy
、WechatPaymentStrategy
、AlipayPaymentStrategy
):它实现了在 抽象策略角色 中声明的算法,在运行时,具体策略角色 将覆盖在 环境角色 中定义的 抽象策略类对象,使用一种具体的算法实现某个业务处理 -
环境角色(
PaymentService
):环境角色 是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在 环境角色 中,通常有两种方式来接收所采用的具体的策略:- 方式一:在 环境角色 中维持一个对 抽象策略角色 的引用实例,用该 引用实例 来接收 具体的策略,即在 环境角色 中维护一个 抽象策略角色 类型的 属性,通过 构造器 或者
setter()
方法为该属性进行赋值
// 环境角色 public class PaymentService { // 持有 抽象策略角色 的引用 private PaymentStrategy strategy; // 利用构造方法为 strategy 属性赋值 public PaymentService(PaymentStrategy strategy) { this.strategy = strategy; } // 具体业务方法 public void pay(BigDecimal amount) { strategy.payment(amount); } }
- 方式二:在 环境角色 的 具体业务方法 中,将 抽象策略角色 做为一个参数传递进去,用该参数来接收 具体的策略
// 环境角色 public class PaymentService { // 具体业务方法 public void pay(PaymentStrategy strategy, BigDecimal amount) { strategy.payment(amount); } }
- 方式一:在 环境角色 中维持一个对 抽象策略角色 的引用实例,用该 引用实例 来接收 具体的策略,即在 环境角色 中维护一个 抽象策略角色 类型的 属性,通过 构造器 或者
类图
案例
在支付业务中,有三种付款方式(银行卡、微信、支付宝),程序运行时使用哪种方式由用户选择,根据用户的选择执行不同的逻辑
未使用设计模式
对于上面的场景,如果在没有使用设计模式时,代码一般都是用下面的方式处理:
public class PaymentService {
CreditService creditService;
WeChatService weChatService;
AlipayService alipayService;
public void payment(PaymentType paymentType, BigDecimal amount) {
if (PaymentType.Credit == paymentType) {
creditService.payment(); // 银行卡支付
} else if (PaymentType.WECHAT == paymentType) {
weChatService.payment(); // 微信支付
} else if (PaymentType.ALIPAY == paymentType) {
alipayService.payment(); // 支付宝支付
} else {
throw new NotSupportPaymentException("paymentType not support");
}
}
}
enum PaymentType {
Credit, WECHAT, ALIPAY;
}
这种使用 if...else
的方式虽然能支持现有的业务需求,但是当业务需求发生改变时,比如增加新的支付方式,或者将某一个支付方式下线,则需要对 PaymentService
进行 修改,显然这种设计不符合 开闭原则(对修改关闭,对扩展开放),修改之后需要重新对其他的支付方式进行测试
弊端:
- 不符合 开闭原则
if...else
逻辑复杂,代码结构混乱- 扩展性差
使用策略模式
- 将支付方式这一行为(
payment()
) 抽象为一个 策略接口,代表支付方式的抽象
// 抽象策略角色
public interface PaymentStrategy {
public void payment(BigDecimal amount);
}
- 针对需要支持的三种支付方式建立对应的 策略实现类
// 具体策略角色 - 银行卡支付
public class CreditPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用银行卡支付" + amount);
}
}
// 具体策略角色 - 微信支付
public class WechatPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用微信支付" + amount);
}
}
// 具体策略角色 - 支付宝支付
public class AlipayPaymentStrategy implements PaymentStrategy {
@Override
public void payment(BigDecimal amount) {
System.out.println("使用支付宝支付" + amount);
// 调用支付宝支付API
}
}
- 实现支付服务
PaymentService
// 环境角色
public class PaymentService {
/**
* 将strategy作为参数传递给支付服务
*/
public void pay(PaymentStrategy strategy, BigDecimal amount) {
strategy.payment(amount);
}
}
将 支付策略 作为参数传递给 支付服务,在 支付服务 中只需要按照运行时传的 支付策略对象 进行支付就可以了
- 测试使用 策略模式 之后的代码
public class StrategyTest {
public static void main(String[] args) {
PaymentService paymentService = new PaymentService();
// 使用微信支付
paymentService.pay(new WechatPaymentStrategy(), new BigDecimal("100"));
//使用支付宝支付
paymentService.pay(new AlipayPaymentStrategy(), new BigDecimal("100"));
}
}
在使用了 策略模式 之后,在支付服务 PaymentService
中便不需要写复杂的 if...else
,如果需要新增加一种支付方式,只需要新增一个新的支付策略实现,这样就满足了 开闭原则,并且对其他支付方式的业务逻辑也不会造成影响,扩展性很好
典型应用
在 JDK中,策略模式 最经典的例子是 Collections.sort(List<T> list, Comparator<? super T> c)
方法,该方法接受一个比较器 Compartor
参数,客户端在运行时可以传入一个比较器的实现,sort()
方法根据不同实现,按照不同的方式进行排序
一个学生类,有两个属性 id
和 name
public class Student {
private Integer id;
private String name;
@Override
public String toString() {
return "{id=" + id + ", name='" + name + "'}";
}
}
实现两个比较器,比较器实现了 Comparator
接口,一个升序,一个降序
// 降序
public class DescSortor implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.getId() - o1.getId();
}
}
// 升序
public class AscSortor implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() - o2.getId();
}
}
通过 Collections.sort()
对集合 List
进行排序
public class Client
{
public static void main(String[] args) {
List<Stu> students = Arrays.asList(
new Stu(3, "张三"),
new Stu(1, "李四"),
new Stu(4, "王五"),
new Stu(2, "赵六")
);
toString(students, "排序前");
Collections.sort(students, new AscSortor());
toString(students, "升序后");
Collections.sort(students, new DescSortor());
toString(students, "降序后");
}
public static void toString(List<Stu> students, String desc){
for (Stu student : students) {
System.out.print(desc + ": " + student.toString() + ", ");
}
System.out.println();
}
}
向 Collections.sort()
方法中传入不同的比较器即可实现不同的排序效果(升序或降序)
这里 Comparator
接口充当了 抽象策略角色,两个比较器 DescSortor
和 AscSortor
则充当了 具体策略角色,Collections
则充当了 环境角色
优劣
优点:
- 策略模式提供了对 开闭原则 的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
- 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码
- 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式而是通过继承,这样算法的使用就和算法本身混在一起,不符合 单一职责原则,而且使用继承无法实现算法或行为在程序运行时的 动态切换
- 使用策略模式可以避免多重条件选择语句(
if...else
);多重条件选择语句是硬编码,不易维护 - 策略模式提供了一种算法的 复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况
- 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类
- 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况
适用场景
- 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据 里氏代换原则 和面向对象的 多态性,客户端可以选择使用任何一个 具体算法类,并只需要维持一个数据类型是 抽象算法类的对象
- 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句(
if...else
) - 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性