策略模式 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

适合使用策略模式的情况:

  1. 许多相关类只是行为不同。

例如不同鸭子的飞行行为和叫声行为不同。

  1. 需要一个算法的不同变体。

例如不同排序算法、不同压缩算法、不同加密算法、不同支付方式。

  1. 算法使用的数据或实现细节不希望暴露给客户端。

可以把复杂算法封装到具体策略类里。

  1. 一个类中有大量条件语句来选择不同行为。

例如:

if (type == "A") {
    ...
} else if (type == "B") {
    ...
} else if (type == "C") {
    ...
}

这种情况可以把每个分支封装成一个策略类。

17. 策略模式的优点 Consequences / Benefits

  1. 封装一组相关算法。

具体策略类形成一族算法,便于复用和管理。

  1. 是继承的一种替代方案。

不用通过继承不断重写行为,而是通过组合策略对象获得行为。

  1. 可以消除大量条件语句。

原来写在 if-elseswitch 中的行为,可以移动到不同策略类中。

  1. 运行时可以切换算法。

同一个 Context 可以在不同时间使用不同策略。

  1. 符合开闭原则。

新增策略类时,通常不需要修改原有 Context。

18. 策略模式的缺点 Liabilities / Drawbacks

  1. 客户端必须知道不同策略的区别。

客户端要负责选择使用哪个策略,因此需要了解各策略的适用场景。

  1. 策略类数量会增加。

每种算法都封装成一个类,会导致系统中类的数量变多。

  1. 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 重复代码 接口方案容易产生的问题