第四部分:行为型模式 - 策略模式 (Strategy Pattern)
接下来,我们学习策略模式。这个模式定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法独立于使用它的客户而变化。
- 核心思想:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
策略模式 (Strategy Pattern)
“定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。” (Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.)
想象一下你要去某个地方旅行,你有多种出行方式可选:
- 坐飞机 (ConcreteStrategyA):速度快,但价格可能较高。
- 坐火车 (ConcreteStrategyB):性价比高,可以欣赏沿途风景。
- 自驾 (ConcreteStrategyC):灵活自由,但可能比较累。
这些出行方式就是不同的“策略”。你可以根据你的需求(时间、预算、偏好)选择其中一种策略。策略模式就是将这些不同的策略(算法)封装起来,让你可以轻松地切换它们,而不需要改变“去旅行”这个行为本身(Context)。
1. 目的 (Intent)
策略模式的主要目的:
- 封装算法族:将相关的算法封装到独立的策略类中。
- 算法可互换:使得这些算法可以根据需要自由切换。
- 避免多重条件选择:如果一个对象的行为有多种方式,并且这些方式可以用
if/else
或switch
来选择,策略模式可以将这些分支转换为对不同策略对象的调用,从而消除条件语句。 - 使算法独立于客户端:客户端代码不需要知道具体算法的实现细节,只需要知道它需要使用哪个策略。
2. 生活中的例子 (Real-world Analogy)
-
支付方式:
- 策略:信用卡支付、支付宝支付、微信支付、银行转账。
- 上下文:电商平台的订单支付模块。用户在下单时可以选择不同的支付策略。
-
排序算法:
- 策略:冒泡排序、快速排序、归并排序、插入排序。
- 上下文:一个需要对数据集合进行排序的程序。可以根据数据量、数据特性选择不同的排序策略。
-
压缩文件:
- 策略:ZIP压缩、RAR压缩、7z压缩。
- 上下文:文件压缩工具。用户可以选择不同的压缩算法(策略)。
-
导航软件的路线规划:
- 策略:最短时间路线、最少换乘路线、避开高速路线、步行路线。
- 上下文:导航应用。用户选择不同的偏好,应用会采用不同的路线规划策略。
3. 结构 (Structure)
策略模式通常包含以下角色:
-
Context (上下文):
- 维护一个对 Strategy 对象的引用。
- 定义一个或多个供客户端调用的方法,这些方法会将请求委托给其 Strategy 对象。
- 可以提供一个方法来让客户端在运行时设置或更换 Strategy 对象。
-
Strategy (策略接口或抽象类):
- 定义所有支持的算法的公共接口。
- Context 使用这个接口来调用某个 ConcreteStrategy 定义的算法。
-
ConcreteStrategy (具体策略):
- 实现 Strategy 接口。
- 封装具体的算法或行为。
客户端通常会创建并传递一个具体策略对象给上下文。或者,上下文可以有一个默认策略,客户端也可以在运行时更改这个策略。
4. 适用场景 (When to Use)
- 当你有许多相关的类,它们之间的区别仅仅是行为不同时。策略模式可以动态地选择这些行为中的一种。
- 当需要使用一个算法的不同变体时。例如,你可能会定义一些反映不同空间/时间权衡的算法。
- 当算法使用了你不想让客户端知道的数据。策略模式可以避免暴露复杂的、与算法相关的数据结构。
- 当一个类定义了多种行为,并且这些行为以多个条件语句的形式出现。将相关的条件分支移入它们各自的 Strategy 类中以代替这些条件语句。
- 当你希望客户端能够选择不同的算法,而不需要修改客户端代码时。
5. 优缺点 (Pros and Cons)
优点:
- 封装算法族:提供了管理和切换相关算法族的简便方法。
- 避免多重条件语句:消除了
if/else
或switch
等条件判断,使代码更清晰。 - 提高灵活性和可扩展性:可以轻松地增加新的策略(算法),而无需修改 Context 或其他策略,符合开闭原则。
- 策略可以复用:不同的 Context 可以共享相同的策略对象。
- 客户端与具体算法解耦:客户端只需要知道 Strategy 接口,不需要了解具体算法的实现细节。
缺点:
- 类数量增多:每个具体策略都是一个类,可能会导致系统中类的数量增加。
- 客户端必须了解不同的策略:客户端需要知道有哪些可用的策略,并选择合适的策略。这在某些情况下可能不是问题,但有时会增加客户端的复杂性(尽管客户端不需知道策略的实现细节)。
- 上下文与策略之间的通信开销:如果策略需要从上下文中获取大量数据,或者上下文需要频繁地与策略交互,可能会有一定的通信开销。有时策略接口可能需要定义得比较宽泛,以适应所有具体策略的需求。
6. 实现方式 (Implementations)
让我们以一个简单的计算器为例,它可以执行不同的数学运算(加法、减法、乘法)作为不同的策略。
策略接口 (OperationStrategy)
// strategy.go (Strategy interface and concrete strategies)
package strategy
// OperationStrategy 策略接口
type OperationStrategy interface {
DoOperation(num1, num2 int) int
GetName() string // For demonstration
}
// OperationStrategy.java (Strategy interface)
package com.example.strategy;
public interface OperationStrategy {
int doOperation(int num1, int num2);
String getName(); // For demonstration
}
具体策略 (AddOperation, SubtractOperation, MultiplyOperation)
// strategy.go (continued)
package strategy
// --- AddOperation --- (具体策略:加法)
type AddOperation struct{}
func (s *AddOperation) DoOperation(num1, num2 int) int {
return num1 + num2
}
func (s *AddOperation) GetName() string { return "Addition" }
// --- SubtractOperation --- (具体策略:减法)
type SubtractOperation struct{}
func (s *SubtractOperation) DoOperation(num1, num2 int) int {
return num1 - num2
}
func (s *SubtractOperation) GetName() string { return "Subtraction" }
// --- MultiplyOperation --- (具体策略:乘法)
type MultiplyOperation struct{}
func (s *MultiplyOperation) DoOperation(num1, num2 int) int {
return num1 * num2
}
func (s *MultiplyOperation) GetName() string { return "Multiplication" }
// AddOperation.java
package com.example.strategy;
public class AddOperation implements OperationStrategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
@Override
public String getName() {
return "Addition";
}
}
// SubtractOperation.java
package com.example.strategy;
public class SubtractOperation implements OperationStrategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
@Override
public String getName() {
return "Subtraction";
}
}
// MultiplyOperation.java
package com.example.strategy;
public class MultiplyOperation implements OperationStrategy {
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
@Override
public String getName() {
return "Multiplication";
}
}
上下文 (Calculator - Context)
// calculator.go (Context)
package context // Renamed package to avoid conflict
import (
"../strategy"
"fmt"
)
// Calculator 上下文
type Calculator struct {
strategy strategy.OperationStrategy
}
// NewCalculator 创建计算器,可以传入初始策略
func NewCalculator(initialStrategy strategy.OperationStrategy) *Calculator {
fmt.Printf("Calculator created with initial strategy: %s\n", initialStrategy.GetName())
return &Calculator{strategy: initialStrategy}
}
// SetStrategy 允许在运行时更改策略
func (c *Calculator) SetStrategy(s strategy.OperationStrategy) {
fmt.Printf("Calculator: Changing strategy to %s\n", s.GetName())
c.strategy = s
}
// ExecuteStrategy 执行当前策略
func (c *Calculator) ExecuteStrategy(num1, num2 int) int {
fmt.Printf("Calculator: Executing strategy %s with numbers %d and %d\n",
c.strategy.GetName(), num1, num2)
result := c.strategy.DoOperation(num1, num2)
fmt.Printf("Calculator: Result = %d\n", result)
return result
}
// Calculator.java (Context)
package com.example.context;
import com.example.strategy.OperationStrategy;
public class Calculator {
private OperationStrategy strategy;
// Constructor injection
public Calculator(OperationStrategy strategy) {
System.out.println("Calculator created with initial strategy: " + strategy.getName());
this.strategy = strategy;
}
// Setter injection - allows changing strategy at runtime
public void setStrategy(OperationStrategy strategy) {
System.out.println("Calculator: Changing strategy to " + strategy.getName());
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
System.out.printf("Calculator: Executing strategy %s with numbers %d and %d%n",
this.strategy.getName(), num1, num2);
int result = strategy.doOperation(num1, num2);
System.out.printf("Calculator: Result = %d%n", result);
return result;
}
}
客户端使用
// main.go (示例用法)
/*
package main
import (
"./context"
"./strategy"
"fmt"
)
func main() {
add := &strategy.AddOperation{}
subtract := &strategy.SubtractOperation{}
multiply := &strategy.MultiplyOperation{}
// 使用加法策略创建计算器
calculator := context.NewCalculator(add)
calculator.ExecuteStrategy(10, 5) // Output: 15
fmt.Println("\n--- Changing strategy to Subtraction ---")
calculator.SetStrategy(subtract)
calculator.ExecuteStrategy(10, 5) // Output: 5
fmt.Println("\n--- Changing strategy to Multiplication ---")
calculator.SetStrategy(multiply)
calculator.ExecuteStrategy(10, 5) // Output: 50
// 也可以直接创建带特定策略的计算器实例
fmt.Println("\n--- New calculator with Multiplication strategy ---")
calc2 := context.NewCalculator(multiply)
calc2.ExecuteStrategy(7, 8) // Output: 56
}
*/
// Main.java (示例用法)
/*
package com.example;
import com.example.context.Calculator;
import com.example.strategy.AddOperation;
import com.example.strategy.MultiplyOperation;
import com.example.strategy.OperationStrategy;
import com.example.strategy.SubtractOperation;
public class Main {
public static void main(String[] args) {
OperationStrategy add = new AddOperation();
OperationStrategy subtract = new SubtractOperation();
OperationStrategy multiply = new MultiplyOperation();
// Create calculator with Add strategy
Calculator calculator = new Calculator(add);
calculator.executeStrategy(10, 5); // Output: 15
System.out.println("\n--- Changing strategy to Subtraction ---");
calculator.setStrategy(subtract);
calculator.executeStrategy(10, 5); // Output: 5
System.out.println("\n--- Changing strategy to Multiplication ---");
calculator.setStrategy(multiply);
calculator.executeStrategy(10, 5); // Output: 50
// Another calculator instance with Multiply strategy directly
System.out.println("\n--- New calculator with Multiplication strategy ---");
Calculator calc2 = new Calculator(multiply);
calc2.executeStrategy(7, 8); // Output: 56
}
}
*/
7. 策略模式 vs. 状态模式 (Strategy vs. State)
这两个模式在结构上非常相似,但意图不同。在状态模式的文档中我们已经对比过,这里简单回顾:
- 状态模式:关注对象在其内部状态改变时其行为的改变。状态转换通常是内部驱动的。
- 策略模式:关注提供一系列可互换的算法,并由客户端选择使用哪个算法。策略的选择通常是外部驱动的。
8. 总结
策略模式是一种非常实用的行为设计模式,它使得算法的选择和实现与使用算法的客户端代码分离。通过将不同的算法封装在独立的策略类中,我们可以轻松地添加、删除或修改算法,而不会影响到客户端代码或其他算法。这大大提高了系统的灵活性、可维护性和可扩展性,是应对需求变化、实现“开闭原则”的有力工具。