策略模式

策略模式

提出问题

  • 在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。即代码中要根据客户不同的选项进行不同的行为

问题案例

问题案例1

电影票打折方案。Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

  • 学生凭学生证可享受票价8折优惠;
  • 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);
  • 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。

该系统在将来可能还要根据需要引入新的打折方式。为了实现上述电影票打折功能,Sunny软件公司开发人员设计了一个电影票类MovieTicket,其核心代码片段如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//电影票类  
class MovieTicket {
private double price; //电影票价格
private String type; //电影票类型

public void setPrice(double price) {
this.price = price;
}

public void setType(String type) {
this.type = type;
}

public double getPrice() {
return this.calculate();
}

//计算打折之后的票价
public double calculate() {
//学生票折后票价计算
if(this.type.equalsIgnoreCase("student")) {
System.out.println("学生票:");
return this.price * 0.8;
}
//儿童票折后票价计算
else if(this.type.equalsIgnoreCase("children") && this.price >= 20 ) {
System.out.println("儿童票:");
return this.price - 10;
}
//VIP票折后票价计算
else if(this.type.equalsIgnoreCase("vip")) {
System.out.println("VIP票:");
System.out.println("增加积分!");
return this.price * 0.5;
}
else {
return this.price; //如果不满足任何打折要求,则返回原始票价
}
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Client {  
public static void main(String args[]) {
MovieTicket mt = new MovieTicket();
double originalPrice = 60.0; //原始票价
double currentPrice; //折后价

mt.setPrice(originalPrice);
System.out.println("原始价为:" + originalPrice);
System.out.println("---------------------------------");

mt.setType("student"); //学生票
currentPrice = mt.getPrice();
System.out.println("折后价为:" + currentPrice);
System.out.println("---------------------------------");

mt.setType("children"); //儿童票
currentPrice = mt.getPrice();
System.out.println("折后价为:" + currentPrice);
}
}

问题

  • MovieTicket类的calculate()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护。
  • 增加新的打折算法或者对原有打折算法进行修改时必须修改MovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。
  • 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。

应用

适用性

  • 许多相关的类仅仅是行为有异。策略提供了一种用多个行为中的一个行为来配置一个类的方法
  • 需要使用一个算法的不同变体
  • 算法使用客户不应该知道的数据。策略模式避免暴露复杂、与算法相关的数据结构
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,将相关的条件分支移入它们各自的Strategy中

案例

案例1

存在一个种群:鸭子

有些鸭子会飞、有些不会、有些快、有些慢,但都是在飞,那么考虑两种实现方法。

  • 使用继承,在每一个子类进行重写。
  • 创建一个飞行接口,然后交由一组类进行实现该接口。这样就可以使得鸭子实现复用

案例2

有许多的算法可以对一个正文流进行分析,将这些算法硬编码进使用它们的类是不可取的

  • 需要换行功能的客户程序如果直接包含换行算法代码的话会变得很复杂,使得客户程序难以维护,尤其是当需要支持多种换行算法时
  • 不同的时候需要不同的算法
  • 当换行功能是客户程序一个难以分割的成分时,增加新的换行算法或改变现有算法十分困难

案例3

电影票打折方案。Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

  • 学生凭学生证可享受票价8折优惠;
  • 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);
  • 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。

具体应用

  • Java SE 中的每个容器都存在多种布局供用户选择,就用到了策略模式

基础概述

是什么

策略模式:定义了算法族,分别分装起来,让他们可以互相替换,此模式使得算法的变化独立于使用算法的客户。使得算法可以在不影响客户端的情况下发生变化,从而改变不同的功能。

策略模式定义了算法族,分别分装起来,让他们可以互相替换,此模式使得算法的变化独立于使用算法的客户

策略模式体现了两个原则

  • 封装变化的概念。(将飞行动作的变化抽离出)
  • 编程中使用接口,而不是使用的是具体的实现类(面向接口编程)。

分类

协作

结构

1563332433137

参与者

  • Strategy:策略
    • 定义所有支持的算法的公共接口,Context使用这个接口来调用某ConcreteStrategy定义的算法
  • ConcreteStrategy:具体策略
    • 以Startegy接口实现某具体算法
  • Context:上下文
    • 是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。
    • 维护一个对Strategy对象的引用,用一个ConcreteStrategy对象来配置
    • 可以定义一个接口来让Stategy访问它的数据

协作

