Rust语言从入门到精通系列 - 玩转“策略模式”

2 分钟阅读

策略模式是面向对象编程中的一种设计模式,在该模式中,算法可以被独立于使用它的客户端和变化。该模式通过定义一个算法族,分别封装起来,使得它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

在Rust中,策略模式可以用于替代函数指针的使用。在本文中,我们将通过讲解常用用法和示例,进阶用法,最佳实践等几个方面探讨Rust中的策略模式实践。

常用用法和示例

在Rust中,我们可以将策略模式应用于以下两种场景:

  • 能够在运行时动态地选择实现的功能。
  • 通过实现不同的功能来清晰地描述某种实体的不同行为。

接下来,我们分别介绍这两种场景对应的示例。

运行时动态选择实现的功能

假设我们正在构建一个程序,该程序可以计算几个数字之间的最大值。然而,我们希望用户能够自由地选择用来计算最大值的算法。我们可以通过策略模式实现这一目标。

首先,我们需要定义一个MaxStrategy trait,该trait定义了求最大值的方法:

1
2
3
4
trait MaxStrategy {
    /// 求一组数字的最大值
    fn find_max(&self, nums: &[i32]) -> i32;
}

然后,我们可以实现几种不同的求最大值的算法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用快速排序求最大值
struct QuickSortStrategy;
impl MaxStrategy for QuickSortStrategy {
    fn find_max(&self, nums: &[i32]) -> i32 {
        let mut nums = nums.to_vec();
        nums.sort();
        *nums.last().unwrap()
    }
}

// 使用选择排序求最大值
struct SelectionSortStrategy;
impl MaxStrategy for SelectionSortStrategy {
    fn find_max(&self, nums: &[i32]) -> i32 {
        let mut nums = nums.to_vec();
        for i in 0..nums.len() {
            let max_index = (i..nums.len()).max_by_key(|&j| nums[j]).unwrap();
            nums.swap(i, max_index);
        }
        *nums.last().unwrap()
    }
}

现在,我们可以编写一个程序,该程序允许用户选择在运行时使用哪种算法来计算最大值。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
    // 读取用户输入
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: ./max <strategy>");
        std::process::exit(1);
    }

    // 根据用户输入选择算法
    let strategy: Box<dyn MaxStrategy> = match args[1].as_str() {
        "qs" => Box::new(QuickSortStrategy),
        "ss" => Box::new(SelectionSortStrategy),
        _ => {
            eprintln!("Invalid strategy");
            std::process::exit(1);
        }
    };

    // 读取一组数字,计算最大值
    let nums: Vec<i32> = read_line().split_whitespace().map(|x| x.parse().unwrap()).collect();
    let max = strategy.find_max(&nums);
    println!("Max: {}", max);
}

这个程序允许用户在运行时选择最大值的计算方法。用户可以执行以下命令来选择计算方法:

./max qs 1 2 3 4 5

该命令使用快速排序计算最大值。您也可以将qs替换为ss来使用选择排序。

通过实现不同的功能来清晰地描述实体的不同行为

假设我们正在为一个游戏编写一个AI模块,该模块可以让NPC根据当前情况进行不同的操作。在这个游戏中,有两种可供选择的操作:战斗和逃跑。我们可以通过策略模式实现这个AI。

首先,我们需要定义一个CombatStrategy trait和一个FleeStrategy trait来描述战斗和逃跑的操作:

1
2
3
4
5
6
7
trait CombatStrategy {
    fn execute(&self);
}

trait FleeStrategy {
    fn execute(&self);
}

然后,我们可以实现不同的战斗和逃跑策略:

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
struct NormalAttackStrategy;
impl CombatStrategy for NormalAttackStrategy {
    fn execute(&self) {
        println!("Performing normal attack");
    }
}

struct HeavyAttackStrategy;
impl CombatStrategy for HeavyAttackStrategy {
    fn execute(&self) {
        println!("Performing heavy attack");
    }
}

struct RunAwayStrategy;
impl FleeStrategy for RunAwayStrategy {
    fn execute(&self) {
        println!("Running away");
    }
}

struct HideStrategy;
impl FleeStrategy for HideStrategy {
    fn execute(&self) {
        println!("Hiding");
    }
}

