65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 设计模式:策略模式

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.82/5 (3投票s)

2023 年 4 月 20 日

CPOL

8分钟阅读

viewsIcon

16300

了解如何在 C# 中使用策略模式来创建灵活且可重用的代码。使用此设计模式替换 if 语句并简化代码。

枯燥的理论

正如引言中所说,策略模式可用于在不更改代码逻辑的情况下轻松切换不同的算法。它实际上定义了一系列算法。它们之所以是一系列,是因为它们通过一个接口连接,该接口承诺了实现的功能。

由于这个接口,您可以拥有具有相同方法但实现不同的类。而且,在不更改任何现有代码逻辑的情况下创建新的实现也相当容易。

所以,想象一下您有一个接口,该接口在 4 个类中使用。这 4 个类在外部是相同的(例如,具有相同的方法),但方法都有不同的实现,导致输出不同。

策略模式是一种在给定条件下使用这些实现之一的模式。接口是策略模式的关键。

好了,够枯燥的理论了。我们开始写代码吧!

文章代码

我创建了一个示例控制台应用程序,我将使用它来向您展示策略模式。如果您想遵循本教程,建议下载它。您可以在此处找到代码:

它包含一个包含两个项目的解决方案;都是控制台应用程序。两个控制台应用程序都有 Program.cs 文件和一个名为“Movies”的文件夹。在该文件夹中有三部具有相同属性和方法的电影。

我将首先使用 StrategyPatternDemo.ConsoleApp

注意:每部电影都包含 Age() 方法。在实际情况中,我会将 Age() 方法移到一个基类,并在电影类中使用它。但出于示例目的,我将方法保留在这些类中,以便您可以看到它是如何工作的。

一个基本示例

StrategyPatternDemo.ConsoleApp 的问题在于,当我想要展示多部电影的信息时,我需要初始化每部电影,展示当前为《怪物史莱克》展示的所有信息,然后重复此过程,直到我处理完所有电影。这还导致了代码重复违规。

另一个问题是,当添加新电影时,我需要从之前处理过的电影中复制粘贴所有内容,并确保它正常工作。

Program.cs 包含一个用于展示电影“Shrek”信息的基本代码。我将继续使用这个小的代码基础。

接口

可以看到,有三部电影:盗梦空间、怪物史莱克和黑客帝国。它们都具有相同的属性和相同的方法。为了利用策略模式,我们需要一个接口。我创建一个名为 IMovieStrategy 的接口,该接口如下所示:

public interface IMovieStrategy
{
    string Title { get; }
    string Description { get; }
    DateTime ReleaseDate { get; }
    int Age();
}

我们需要确保每部电影都实现这个接口。现在我们已经创建了一个算法家族。

背景

关键在于,我们不关心家族的哪个部分在控制台显示,或者在代码中使用。这就是接口的用途。但是我们需要一些上下文,字面意义上的。这个上下文将有一个方法,该方法获取一个家族成员并执行代码。

这个上下文需要接收它需要执行的策略以及策略的方法和属性(这些是已知的,因为它是接口)。我创建一个名为 Context.cs 的新类,并添加以下代码:

public class Context
{
    private IMovieStrategy? _movieStrategy;

    public void SetStrategy(IMovieStrategy strategy)
    {
        _movieStrategy = strategy;
    }

