策略模式

设计模式笔记:策略模式 (Strategy Pattern)

一、一句话概括

定义一系列算法,将每一个算法封装起来,并使它们可以互相替换。此模式让算法的变化独立于使用算法的客户。

二、为什么需要它 (Why - The Pain Point)

想象一下,你正在开发一个鸭子模拟游戏。你有各种各样的鸭子,比如 MallardDuck(绿头鸭)、RedheadDuck(红头鸭)等。所有的鸭子都会游泳(swim())和呱呱叫(quack()),所以你可能会设计一个 Duck 基类。

1
2
3
4
5
abstract class Duck {
public void quack() { System.out.println("Quack quack!"); }
public void swim() { System.out.println("Swimming..."); }
public abstract void display();
}

现在,需求变更,鸭子需要会飞(fly())。一个很自然的想法是在 Duck 基类中添加一个 fly() 方法。

1
2
3
4
5
abstract class Duck {
// ...
public void fly() { System.out.println("I'm flying!"); }
// ...
}

痛点:继承带来的僵化

这个设计很快就暴露出问题:

  1. 并非所有子类都适用:我们现在想加入一种 RubberDuck(橡皮鸭)。橡皮鸭继承了 Duck 类,因此它也继承了 fly() 方法。但橡皮鸭是不会飞的!我们必须在 RubberDuck 中重写 fly() 方法,让它什么都不做,这很奇怪。
  2. 代码重复:如果未来有多种“不会飞的鸭子”(比如 DecoyDuck 诱饵鸭),它们都需要重写 fly() 方法。
  3. 难以动态改变行为:如果一只鸭子在游戏中受伤了,它的飞行行为可能会从“展翅高飞”变为“一瘸一拐地飞”。使用继承很难在运行时改变对象的具体行为。

核心问题:如何处理一个类中易于变化的部分(比如飞行行为、叫声行为),使它能够灵活地适应不同的情况,同时又避免因为继承而带来的种种问题?

三、它是什么 (What - The Solution)

策略模式的核心思想是:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

它提倡 “多用组合,少用继承”。与其让 Duck 类通过继承来获得飞行和呱呱叫的行为,不如将这些行为封装成独立的对象(策略对象),然后让 Duck持有(组合) 这些行为对象的引用。

核心思想和角色:

  1. **策略接口 (Strategy)**:
    • 这是一个接口,为所有支持的算法定义了一个公共的操作。
    • 例如,FlyBehavior 接口,包含一个 fly() 方法。QuackBehavior 接口,包含一个 quack() 方法。
  2. **具体策略 (Concrete Strategy)**:
    • 实现了策略接口的具体类,封装了具体的算法或行为。
    • 例如,FlyWithWingsFlyNoWay(不会飞的策略)、Squeak(吱吱叫的策略)。
  3. **上下文 (Context)**:
    • 即使用策略的对象,例如 Duck 类。
    • 内部持有一个策略接口的引用
    • 它不实现具体的算法,而是将行为委托给它所持有的策略对象。
    • 它通常提供一个 setter 方法,允许客户端在运行时动态地更换策略对象。

工作流程:
MallardDuck 在构造时,会被设置一个 FlyWithWings 的实例和一个 Quack(正常叫)的实例。当 mallard.performFly() 被调用时,Duck 类内部会调用 flyBehavior.fly(),从而执行了 FlyWithWings 的逻辑。

四、怎么做 (How - The Blueprint)

基本步骤(以鸭子模拟器为例):

  1. 识别变化的行为:飞行行为和叫声行为是变化的。
  2. 为每个变化的行为定义一个策略接口:创建 FlyBehaviorQuackBehavior 接口。
  3. 为每个具体的行为创建具体策略类FlyWithWings, FlyNoWay, Quack, Squeak, MuteQuack
  4. **修改上下文类 Duck**:
    • Duck 类中添加两个实例变量,类型为 FlyBehaviorQuackBehavior
    • 移除 Duck 基类中的 fly()quack() 方法。
    • 创建两个新的方法 performFly()performQuack(),这两个方法分别委托给 flyBehaviorquackBehavior 去执行。
    • 提供 setFlyBehavior()setQuackBehavior() 方法,用于动态改变行为。
  5. 在具体的鸭子子类(如 MallardDuck)的构造函数中,初始化其具体的行为策略。

Java 示例代码

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// --- 1 & 2. 策略接口和具体策略 ---
interface FlyBehavior {
void fly();
}

class FlyWithWings implements FlyBehavior {
public void fly() { System.out.println("I'm flying with wings!"); }
}

class FlyNoWay implements FlyBehavior {
public void fly() { System.out.println("I can't fly."); }
}

