策略模式 Strategy Pattern
1. 课程主线
本章通过一个 SimUDuck 鸭子模拟系统引出策略模式。
最开始系统中有一个父类 Duck,不同鸭子类继承它。后来需求增加:鸭子不仅要会叫,还要会飞。直接把 fly() 方法加到 Duck 父类中,会导致所有子类都自动拥有飞行能力。
问题是:
- 普通鸭子可以飞、可以嘎嘎叫。
- 橡皮鸭不会飞,也不会嘎嘎叫,只会吱吱叫。
- 木头诱饵鸭不会飞,也不会叫。
如果把 fly()、quack() 都放在父类里,再让特殊鸭子去重写这些方法,会导致大量重复修改。随着鸭子种类越来越多,维护会变得很麻烦。
这正好引出策略模式要解决的问题:
当系统中某些行为经常变化时,不要把变化行为硬编码在类中,而应该把变化的部分封装起来,让它们可以独立变化。
2. 继承方案的问题 Inheritance Problem
最初设计可能是:
class Duck {
public void quack() {}
public void swim() {}
public void display() {}
public void fly() {}
}
class MallardDuck extends Duck {}
class RedheadDuck extends Duck {}
class RubberDuck extends Duck {}
如果 Duck 父类中加入 fly(),那么所有鸭子都会继承飞行能力。
这会出现错误:
Rubber duckies flying
橡皮鸭竟然会飞
虽然可以在 RubberDuck 中重写 fly():
class RubberDuck extends Duck {
@Override
public void fly() {
// do nothing
}
}
但如果以后又增加很多不会飞的鸭子,比如木头鸭,就要不断重写 fly()。这说明继承方案不够灵活。
继承的问题:
- 父类变化会影响所有子类。
- 子类可能继承到不应该拥有的行为。
- 特殊子类需要不断重写父类方法。
- 新增类型越多,维护成本越高。
3. 接口方案的问题 Interface Problem
另一种想法是把变化行为做成接口:
interface Flyable {
void fly();
}
interface Quackable {
void quack();
}
会飞的鸭子实现 Flyable,会叫的鸭子实现 Quackable。
这样可以避免不会飞的鸭子继承 fly(),但又出现新问题:
如果有 48 个会飞的鸭子类,每个类都要自己实现类似的
fly()代码,重复会非常严重。
接口只规定“必须有什么方法”,但不提供统一的行为复用。于是代码可能大量重复,后续修改飞行行为时,也需要改很多类。
所以 PPT 中说:
No. Duplicate code.
不行,会产生重复代码。
4. 软件中的变化 Change in Software Development
PPT 强调:
Change: The one constant in software development.
变化是软件开发中唯一不变的东西。
代码变化的原因可能包括:
- 用户想要新功能。
- 公司更换数据库厂商。
- 数据格式发生变化。
- 技术协议升级。
- 开发过程中发现之前设计不够好。
因此,好的设计要能应对变化,而不是害怕变化。
5. 设计原则一:封装变化 Encapsulate What Varies
第一个重要原则:
Encapsulate what varies.
封装变化。
完整理解:
找出应用中会变化的部分,把它们和不变的部分分离;把变化的部分封装起来,使得以后修改或扩展变化部分时,不会影响不变的部分。
在鸭子案例中:
不变的部分:
Duck 仍然是鸭子的抽象,鸭子都会 swim(),都会 display()
变化的部分:
fly() 飞行行为
quack() 叫声行为
所以应该把 fly() 和 quack() 从 Duck 类中拿出去,单独设计成一组行为类。
6. 设计原则二:面向接口编程 Program to an Interface
第二个重要原则:
Program to an interface, not an implementation.
面向接口编程,而不是面向实现编程。
这里的 interface 不一定专指 Java 的 interface 关键字,也可以指抽象类或父类型。
更准确地说:
变量声明的类型应该是父类型、抽象类或接口,而不是具体实现类。这样变量可以引用任何具体实现对象,使用者不需要知道真实对象的具体类型。
例如不推荐:
FlyWithWings flyBehavior = new FlyWithWings();
更推荐:
FlyBehavior flyBehavior = new FlyWithWings();
这样以后可以替换为:
flyBehavior = new FlyNoWay();
flyBehavior = new FlyRocketPowered();
使用者只依赖 FlyBehavior 这个抽象,而不依赖具体飞行类。
7. 把行为抽成独立类
飞行行为可以设计成:
interface FlyBehavior {
void fly();
}
class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying!");
}
}
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");
}
}
class Squeak implements QuackBehavior {
public void quack() {
System.out.println("Squeak");
}
}
class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<< Silence >>");
}
}
这样每一种行为都成为一个独立类。类的职责更加单一,也方便复用。
例如:
FlyWithWings表示用翅膀飞。FlyNoWay表示不会飞。FlyRocketPowered表示火箭动力飞行。Quack表示嘎嘎叫。Squeak表示吱吱叫。MuteQuack表示不会叫。
8. Duck 通过组合使用行为 Composition and Delegation
新的 Duck 不再自己实现 fly() 和 quack(),而是持有两个行为对象:
abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("All ducks float");
}
public abstract void display();
}
重点是:
Duck 不亲自飞,也不亲自叫。
Duck 把飞行和叫声委派给行为对象。
这就是委派:
Duck.performFly()
↓
flyBehavior.fly()
Duck.performQuack()
↓
quackBehavior.quack()
9. 具体鸭子设置具体行为
例如绿头鸭:
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 duck");
}
}
木头诱饵鸭:
class DecoyDuck extends Duck {
public DecoyDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new MuteQuack();
}
public void display() {
System.out.println("I'm a duck decoy");
}
}
这样特殊行为不需要靠重写父类方法,而是通过组合不同的行为对象来实现。
10. 运行时动态改变行为 Dynamic Behavior Change
策略模式的一个重要优点是:可以在运行时改变对象行为。
在 Duck 中加入 setter:
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
然后可以这样使用:
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
意思是:
模型鸭一开始不会飞。
后来给它装上火箭动力飞行行为。
于是它运行时变得可以飞。
这体现了对象模式的动态性:对象之间的组合关系可以在运行时改变。
11. HAS-A 优于 IS-A Composition over Inheritance
PPT 中强调:
HAS-A can be better than IS-A.
Favor composition over inheritance.
中文意思是:
“有一个”有时比“是一个”更好。
优先使用组合,而不是继承。
在鸭子案例中:
Duck has a FlyBehavior
Duck has a QuackBehavior
而不是让所有具体鸭子通过继承获得固定的 fly() 和 quack()。
组合的好处:
- 可以把一组算法或行为封装成独立类。
- 行为可以复用给其他对象。
- 可以运行时替换行为。
- 新增行为时,不需要修改已有鸭子类。
- 避免继承带来的强耦合。
12. 策略模式定义 Strategy Pattern Definition
PPT 中给出的定义:
The Strategy Pattern defines a family of algorithms,
encapsulates each one, and makes them interchangeable.
Strategy lets the algorithm vary independently from clients that use it.
中文:
策略模式定义一系列算法,把每一个算法封装起来,并使它们可以相互替换。策略模式让算法可以独立于使用它的客户而变化。
关键词:
family of algorithms
一组算法 / 一族行为
encapsulates each one
分别封装每一个算法
interchangeable
可以互相替换
vary independently
可以独立变化
策略模式也叫:
Policy
政策模式 / 策略模式
13. 策略模式的角色 Participants
策略模式通常包含三个角色:
Context:上下文类
使用策略的类。
在鸭子例子中:
Duck 是 Context
它持有策略对象:
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
Strategy:抽象策略
定义策略接口。
在鸭子例子中:
FlyBehavior
QuackBehavior
ConcreteStrategy:具体策略
实现具体算法或行为。
在鸭子例子中:
FlyWithWings
FlyNoWay
FlyRocketPowered
Quack
Squeak
MuteQuack
14. 策略模式结构 Structure
可以用文字表示为:
Duck
├── has a FlyBehavior
│ ├── FlyWithWings
│ ├── FlyNoWay
│ └── FlyRocketPowered
│
└── has a QuackBehavior
├── Quack
├── Squeak
└── MuteQuack
通用结构:
Context
└── has a Strategy
├── ConcreteStrategyA
├── ConcreteStrategyB
└── ConcreteStrategyC
调用流程:
Client 创建具体策略
↓
把具体策略设置给 Context
↓
Context 调用 Strategy 接口
↓
实际执行 ConcreteStrategy 的算法
15. 策略模式解决的典型问题 Typical Problem
PPT 中给了一个文本换行算法的例子:
public class Context {
public void algorithm(String type) {
if (type == "strategyA") {
...
} else if (type == "strategyB") {
...
} else if (type == "strategyC") {
...
}
}
}
这种写法的问题是:
- 一个类中塞了很多算法分支。
- 新增算法时要修改原来的条件判断。
- 算法代码和上下文类耦合在一起。
- 条件分支越来越多,可读性变差。
策略模式的改法是:
把每个算法分支拆成一个策略类。
Context 不再用 if-else 判断算法细节。
Context 只调用统一的 Strategy 接口。
16. 适用场景 Applicability
适合使用策略模式的情况:
- 许多相关类只是行为不同。
例如不同鸭子的飞行行为和叫声行为不同。
- 需要一个算法的不同变体。
例如不同排序算法、不同压缩算法、不同加密算法、不同支付方式。
- 算法使用的数据或实现细节不希望暴露给客户端。
可以把复杂算法封装到具体策略类里。
- 一个类中有大量条件语句来选择不同行为。
例如:
if (type == "A") {
...
} else if (type == "B") {
...
} else if (type == "C") {
...
}
这种情况可以把每个分支封装成一个策略类。
17. 策略模式的优点 Consequences / Benefits
- 封装一组相关算法。
具体策略类形成一族算法,便于复用和管理。
- 是继承的一种替代方案。
不用通过继承不断重写行为,而是通过组合策略对象获得行为。
- 可以消除大量条件语句。
原来写在 if-else 或 switch 中的行为,可以移动到不同策略类中。
- 运行时可以切换算法。
同一个 Context 可以在不同时间使用不同策略。
- 符合开闭原则。
新增策略类时,通常不需要修改原有 Context。
18. 策略模式的缺点 Liabilities / Drawbacks
- 客户端必须知道不同策略的区别。
客户端要负责选择使用哪个策略,因此需要了解各策略的适用场景。
- 策略类数量会增加。
每种算法都封装成一个类,会导致系统中类的数量变多。
- Context 和 Strategy 之间可能有通信开销。
Context 调用 Strategy 时,可能需要传递数据。如果策略需要很多上下文信息,接口设计会变复杂。
19. 和几个设计原则的关系
封装变化
策略模式把变化的算法或行为封装到独立策略类中。
面向接口编程
Context 依赖策略接口,而不是具体策略类。
private FlyBehavior flyBehavior;
而不是:
private FlyWithWings flyBehavior;
优先组合而不是继承
Duck 通过组合获得行为:
Duck has a FlyBehavior
Duck has a QuackBehavior
而不是把所有行为都放到父类里。
开闭原则
新增一种飞行方式,只需要新增策略类:
class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("I'm flying with a rocket!");
}
}
不需要修改已有的 Duck 类和已有的飞行策略类。
20. 一个通用例子:支付策略
抽象策略:
interface PaymentStrategy {
void pay(double amount);
}
具体策略:
class AlipayStrategy implements PaymentStrategy {
public void pay(double amount) {
System.out.println("使用支付宝支付:" + amount);
}
}
class WeChatPayStrategy implements PaymentStrategy {
public void pay(double amount) {
System.out.println("使用微信支付:" + amount);
}
}
class CreditCardStrategy implements PaymentStrategy {
public void pay(double amount) {
System.out.println("使用信用卡支付:" + amount);
}
}
上下文类:
class Order {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void pay(double amount) {
paymentStrategy.pay(amount);
}
}
客户端:
Order order = new Order();
order.setPaymentStrategy(new AlipayStrategy());
order.pay(100);
order.setPaymentStrategy(new WeChatPayStrategy());
order.pay(200);
这里:
Order = Context
PaymentStrategy = Strategy
AlipayStrategy / WeChatPayStrategy / CreditCardStrategy = ConcreteStrategy
21. 共享模式词汇 Shared Vocabulary
PPT 后半部分强调:设计模式提供了开发者之间的共享词汇。
当你说“这里可以用策略模式”时,别人不仅知道一个名字,还能理解背后的设计含义:
- 有一组可替换算法。
- 算法被封装到独立类中。
- 上下文类依赖抽象策略。
- 可以通过组合在运行时切换行为。
- 目的是增强可维护性、可扩展性和复用性。
模式的作用不是直接给代码,而是给通用解决方案。
22. 复习重点
需要重点记住:
策略模式定义:
定义一系列算法,把每一个算法封装起来,并使它们可以相互替换。
策略模式让算法可以独立于使用它的客户而变化。
三大原则:
1. 封装变化
2. 面向接口编程,而不是面向实现编程
3. 优先使用组合,而不是继承
三个角色:
1. Context 上下文类
2. Strategy 抽象策略
3. ConcreteStrategy 具体策略
适用场景:
1. 多个类只有行为不同
2. 一个算法有多个变体
3. 想隐藏算法细节
4. 类中有大量 if-else / switch 判断
优点:
1. 算法可以独立变化
2. 可以替代继承
3. 可以消除条件语句
4. 可以运行时切换行为
5. 扩展方便
缺点:
1. 客户端需要了解不同策略
2. 策略类数量增加
3. Context 和 Strategy 之间可能有通信开销
23. 一句话总结
策略模式就是:
把容易变化的算法或行为单独封装成策略类,让使用者通过组合持有策略对象,从而可以灵活替换行为,而不用修改原来的核心类。
鸭子案例中就是:
不要让 Duck 父类固定写死 fly() 和 quack()。
把飞行和叫声分别封装成 FlyBehavior 和 QuackBehavior。
不同鸭子组合不同的行为对象。
需要变化时,替换行为对象即可。
高频英文术语 Glossary
| English Term | 中文 | 备注 |
|---|---|---|
| Strategy Pattern | 策略模式 | 封装一族算法并使其可替换 |
| Context | 上下文类 | 使用策略的类,如 Duck |
| Strategy | 抽象策略 | 定义算法/行为接口 |
| ConcreteStrategy | 具体策略 | 具体算法或行为实现 |
| SimUDuck | 鸭子模拟系统 | PPT 示例系统 |
| Inheritance | 继承 | 父类行为被子类继承 |
| Override | 重写 | 子类覆盖父类方法 |
| Interface | 接口 | 也可泛指抽象类型 / supertype |
| Supertype | 超类型 | 父类、抽象类或接口 |
| Subtype | 子类型 | 子类或接口实现类 |
| Polymorphism | 多态 | 同一抽象引用指向不同具体对象 |
| Encapsulate What Varies | 封装变化 | 找出变化部分并独立封装 |
| Program to an Interface | 面向接口编程 | 依赖抽象,不依赖具体实现 |
| Favor Composition over Inheritance | 优先组合而非继承 | HAS-A can be better than IS-A |
| Composition | 组合 | 对象持有另一个对象 |
| Delegation | 委派 | 把行为交给成员对象执行 |
| Interchangeable | 可互换 | 策略之间可替换 |
| Runtime | 运行时 | 程序运行期间 |
| Dynamic Behavior Change | 动态改变行为 | 运行时替换策略对象 |
| Duplicate Code | 重复代码 | 接口方案容易产生的问题 |