    public void ShowMovie()
    {
        if (_movieStrategy == null)
            return;

        Console.WriteLine(_movieStrategy.Title);
        Console.WriteLine(_movieStrategy.Description);

        Console.WriteLine($"{_movieStrategy.ReleaseDate} 
                         ({_movieStrategy.Age()} years old)");
    }
}

代码没有显示任何关于特定电影的内容。它非常通用。SetStrategy 方法接收 IMovieStrategy 的一个实现。它不在乎是哪个,只关心该类是否实现了该接口。

一旦设置好,ShowMovie 方法就会显示所有必需的信息。因为它是接口,所以我只需要编写一次,就可以为不同的 IMovieStrategy 实现重复使用。这也解决了代码重复违规问题。

在应用程序中实现

让我们回到 Program.cs。我们现在可以一次性初始化 Context 类,然后为每部电影设置策略。也许我直接展示给您看会更好。

Context context = new();
context.SetStrategy(new Shrek());
context.ShowMovie();
Console.WriteLine("-----------------------------------------------------");

context.SetStrategy(new TheMatrix());
context.ShowMovie();
Console.WriteLine("-----------------------------------------------------");

context.SetStrategy(new Inception());
context.ShowMovie();
Console.WriteLine("-----------------------------------------------------");

首先,我初始化 context 类。然后我添加电影,该电影是 IMovieStrategy 的一个实现。然后我调用 context 上的 ShowMovie() 来显示电影信息。

就是这样!一个基本的策略模式示例。我已经将所有逻辑移到了一个处理信息的类(上下文)中。

假设我想更改显示发布日期和年龄的行。我只需要在一个地方进行更改:Context,方法 ShowMovie()。我不必像以前的代码那样在三个不同的地方进行更改。

如果添加第四部电影,ShowMovie 中的更改也适用于该电影。

替换 If 语句

但我使用策略模式的真正原因是为了摆脱冗长的 if 语句。在项目 StrategyPatternPartTwo.ConsoleApp 中,我创建了一个这样的 if 语句。如果您打开 Program.cs,您将看到代码。不是最好的代码,但这就是重点。

它看起来不像一个真正的长 if 语句,代码也不复杂。但这是一个教程,而不是一个真实的应用。我喜欢保持示例的简洁,以便您了解发生了什么。

这段代码中的“问题”:首先……它是区分大小写的。如果我搜索“shrek”(小写 s),它将找不到电影。所有电影都存在这种情况,我需要在 3 个地方修复它。如果添加第 4 部电影,我需要确保不再犯同样的错误。

另一个问题是增长。世界上有很多电影,我可能想在我的应用程序中添加更多电影。添加越来越多的 else-if 语句会使文件变得非常大。如果我有 100 部电影,并且我发现我在第一部电影中犯了一个错误,我需要修复所有 100 部。

我想完全删除 if 语句,让逻辑“决定”我们需要哪部电影。或者更确切地说,需要哪个接口的实现。

因为就像我们在上一部分所做的那样,我们需要一个接口。您可以直接复制粘贴之前的接口,因为它是一样的。不要忘记将接口连接到电影。

一个新的上下文

我们将把我们所有的电影注入到一个名为 Context 的类中。该类将与用户输入一起决定显示哪部电影。所以,让我们创建 Context 类。

在这个 Context 类中,我们需要注入电影。我们通过之前创建的接口来实现。由于我们有多部电影,我们将注入一个 IMovieStrategy 列表。

ShowMovie 方法也属于这个 Context,但它接收用户可以输入的标题。正是这个标题让 Context 做出决定。

总而言之,代码看起来是这样的:

public class Context
{
    private readonly IEnumerable<IMovieStrategy> movieStrategies;

    public Context(IEnumerable<IMovieStrategy> movieStrategies)
    {
        this.movieStrategies=movieStrategies;
    }

    public void ShowMovie(string title)
    {
        IMovieStrategy movie = movieStrategies.SingleOrDefault(x => x.Title == title) 
            ?? throw new Exception("Movie not found");

        Console.WriteLine(movie.Title);
        Console.WriteLine(movie.Description);
        Console.WriteLine($"{movie.ReleaseDate} ({movie.Age()} years old)");
    }
}

请注意这一行:“IMovieStrategy movie = movieStrategies.SingleOrDefault(x => x.Title == title) ?? throw new Exception(“Movie not found”);” 它的作用是遍历注入的 IMovieStrategies(多个),找到包含用户输入的标题的那一个。这就是整个诀窍。

如果找到了策略,它就可以获取该初始化的电影并在屏幕上显示信息。

但是……我们如何将这些电影注入到系统中呢?为此,我们必须切换到 Program.cs

设置依赖项

我们需要像往常一样配置我们的注入。您可能需要为此安装 Microsoft.Extensions.DependencyInjection 包。

要配置依赖注入,我们需要一个 ServiceCollection 和一个 ServiceProvider。前者可以配置策略和 Context 类。之后,我们获取服务 Context,然后就可以开始了。

ServiceProvider serviceProvider = new ServiceCollection()
    .AddScoped<IMovieStrategy, Shrek>()
    .AddScoped<IMovieStrategy, TheMatrix>()
    .AddScoped<IMovieStrategy, Inception>()
    .AddScoped<Context>()
    .BuildServiceProvider();

Context context = serviceProvider.GetService<Context>();

Console.WriteLine("Type the name of the movie you want to get information about:");
Console.WriteLine("Shrek");
Console.WriteLine("Inception");
Console.WriteLine("The Matrix");
Console.WriteLine("");
Console.Write("Type here: ");
string? name = Console.ReadLine();

context.ShowMovie(name);

可以看到,if 语句消失了,我们从 39 行代码减少到了 22 行(包括 usings 和 service provider)。应用程序仍然按预期工作,但现在更加通用了。

我们可以更轻松地修复区分大小写的问题。只需转到 Context.cs 并进行以下更改:

IMovieStrategy movie = movieStrategies.SingleOrDefault(x => x.Title == title)
    ?? throw new Exception("Movie not found");

// To: 

IMovieStrategy movie = movieStrategies.SingleOrDefault
                       (x => x.Title.ToLower() == title.ToLower()) 
    ?? throw new Exception("Movie not found");

一个新的策略

让我们添加电影“Jaws”。只需简单地复制粘贴现有的电影之一,然后重命名为“Jaws”。

public class Jaws: IMovieStrategy
{
    public string Title => "Jaws";
    public string Description => "When a killer shark unleashes chaos 
         on a beach community off Cape Cod, it's up to a local sheriff, 
         a marine biologist, and an old seafarer to hunt the beast down.";
    public DateTime ReleaseDate => new(1975, 12, 18);