interface QuackBehavior {
void quack();
}

class Quack implements QuackBehavior {
public void quack() { System.out.println("Quack quack!"); }
}

class Squeak implements QuackBehavior {
public void quack() { System.out.println("Squeak squeak!"); }
}

// --- 3. 上下文 (Context) ---
abstract class Duck {
// 组合行为接口,而不是继承具体行为
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;

public Duck() {}

public abstract void display();

public void performFly() {
flyBehavior.fly(); // 委托给行为类
}

public void performQuack() {
quackBehavior.quack(); // 委托给行为类
}

// 允许动态改变行为
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb;
}

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

public void swim() {
System.out.println("All ducks float, even decoys!");
}
}

// --- 4. 具体的上下文子类 ---
class MallardDuck extends Duck {
public MallardDuck() {
// 在构造时设置具体的策略
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}

public void display() {
System.out.println("I'm a real Mallard duck");
}
}

class RubberDuck extends Duck {
public RubberDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Squeak();
}

public void display() {
System.out.println("I'm a rubber duckie");
}
}

// 客户端
public class StrategyPatternDemo {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.display();
mallard.performFly();
mallard.performQuack();

System.out.println("--------------------");

Duck rubberDuck = new RubberDuck();
rubberDuck.display();
rubberDuck.performFly();
rubberDuck.performQuack();

System.out.println("--------------------");

// 动态改变行为
System.out.println("The rubber duck got a rocket!");
rubberDuck.setFlyBehavior(new FlyWithWings()); // 在运行时更换策略
rubberDuck.performFly();
}
}

五、UML 类图

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
@startuml
skinparam classAttributeIconSize 0
hide empty members

' Context
abstract class Duck {
# FlyBehavior flyBehavior
# QuackBehavior quackBehavior
+ void performFly()
+ void performQuack()
+ void setFlyBehavior(FlyBehavior fb)
}

' Strategy Interfaces
interface FlyBehavior {
+ void fly()
}
interface QuackBehavior {
+ void quack()
}

' Concrete Strategies
class FlyWithWings implements FlyBehavior
class FlyNoWay implements FlyBehavior
class Quack implements QuackBehavior
class Squeak implements QuackBehavior

' Concrete Contexts
class MallardDuck extends Duck
class RubberDuck extends Duck

' Relationships
' Context HAS-A Strategy
Duck o-- "1" FlyBehavior
Duck o-- "1" QuackBehavior

@enduml

六、优缺点分析

优点 (Pros):

  1. 完美解耦:将算法(策略)的实现与使用它的客户(上下文)完全分离开来。
  2. 易于扩展:增加一个新的策略非常简单,只需添加一个实现了策略接口的新类即可,完全符合开闭原则。
  3. 避免条件语句:消除了在上下文中使用大量 if-elseswitch 来选择算法的需要。
  4. 提供多种算法选择:客户端可以根据需要,在运行时自由切换不同的算法。
  5. 组合优于继承:是“组合优于继承”设计原则的最佳实践之一,避免了继承带来的僵化问题。

缺点 (Cons):

  1. 客户端必须了解所有策略:客户端需要知道有哪些不同的策略类,以便在适当时机进行选择和切换。这在一定程度上增加了客户端的复杂性。
  2. 类的数量增多:每个策略都是一个独立的类,会导致系统中类的数量增加。
    • 优化:在 Java 8+ 中,如果策略接口是函数式接口(只有一个抽象方法),可以直接使用 Lambda 表达式方法引用作为具体策略,极大地减少了创建大量具体策略类的需要。

七、在 Java 中的应用

策略模式在 Java 开发中非常普遍。

  • java.util.Comparator: 这是最经典的例子。
    • Collections.sort(list, comparator)list.sort(comparator) 方法。
    • Context: CollectionsList
    • Strategy: Comparator 接口。
    • Concrete Strategy: 你传入的各种 Comparator 实现(通常是匿名内部类或 Lambda 表达式)。sort 方法使用你提供的比较策略来对集合进行排序。
  • Java Swing 的布局管理器:
    • Context: JPanelJFrame
    • Strategy: LayoutManager 接口。
    • Concrete Strategy: BorderLayout, FlowLayout, GridLayout 等。你可以通过 panel.setLayoutManager(...) 动态地改变容器的布局策略。
  • 线程池的拒绝策略:
    • java.util.concurrent.ThreadPoolExecutor 的构造函数可以接收一个 RejectedExecutionHandler
    • Context: ThreadPoolExecutor
    • Strategy: RejectedExecutionHandler 接口。
    • Concrete Strategy: AbortPolicy (抛异常), CallerRunsPolicy (调用者自己执行), DiscardPolicy (直接丢弃) 等。
      dg