  • 类关系
    • Strategy定义一个公共接口,表示策略的意图
    • ConcreteStrategy继承自Strategy,是实现具体的策略
    • Context持有一个Strategy的引用,环境角色使用这个接口调用不同的算法,最终给客户端使用
  • 逻辑关系
    • Strategy与Context相互作用以实现选定的算法,当算法被调用时,Context可以将该算法所需要的所有数据都传递给该Strategy。或者Context将自身作为一个参数传递给Strategy,让Strategy在需要时可以毁掉Context
    • Context将它的客户请求转发给它的Strategy。客户通常创建并传递一个ConcreteStrategy对象给该Context,这样客户仅仅与Context交互。通常有一系列的ConcreteStrategy可供客户选择。

权衡

分类

结构

1563332433137

效果(优缺)

优点

  • 简化了单元测试,每个算法都有自己的类,可以通过自己的接口单独测试

  • 相关算法系列。Strategy类层次为Context定义了一系列的可供重用的算法或行为,继承有助于提取出这些算法中的公共功能

  • 一个替代继承的方法。

    • 可以直接生成一个Context类的子类,从而给它以不同的行为,但会将行为硬编码到Context,将算法的实现与Context耦合到一起,使得难以理解、维护、扩展。并且他们还不能动态改变算法。因此得到一堆相关的类,唯一差别仅仅是算法不同
    • 将算法封装在独立的Strategy类中,使得你可以独立于Context改变,并易于切换、理解、扩展
  • 消除了一些条件语句。提供了用条件语句选择所需的行为以外的另一种选择,当不同的行为堆砌在一个类中很难避免使用条件语句选择何时的行为,将行为封装在一个个独立的Strategy中消除了这些语句。

  • 实现的选择。提供相同行为的不同实现,客户可以根据不同时间/空间的权衡从不同策略中进行选择

缺陷

  • 策略模式把每一种具体的策略都封装成一个实现类,如果策略有很多的话,很显然是实现类就会导致过多,显得臃肿
    • 如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
  • 客户必须了解不同的Strategy。因为客户要选择一个合适的Strategy就必须知道他们有什么不同,因此仅仅当这些不同行为变体与客户相关的行为才需要使用
  • Strategy和Context之间的通信开销。某些ConcreteStrategy可能永不到所有Strategy传递过来的参数。

实现

实现步骤

  • 定义Strategy、Context接口,它们必须是的ConcreteStrategy能够有效的访问它所需要的Context中的任何数据,反之亦然
    • 让Context将数据放在参数中传递给Strategy操作,即将数据发送过去,使得解耦。但是可能发送一些Strategy不需要的数据
    • 将Context作为一个参数传递过去,Strategy显式向其请求数据,或存储它的一个引用。
  • Context接口内部持有一个策略类的引用
  • 编写具体策略角色(实际上就是实现上面定义的公共接口)
  • 考虑使得Strategy对象成为可选的,如果即使在不使用额外的Strategy对象的情况下,Context依然有意义,则可以简化为在访问Strategy前先检查是否存在,如果存在则使用,如果不存在则执行缺省的行为

案例1

Context父类Duck

可以看到在父类当中,我们定义了两个策略的接口,但是没有任何的引用对象。
同样也定义了一个方法,但是没有具体的内容,在子类中,需要对它进行@Override重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @Author : Hyper
* @Time : 2018/10/8 19:17
*/
public abstract class Duck {
/**
* 声明两个行为为接口类型
* 每一个鸭子都会引用实现该接口的对象
*/
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;

/**
* 这些行为不是由Duck本身进行实现的,而是转交给了对应的接口引用的对象
*/
public void performQuack() {
quackBehavior.quack();
}

public void performFly() {
flyBehavior.fly();
}

/**
* 动态地设定鸭子的行为
*
* @param fb
*/
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb;
}

public void setQuackBehavior(QuackBehavior qb) {
this.quackBehavior = qb;
}

/**
* 较差的方法:呱呱叫
* 需要通过继承@Override重写
*/
public void quack() {
}

/**
* 游泳
*/
public void swim() {
}

}