    public int Age()
    {
        DateTime currentDate = DateTime.Now;
        int age = currentDate.Year - ReleaseDate.Year;

        if (currentDate < ReleaseDate.AddYears(age))
        {
            age--;
        }

        return age;
    }
}

然后我们可以将其添加到我们的依赖注入配置和控制台应用程序的菜单中。

ServiceProvider serviceProvider = new ServiceCollection()
    .AddScoped<IMovieStrategy, Shrek>()
    .AddScoped<IMovieStrategy, TheMatrix>()
    .AddScoped<IMovieStrategy, Inception>()
    .AddScoped<IMovieStrategy, Jaws>()
    .AddScoped<Context>()
    .BuildServiceProvider();

Context context = serviceProvider.GetService<Context>();

Console.WriteLine("Type the name of the movie you want to get information about:");
Console.WriteLine("Shrek");
Console.WriteLine("Inception");
Console.WriteLine("The Matrix");
Console.WriteLine("Jaws");
Console.WriteLine("");
Console.Write("Type here: ");
string? name = Console.ReadLine();

context.ShowMovie(name);

就是这样!现在它奏效了。只需启动应用程序,输入“jaws”或“JaWs”,然后按 Enter。您将在屏幕上看到关于 Jaws 的信息。

更进一步

严格来说这不是策略模式,但很方便。在上面的示例中,我输入了电影的名称,以便用户知道选择什么。我们也可以从策略中获取这些信息。

如果我们使用策略,我们将使自己更加轻松,因为这样我们只需确保在服务提供商中注册带有接口的电影。看看下面的代码:

ServiceProvider serviceProvider = new ServiceCollection()
    .AddScoped<IMovieStrategy, Shrek>()
    .AddScoped<IMovieStrategy, TheMatrix>()
    .AddScoped<IMovieStrategy, Inception>()
    .AddScoped<IMovieStrategy, Jaws>()
    .AddScoped<Context>()
    .BuildServiceProvider();

Context context = serviceProvider.GetService<Context>();
IEnumerable<IMovieStrategy> movies = 
            serviceProvider.GetService<IEnumerable<IMovieStrategy>>();

Console.WriteLine("Type the name of the movie you want to get information about:");

foreach(IMovieStrategy strategy in movies)
{
    Console.WriteLine(strategy.Title);
}

Console.WriteLine("");
Console.Write("Type here: ");
string? name = Console.ReadLine();

context.ShowMovie(name);

让我们添加另一部电影。比如说“The Muppets Take Manhattan”。代码会是这样的:

public class TheMuppetsTakeManhattan: IMovieStrategy
{
    public string Title => "The Muppets Take Manhattan";
    public string Description => "Kermit and his friends go to New York City 
         to get their musical on Broadway only to find it's a more difficult task 
         than they anticipated.";
    public DateTime ReleaseDate => new(1984, 12, 20);

    public int Age()
    {
        DateTime currentDate = DateTime.Now;
        int age = currentDate.Year - ReleaseDate.Year;

        if (currentDate < ReleaseDate.AddYears(age))
        {
            age--;
        }

        return age;
    }
}

接下来,我们将其添加到策略的注册(依赖注入)中:

ServiceProvider serviceProvider = new ServiceCollection()
    .AddScoped<IMovieStrategy, Shrek>()
    .AddScoped<IMovieStrategy, TheMatrix>()
    .AddScoped<IMovieStrategy, Inception>()
    .AddScoped<IMovieStrategy, Jaws>()
    .AddScoped<IMovieStrategy, TheMuppetsTakeManhattan>()
    .AddScoped<Context>()
    .BuildServiceProvider();

应用程序显示新电影:

结论

就这样:C# 中的策略模式。老实说,我花了一段时间才掌握这个模式。主要是因为我听说过它,但从未用过它。我参与的大多数大型项目都使用这个模式,尤其是我看到一些 if 语句变得过于冗长时。

有很多理由可以使用策略模式。但请记住不要过度使用。如果一个策略的实现只有一行代码,或者您发现自己只有两个策略:为了您好,请不要使用该模式。

我没有谈论的一个方面是 CanExecuteExecute。当我完成这篇文章时,我认为这已经够读者看了。但如果您真的想了解更多这方面的信息,请在评论中告诉我。

历史

  • 2023 年 4 月 20 日:初始版本
© . All rights reserved.