现在,我们可以实现一个AI结构体,该结构体包含了一个战斗策略和一个逃跑策略。这个AI结构体可以通过选择战斗和逃跑策略来适应不同的游戏情境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct AI<T: CombatStrategy, U: FleeStrategy> {
    combat_strategy: T,
    flee_strategy: U,
}

impl<T: CombatStrategy, U: FleeStrategy> AI<T, U> {
    fn new(combat_strategy: T, flee_strategy: U) -> Self {
        Self {
            combat_strategy,
            flee_strategy,
        }
    }

    fn attack(&self) {
        self.combat_strategy.execute();
    }

    fn flee(&self) {
        self.flee_strategy.execute();
    }
}

现在,我们可以创建不同的AI实例,该实例可以根据当前情况执行不同的操作:

1
2
3
4
5
6
7
8
9
10
fn main() {
    // 创建一个能够攻击并逃跑的AI let ai1 = AI::new(NormalAttackStrategy, RunAwayStrategy);
    ai1.attack();
    ai1.flee();

    // 创建一个能够进行重型攻击并隐藏的AI
    let ai2 = AI::new(HeavyAttackStrategy, HideStrategy);
    ai2.attack();
    ai2.flee();
}

进阶用法

在本节中,我们将介绍一些高级用法,以提高策略模式的可定制性和代码的重用性。

使用associated type

在Rust中,我们可以将associated type用于定义策略模式的实现。通过关联类型,我们可以让实现具有更高的灵活性(可以定义impl中的任何类型)。

使用associated type,我们可以将上面的MaxStrategy trait改写成如下的形式:

1
2
3
4
5
trait MaxStrategy {
    /// 计算一组数字的最大值
    type Output;
    fn find_max(&self, nums: &[i32]) -> Self::Output;
}

我们可以在每个实现中定义Associated Type的类型别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct QuickSortStrategy;
impl MaxStrategy for QuickSortStrategy {
    type Output = i32;
    fn find_max(&self, nums: &[i32]) -> Self::Output {
        let mut nums = nums.to_vec();
        nums.sort();
        *nums.last().unwrap()
 }
}

struct SelectionSortStrategy;
impl MaxStrategy for SelectionSortStrategy {
    type Output = i32;
    fn find_max(&self, nums: &[i32]) -> Self::Output {
        let mut nums = nums.to_vec();
        for i in 0..nums.len() {
            let max_index = (i..nums.len()).max_by_key(|&j| nums[j]).unwrap();
            nums.swap(i, max_index);
        }
        *nums.last().unwrap()
    }
}

使用泛型

Rust中的策略模式还可以使用泛型来提高代码的灵活性和可重用性。例如,在上面的战斗和逃跑的示例中,我们可以将AI结构体改写成以下的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct AI<T> {
    combat_strategy: T,
}

impl<T: CombatStrategy> AI<T> {
    fn new(combat_strategy: T) -> Self {
        Self {
            combat_strategy,
        }
    }

    fn execute_strategy(&self) {
        self.combat_strategy.execute();
    }
}

现在,我们只需要一个CombatStrategy trait来定义所有的AI操作。这使得对于AI而言,战斗和逃跑等操作可以相互替换。

最佳实践

在使用策略模式时,为了实现代码的可维护性和可读性,需要遵循一些最佳实践:

  1. 将策略模式的变量作为trait对象使用,而不是作为struct实例使用。
  2. 提高使用associated type定义的trait的灵活性。
  3. 对于涉及到多个策略的情况,需要使用泛型。
  4. 对于每个策略,使用单独的模块定义该策略的实现,以提高代码的可维护性。
  5. 尽量避免过度使用策略模式。

结论

Rust中的策略模式是一种非常灵活和可定制的设计模式,可以用于在运行时动态地选择实现、或者通过实现不同的功能来清晰地描述某种实体的不同行为。在实现策略模式时,我们可以使用associated type和泛型来增强策略模式的可定制性和代码的重用性。如果你想将这些概念应用于实际项目中,请根据最佳实践编写代码,以提高代码的可读性和可维护性。

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 TinyZ Zzh (包含链接: https://tinyzzh.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请 与我联系 (tinyzzh815@gmail.com)

TinyZ Zzh

TinyZ Zzh

专注于高并发服务器、网络游戏相关(Java、PHP、Unity3D、Unreal Engine等)技术,热爱游戏事业, 正在努力实现自我价值当中。

评论

  点击开始评论...