定义策略Strategy

首先,为了多态,先定义了一个飞行行为的接口,可以看到,在该接口当中,我们定义了一个fly()的方法。

1
2
3
4
5
6
/**
* 抽离飞行的动作,制作接口
*/
public interface FlyBehavior {
void fly();
}

接下来,对该接口进行实现,即具体的策略实现

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 实现飞行的动作
*
* @Author : Hyper
* @Time : 2018/10/8 19:41
*/
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("FlyWithWings.class");
}
}

另一个实现

1
2
3
4
5
6
7
8
9
/**
* 火箭动力
*/
public class FlyRocketPowered implements FlyBehavior {
@Override
public void fly() {
System.out.println("FlyRocketPowered.class");
}
}

这样一来,我们还可以看到所有的飞行行为,同时如果有需求的变化,我们可以在一个实现里面修改

Context的具体子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 整合接口的实例变量到具体的Duck中
*/
public class MarrardDuck extends Duck {

/**
* 在实例化的时候更改接口的对象,使得所有的对象都是这样的一个引用对象
* 在该类使用飞行的时候,就会调用FlyWithWings的对象
* 因为它引用的对象是FlyWithWings的实体
*/
public MarrardDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
}

在该子类当中,我们将超类的接口,引用到了一个实例。

测试

做一个简单的测试

1
2
3
4
5
6
7
8
9
10
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
Duck duck = new MarrardDuck();
duck.performFly();
duck.performQuack();
}
}

输出结果为:

1
2
FlyWithWings.class
Quack.class

即接口引用到了该子类真正的飞行方法。

动态更改

在父类当中,设置了一个方法setFlyBehavior(FlyBehavior fb),该方法实现动态化

应用到子类当中

1
2
3
4
5
6
public class ModuleDuck extends Duck {
public ModuleDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
}

进行一次测试

1
2
3
4
5
6
7
8
9
10
11
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck duck = new ModuleDuck();
duck.performQuack();
duck.performFly();
//升级这鸭子,让他来点花样飞行
duck.setFlyBehavior(new FlyRocketPowered());
//它变强了
duck.performFly();
}
}

输出结果为:

1
2
3
Quack.class
FlyWithWings.class
FlyRocketPowered.class

即方法动态的更改

案例3

设计Context类,持有一个对抽象算法的引用,并且可以在运行期进行改变算法。在执行getPrice时,即将计算方法教给具体的算法去执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MovieTicketContext {
private double price;
private DiscountStrategy discount; //维持一个对抽象折扣类的引用

public void setPrice(double price) {
this.price = price;
}

//注入一个折扣类对象
public void setDiscount(DiscountStrategy discount) {
this.discount = discount;
}

public double getPrice() {
//调用折扣类的折扣价计算方法
return discount.calculate(this.price);
}
}

设计抽象算法的接口,拥有一个方法即计算价格

1
2
3
public interface DiscountStrategy {
public double calculate(double price);
}

设计具体的实现类,通过实现接口,确定该子算法的具体算法实现方式

1
2
3
4
5
6
7
public class StudentDiscountStrategy implements DiscountStrategy {
@Override
public double calculate(double price) {
System.out.println("学生票:");
return price * 0.8;
}
}

测试得:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class example_2_test {
public static void main(String[] args) {
MovieTicketContext context = new MovieTicketContext();
DiscountStrategy strategy = new StudentDiscountStrategy();
context.setDiscount(strategy);
context.setPrice(20);
System.out.println(context.getPrice());
//运行期切换算法
strategy = new ChildrenDiscountStrategy();
context.setDiscount(strategy);
System.out.println(context.getPrice());
}
}

相关模式

Flyweight:Strategy对象经常是很好的轻量级对象

进阶

策略工厂模式

在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度,其结构图如图 5 所示。

策略工厂模式的结构图
图5 策略工厂模式的结构图

Lambda重构模式

即将原有的写法:传入一个新的策略类转换为传入一个Lambda表达式。即将策略类的具体实现转由一个Lambda实现

1
2
3
4
5
//原有写法
Context context = new Context(new Strategy1());
//改变为:
Conext context = new Context((String s) -> s.matches("[a-z]+"));
context.validate();

反省总结

参考