零基础设计模式——行为型模式 - 策略模式

第四部分:行为型模式 - 策略模式 (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)

策略模式的主要目的:

  1. 封装算法族:将相关的算法封装到独立的策略类中。
  2. 算法可互换:使得这些算法可以根据需要自由切换。
  3. 避免多重条件选择:如果一个对象的行为有多种方式,并且这些方式可以用 if/elseswitch 来选择,策略模式可以将这些分支转换为对不同策略对象的调用,从而消除条件语句。
  4. 使算法独立于客户端:客户端代码不需要知道具体算法的实现细节,只需要知道它需要使用哪个策略。

2. 生活中的例子 (Real-world Analogy)

  • 支付方式

    • 策略:信用卡支付、支付宝支付、微信支付、银行转账。
    • 上下文:电商平台的订单支付模块。用户在下单时可以选择不同的支付策略。
  • 排序算法

    • 策略:冒泡排序、快速排序、归并排序、插入排序。
    • 上下文:一个需要对数据集合进行排序的程序。可以根据数据量、数据特性选择不同的排序策略。
  • 压缩文件

    • 策略:ZIP压缩、RAR压缩、7z压缩。
    • 上下文:文件压缩工具。用户可以选择不同的压缩算法(策略)。
  • 导航软件的路线规划

    • 策略:最短时间路线、最少换乘路线、避开高速路线、步行路线。
    • 上下文:导航应用。用户选择不同的偏好,应用会采用不同的路线规划策略。

3. 结构 (Structure)

策略模式通常包含以下角色:

  1. Context (上下文)

    • 维护一个对 Strategy 对象的引用。
    • 定义一个或多个供客户端调用的方法,这些方法会将请求委托给其 Strategy 对象。
    • 可以提供一个方法来让客户端在运行时设置或更换 Strategy 对象。
  2. Strategy (策略接口或抽象类)

    • 定义所有支持的算法的公共接口。
    • Context 使用这个接口来调用某个 ConcreteStrategy 定义的算法。
  3. ConcreteStrategy (具体策略)

    • 实现 Strategy 接口。
    • 封装具体的算法或行为。
      在这里插入图片描述
      客户端通常会创建并传递一个具体策略对象给上下文。或者,上下文可以有一个默认策略,客户端也可以在运行时更改这个策略。

4. 适用场景 (When to Use)

  • 当你有许多相关的类,它们之间的区别仅仅是行为不同时。策略模式可以动态地选择这些行为中的一种。
  • 当需要使用一个算法的不同变体时。例如,你可能会定义一些反映不同空间/时间权衡的算法。
  • 当算法使用了你不想让客户端知道的数据。策略模式可以避免暴露复杂的、与算法相关的数据结构。
  • 当一个类定义了多种行为,并且这些行为以多个条件语句的形式出现。将相关的条件分支移入它们各自的 Strategy 类中以代替这些条件语句。
  • 当你希望客户端能够选择不同的算法,而不需要修改客户端代码时。

5. 优缺点 (Pros and Cons)

优点:

  1. 封装算法族:提供了管理和切换相关算法族的简便方法。
  2. 避免多重条件语句:消除了 if/elseswitch 等条件判断,使代码更清晰。
  3. 提高灵活性和可扩展性:可以轻松地增加新的策略(算法),而无需修改 Context 或其他策略,符合开闭原则。
  4. 策略可以复用:不同的 Context 可以共享相同的策略对象。
  5. 客户端与具体算法解耦:客户端只需要知道 Strategy 接口,不需要了解具体算法的实现细节。

缺点:

  1. 类数量增多:每个具体策略都是一个类,可能会导致系统中类的数量增加。
  2. 客户端必须了解不同的策略:客户端需要知道有哪些可用的策略,并选择合适的策略。这在某些情况下可能不是问题,但有时会增加客户端的复杂性(尽管客户端不需知道策略的实现细节)。
  3. 上下文与策略之间的通信开销:如果策略需要从上下文中获取大量数据,或者上下文需要频繁地与策略交互,可能会有一定的通信开销。有时策略接口可能需要定义得比较宽泛,以适应所有具体策略的需求。

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. 总结

策略模式是一种非常实用的行为设计模式,它使得算法的选择和实现与使用算法的客户端代码分离。通过将不同的算法封装在独立的策略类中,我们可以轻松地添加、删除或修改算法,而不会影响到客户端代码或其他算法。这大大提高了系统的灵活性、可维护性和可扩展性,是应对需求变化、实现“开闭原则”的有力工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值