C--设计模式-五-

42 阅读21分钟

C# 设计模式(五)

原文:Design Patterns in C#

协议:CC BY-NC-SA 4.0

十七、命令模式

本章介绍了命令模式。

GoF 定义

将请求封装为一个对象,从而允许您用不同的请求、队列或日志请求参数化客户端,并支持可撤销的操作。

概念

使用这种模式,您可以封装一个方法调用过程。这里,一个对象可以通过某种明确的方法调用一个操作,而不用担心如何执行这个操作。这种模式是那些仅仅通过阅读描述通常很难理解的模式之一。当您看到实现时,这个概念会变得更加清晰。所以,请跟我一起继续阅读,直到看到演示 1。

一般来说,这里有四个术语很重要:调用方客户端命令方接收方,具体如下。

  • 命令对象由接收者执行的动作组成。

  • 一个命令对象可以以特定于接收者的类的方式调用接收者的方法。然后接收器开始处理作业(或动作)。

  • 命令对象被单独传递给调用者对象来调用命令。invoker 对象包含具体化的方法,通过这些方法,客户端可以执行工作,而不用担心目标接收者如何执行实际的工作。

  • 客户端对象保存调用程序对象和命令对象。客户端只作出决定(即执行哪些命令),然后将命令传递给调用程序对象来执行。

真实世界的例子

当你在画一幅画的时候,你可能需要重画(撤销)它的一些部分来使它变得更好。

计算机世界的例子

通常,您可以在编辑器或集成开发环境(IDE)的菜单系统中观察到这种模式。例如,您可以使用命令模式来支持撤销、多次撤销或软件应用中的类似操作。

微软在 Windows 演示基础(WPF)中使用了这种模式。出现在 Visual Studio 杂志 ( https://visualstudiomagazine.com/articles/2012/04/10/command-pattern-in-net.aspx )上的一篇 2012 年的文章详细描述了它。命令模式非常适合处理 GUI 交互。它工作得非常好,微软已经将其紧密集成到 Windows 演示基础(WPF)堆栈中。最重要的部分是来自 System.Windows.Input 名称空间的 ICommand 接口。任何实现了 ICommand 接口的类都可以通过通用的 WPF 控件来处理键盘或鼠标事件。这种链接既可以在 XAML 中完成,也可以在代码隐藏中完成。

另外,如果你熟悉 Java 或者 Swing,你看到Action也是一个命令对象。

履行

在这个例子中,RemoteControl是 Invoker 类。GameStartCommandGameStartCommand是表示命令的具体类。这两个类实现了公共接口ICommand ,,如下所示(相关注释说明了每个方法的用途)。

  public interface ICommand
  {
        // To execute a command
        void Execute();
        // To undo last command execution
        void Undo();
    }

Game是接收器类,其定义如下。

public class Game
{
  string gameName;
  public Game(string name)
   {
     this.gameName = name;
   }
  public void Start()
   {
     Console.WriteLine($"{gameName} is on.");
   }
  public void DisplayScore()
   {
     Console.WriteLine("The score is changing time to time.");
   }
  public void Finish()
   {
     Console.WriteLine($"---The game of {gameName} is over.---");
   }
}

当客户端使用一个GameStopCommand命令并对一个Invoker对象调用ExecuteCommand方法时,如下所示。

invoker.ExecuteCommand();

目标接收者(本例中的Game类对象)只执行以下动作。

game.Finish();

但是当客户端使用一个GameStartCommand命令并使用如下代码调用一个Invoker对象上的ExecuteCommand方法时。

invoker.ExecuteCommand();

目标接收者(本例中的Game类对象)执行以下一组动作。

game.Start();
game.DisplayScore();

所以,你可以看到一个命令不需要只执行一个动作;相反,根据您的需要,您可以在目标接收者上执行一系列操作,并将它们封装在一个命令对象中。

Points to Note

本章中的例子展示了撤销操作的简单演示。撤消的实现取决于规范,在某些情况下可能很复杂。对于演示 1,我简单地假设撤销调用只是撤销上一个成功执行的命令。GameStartCommandGameStopCommand类的Execute()Undo()方法正在做相反的事情。也就是说,当客户端使用GameStopCommand调用撤销操作时,游戏重启并显示分数(在本例中是一个简单的控制台消息)。但是如果客户端使用GameStartCommand调用撤销操作,游戏会立即停止。这类似于打开一盏灯,关掉同样的灯;或者将一个数加到一个目标数上,作为相反的情况,再次从结果数中减去相同的数。

最后,看看下面的代码段,这是我如何创建一个命令对象的。

Game gameName = new Game("Golf");
// Command to start the game
GameStartCommand gameStartCommand = new GameStartCommand(gameName);

我将命令设置为调用程序,并使用它的ExecuteCommand()方法来执行命令。后来,我又撤销了这个。我保留了控制台消息以帮助您理解。

Console.WriteLine("**Starting the game and performing undo immediately.**");
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();
// Performing undo operation
Console.WriteLine("\nUndoing the previous command now.");
invoker.UndoCommand();

类图

图 17-1 为类图。

img/463942_2_En_17_Fig1_HTML.jpg

图 17-1

类图

解决方案资源管理器视图

图 17-2 显示了程序的高层结构。

img/463942_2_En_17_Fig2_HTML.jpg

图 17-2

解决方案资源管理器视图

演示 1

这是完整的程序。

using System;

namespace CommandPattern
{
    /// <summary>
    ///  Receiver Class
    /// </summary>
    public class Game
    {
        string gameName;
        public Game(string name)
        {
            this.gameName = name;
        }
        public void Start()
        {
            Console.WriteLine($"{gameName} is on.");
        }
        public void DisplayScore()
        {
            Console.WriteLine("The score is changing time to time.");
        }
        public void Finish()
        {
            Console.WriteLine($"---The game of {gameName} is over.---");
        }

    }
    /// <summary>
    /// The command interface
    /// </summary>
    public interface ICommand
    {
        // To execute a command
        void Execute();
        // To undo last command execution
        void Undo();

    }
    /// <summary>
    /// GameStartCommand
    /// </summary>
    public class GameStartCommand : ICommand
    {
        private Game game;
        public GameStartCommand(Game game)
        {
            this.game = game;
        }
        public void Execute()
        {
            game.Start();
            game.DisplayScore();
        }

        public void Undo()
        {
            Console.WriteLine("Undoing start command.");
            game.Finish();
        }
    }
    /// <summary>
    /// GameStopCommand
    /// </summary>

    public class GameStopCommand : ICommand
    {
        private Game game;
        public GameStopCommand(Game game)
        {
            this.game = game;
        }
        public void Execute()
        {
            Console.WriteLine("Finishing the game.");
            game.Finish();
        }

        public void Undo()
        {
            Console.WriteLine("Undoing stop command.");
            game.Start();
            game.DisplayScore();
        }
    }

    /// <summary>
    /// Invoker class
    /// </summary>
    public class RemoteControl
    {
        ICommand commandToBePerformed, lastCommandPerformed;
        public void SetCommand(ICommand command)
        {
            this.commandToBePerformed = command;
        }
        public void ExecuteCommand()
        {
            commandToBePerformed.Execute();
            lastCommandPerformed = commandToBePerformed;
        }

        public void UndoCommand()
        {
            // Undo the last command executed
            lastCommandPerformed.Undo();
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Command Pattern Demonstration***\n");

            /* Client holds both the Invoker and Command Objects */
            RemoteControl invoker = new RemoteControl();

            Game gameName = new Game("Golf");
            // Command to start the game
            GameStartCommand gameStartCommand = new GameStartCommand(gameName);
            // Command to stop the game
            GameStopCommand gameStopCommand = new GameStopCommand(gameName);

            Console.WriteLine("**Starting the game and performing undo immediately.**");
            invoker.SetCommand(gameStartCommand);
            invoker.ExecuteCommand();
            // Performing undo operation
            Console.WriteLine("\nUndoing the previous command now.");
            invoker.UndoCommand();

            Console.WriteLine("\n**Starting the game again.Then stopping it and undoing the stop operation.**");
            invoker.SetCommand(gameStartCommand);
            invoker.ExecuteCommand();
            // Stop command to finish the game
            invoker.SetCommand(gameStopCommand);
            invoker.ExecuteCommand();
            // Performing undo operation
            Console.WriteLine("\nUndoing the previous command now.");
            invoker.UndoCommand();

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Command Pattern Demonstration***

**Starting the game and performing undo immediately.**
Golf is on.
The score is changing time to time.

Undoing the previous command now.
Undoing start command.
---The game of Golf is over.---

**Starting the game again.Then stopping it and undoing the stop operation.**
Golf is on.
The score is changing time to time.
Finishing the game.
---The game of Golf is over.---

Undoing the previous command now.
Undoing stop command.
Golf is on.
The score is changing time to time.

问答环节

17.1 GoF 定义从“封装请求”开始在演示 1 中,你是如何实现 封装 的?

命令对象包含一组针对特定接收器的操作。当您设置命令并在 invoker 对象上调用ExecuteCommand()时,预期的动作在接收者端执行。从外面看,没有其他物体知道这是如何发生的;他们只知道如果他们调用ExecuteCommand(),他们的请求就会被处理。

遵循 GoF 定义,你如何参数化其他有不同请求的对象?

注意,我首先在 invoker 中设置了GameStartCommand,后来,我用GameStopCommand.Invoker对象替换了它,在两种情况下都简单地调用了ExecuteCommand()

在这个例子中,你只和一个接收者打交道。你如何处理多个接收者?

在这个例子中,Game 是 receiver 类,但是没有人限制您创建一个新的类,并遵循演示 1 中所示的实现。另外,请注意,您使用下面的代码行创建了一个Game类对象。

Game gameName = new Game("Golf");

由于Game类构造函数接受一个字符串参数,您也可以传递一个不同的值并创建一个不同的对象。以下代码段是一个示例。

Console.WriteLine("\nPlaying another game now.(Optional for you)");

gameName = new Game("Soccer");
// Command to start the game
gameStartCommand = new GameStartCommand(gameName);
// Command to stop the game
gameStopCommand = new GameStopCommand(gameName);

// Starting the game
invoker.SetCommand(gameStartCommand);
invoker.ExecuteCommand();

// Stopping the game
invoker.SetCommand(gameStopCommand);
invoker.ExecuteCommand();

The previous code segment can generate the following output as expected:
Playing another game now.(Optional for you)
Soccer is on.
The score is changing time to time.
Finishing the game.
---The game of Soccer is over.---

17.4 我可以忽略 invoker 对象吗?

很多时候,程序员试图在面向对象编程(OOP)中封装数据和相应的方法。但是你发现在命令模式中,你在尝试封装命令对象。换句话说,您正在从不同的角度实现封装。

我之前告诉过你,当调用 invoker 对象的ExecuteCommand()时,预期的动作在接收者端执行。从外面看,没有其他物体知道它是如何发生的;他们只知道如果他们调用ExecuteCommand(),,他们的请求就会被处理。因此,简单地说,一个调用程序包含一些明确的方法,通过这些方法,客户端可以执行一项工作,而不用担心实际的工作在接收端是如何执行的。

当您需要处理一组复杂的命令时,这种方法很有意义。

让我们再看一遍条款。您创建命令对象,并将其传递给一些接收者来访问它们,然后通过调用命令对象的方法的调用者来执行这些命令(例如,本例中的ExecuteCommand)。对于一个简单的用例,这个 invoker 类不是强制性的。例如,考虑这样一种情况,其中一个命令对象只有一个方法要执行,并且您正试图免除调用程序来调用该方法。但是,当您想要跟踪日志文件(或队列)中的一系列命令时,调用程序可能会发挥重要作用。

你为什么要跟踪这些日志?

您可能希望创建撤消或重做操作。

17.6 指挥模式的主要优势是什么?

以下是一些优点。

  • 创建和最终执行的请求是分离的。客户端可能不知道调用者如何执行操作。

  • 您可以创建宏命令(这些是多个命令的序列,可以一起调用。例如,对于宏命令,您可以创建一个类,该类具有一个接受命令列表的构造函数。在它的Execute()方法中,您可以使用for循环/ foreach循环依次调用这些命令中的Execute()

  • 可以在不影响现有系统的情况下添加新命令。

  • 最重要的是,您可以支持急需的撤销(和重做)操作。

  • 应该注意的是,一旦您简单地创建了一个命令对象,并不意味着计算会立即开始。您可以将它安排在以后,或者将它们放在作业队列中,以后再执行。此外,通过使用线程池,您可以在多线程环境中异步执行它们。(异步编程在本书第二十七章讨论。)

17.7 指挥模式面临哪些挑战?

以下是一些缺点。

  • 为了支持更多的命令,您需要创建更多的类。因此,随着时间的推移,维护可能会很困难。

  • 当出现错误情况时,如何处理错误或决定如何处理返回值变得很棘手。客户可能想知道这些。但是这里您将命令与客户端代码解耦,所以这些情况很难处理。在调用者可以在不同的线程中运行的多线程环境中,这一挑战变得非常重要。

17.8 在演示 1 中,您只撤销了最后一个命令?有什么方法可以实现“撤销全部”吗?此外,您如何记录请求?

问得好。您可以简单地维护一个可以存储命令的堆栈,然后您可以简单地从堆栈中弹出项目并调用它的undo()方法。在第十九章(在 Memento 模式上,类似于这个模式),我进一步讨论了撤销和各种实现。现在,让我向您展示一个简单的例子,在这个例子中,您可以撤销所有以前的命令。演示 2 就是为此而做的。它是对演示 1 的简单修改,因此省略了类图和解决方案资源管理器视图;可以直接跳转到实现中。

你问了另一个关于如何记录请求的问题。在演示 2 中,当我维护列表来存储执行的命令时,我使用这个列表来支持使用单个方法调用“撤销所有命令”。同一个列表可以作为您可以在控制台中打印的命令历史。或者,您可以创建一个单独的文件来保存每次命令执行时的详细信息。如有必要,稍后您可以检索该文件进行详细查看。

修改的实现

这个例子向你展示了一种调用多个撤销操作的方法。对 invoker 类做了一些小的修改。我维护一个列表来存储所有执行的命令。每当一个命令被执行时,它都会被添加到列表中,稍后当我调用UndoAll()时,我可以简单地迭代这个列表并调用相应的撤销操作。调用者以粗体显示主要变化,如下所示。

/// <summary>
/// Invoker class
/// </summary>
public class RemoteControl
{
 ICommand commandToBePerformed, lastCommandPerformed;
 List<ICommand> savedCommands = new List<ICommand>();
 public void SetCommand(ICommand command)
 {
   this.commandToBePerformed = command;
 }
 public void ExecuteCommand()
 {
   commandToBePerformed.Execute();
   lastCommandPerformed = commandToBePerformed;
   savedCommands.Add(commandToBePerformed);
  }
 public void UndoCommand()
  {
    // Undo the last command executed
    lastCommandPerformed.Undo();
   }
 public void UndoAll()
  {
    for (int i = savedCommands.Count; i > 0; i--)
     {
       // Get a restore point and call Undo()
       savedCommands[i - 1].Undo();
      }
   }
}

Game类现在没有Start()方法;相反,它有两个新方法叫做UpLevel()DownLevel(),如下所示。

public void UpLevel()
{
 ++level;
 Console.WriteLine("Level upgraded.");
}
public void DownLevel()
{
 --level;
 Console.WriteLine("Level downgraded.");
}

UpLevel()方法升级游戏等级。DownLevel()方法做相反的事情,所以它被用在GameStartCommand类的Undo操作中。为了达到我的主要目的(向您展示“撤销全部”),我不需要这个例子中的GameStopCommand类,所以为了使这个例子简短,我也省略了那个类。最后,我做了一个简单的假设,当游戏等级设置为 0 时(即处于出生状态),如果你执行Undo(),游戏就会停止。剩下的代码很容易理解,现在可以开始演示 2 了。

演示 2

这是完整的程序。

using System;
using System.Collections.Generic;

namespace CommandPatternDemonstration2
{
    // Receiver Class
    public class Game
    {
        string gameName;
        public int level;
        public Game(string name)
        {
            this.gameName = name;
            level = -1;
            Console.WriteLine($"Game started.");
        }
        public void DisplayLevel()
        {
            Console.WriteLine($"Current level is set to {level}.");
        }
        public void UpLevel()
        {
            ++level;
            Console.WriteLine("Level upgraded.");
        }
        public void DownLevel()
        {
            --level;
            Console.WriteLine("Level downgraded.");
        }
        public void Finish()
        {
            Console.WriteLine($"---The game of {gameName} is over.---");
        }

    }
    public interface ICommand
    {
        void Execute();
        void Undo();

    }
    /// <summary>
    /// GameStartCommand
    /// </summary>
    public class GameStartCommand : ICommand
    {
        private Game game;
        public GameStartCommand(Game game)
        {
            this.game = game;
        }
        public void Execute()
        {
            game.UpLevel();
            game.DisplayLevel();
        }

        public void Undo()
        {
            if (game.level > 0)
            {
                game.DownLevel();
                game.DisplayLevel();
            }
            else
            {
                game.Finish();
            }
        }
    }

    /// <summary>
    /// Invoker class
    /// </summary>
    public class RemoteControl
    {
        ICommand commandToBePerformed, lastCommandPerformed;
        List<ICommand> savedCommands = new List<ICommand>();
        public void SetCommand(ICommand command)
        {
            this.commandToBePerformed = command;
        }
        public void ExecuteCommand()
        {
            commandToBePerformed.Execute();
            lastCommandPerformed = commandToBePerformed;
            savedCommands.Add(commandToBePerformed);
        }

        public void UndoCommand()
        {
            // Undo the last command executed
            lastCommandPerformed.Undo();
        }
        public void UndoAll()
        {
            for (int i = savedCommands.Count; i > 0; i--)
            {
                // Get a restore point and call Undo()
                savedCommands[i - 1].Undo();
            }
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Command Pattern Demonstration2***\n");

            // Client holds both the Invoker and Command Objects
            RemoteControl invoker = new RemoteControl();

            Game gameName = new Game("Golf");
            // Command to start the game
            GameStartCommand gameStartCommand = new GameStartCommand(gameName);

            Console.WriteLine("**Starting the game and upgrading the level 3 times.**");
            invoker.SetCommand(gameStartCommand);
            invoker.ExecuteCommand();
            invoker.ExecuteCommand();
            invoker.ExecuteCommand();

            // Performing undo operation(s) one at a time
            //invoker.UndoCommand();
            //invoker.UndoCommand();
            //invoker.UndoCommand();

            Console.WriteLine("\nUndoing all the previous commands at one shot.");
            invoker.UndoAll();
            Console.ReadKey();
        }
    }
}

输出

这是新的输出。

***Command Pattern Demonstration2***

Game started.
**Starting the game and upgrading level 3 times.**
Level upgraded.
Current level is set to 0.
Level upgraded.
Current level is set to 1.
Level upgraded.
Current level is set to 2.

Undoing all the previous commands at one shot.
Level downgraded.
Current level is set to 1.
Level downgraded.
Current level is set to 0.
---The game of Golf is over.---

十八、迭代器模式

本章涵盖了迭代器模式。

GoF 定义

提供一种方法来顺序访问聚合对象的元素,而不暴露其底层表示。

概念

迭代器通常用于遍历容器(或对象集合)以访问其元素,而不知道数据在内部是如何存储的。当您需要以标准和统一的方式遍历不同种类的集合对象时,它非常有用。图 18-1 显示了一个迭代器模式的示例和最常见的图表。

img/463942_2_En_18_Fig1_HTML.jpg

图 18-1

迭代器模式的示例图

参与者描述如下。

  • 迭代器是访问或遍历元素的接口。

  • 具体迭代器实现了Iterator接口方法。它还可以跟踪聚合遍历中的当前位置。

  • 聚合定义了一个可以创建Iterator对象的接口。

  • 混凝土骨料实现了Aggregate接口。它返回一个ConcreteIterator的实例。

Points to Note

  • 它经常用于遍历树状结构的节点。在许多例子中,您可能会注意到迭代器模式和组合模式。

  • 迭代器的作用不仅限于遍历。这个角色可以改变以支持各种需求。例如,您可以用各种方式过滤元素。

  • 客户端看不到实际的遍历机制。客户端程序只使用公共迭代器方法。

  • 迭代器和枚举器的概念已经存在很久了。枚举器根据一个标准产生下一个元素,而使用迭代器,你从起点到终点循环一个序列。

  • 将 foreach 迭代器应用于由枚举器生成的集合是一种常见的做法。然后,您可以获取该值并将其应用到循环体中。

真实世界的例子

假设有两家公司:A 公司和 b 公司。A 公司存储其员工记录(即每个员工的姓名、地址、工资明细等。)在链表数据结构中。B 公司将其员工数据存储在一个数组中。一天,两家公司决定合并成一家大公司。迭代器模式在这种情况下非常方便,因为您不需要从头开始编写代码。在这种情况下,您可以使用一个公共接口来访问两家公司的数据。因此,您可以简单地调用这些方法,而无需重写代码。

考虑另一个例子。假设你的公司决定根据员工的表现提升他们。所以,所有的经理聚在一起,为晋升制定一个共同的标准。然后,他们一个接一个地遍历员工的记录,以标记潜在的晋升候选人。

你也可以考虑不同领域的例子。例如,当您将歌曲存储在您喜欢的音频设备(例如,MP3 播放器)或移动设备中时,您可以通过各种按钮按压或滑动动作来迭代它们。基本思想是为您提供一种机制,以便您可以平滑地迭代您的列表。

计算机世界的例子

浏览以下两个要点。这些是迭代器模式的常见例子。

  • C# 拥有在 Visual Studio 2005 中引入的迭代器。在这个上下文中经常使用foreach语句。要了解关于这些内置功能的更多信息,请参考 https://docs.microsoft.com/en-us/dotnet/csharp/iterators

  • 如果你熟悉 Java,可能用过 Java 内置的Iterator接口,java.util.Iterator。这种模式用于像java.util.Iteratorjava.util.Enumeration这样的接口。

履行

类似于我们现实世界的例子,让我们假设有一个学院有两个部门:科学和艺术。艺术系使用数组数据结构来维护其课程细节,但科学系使用链表数据结构来保持不变。行政部门不干涉一个部门如何维护这些细节。它只是对从每个部门获取数据感兴趣,并希望统一访问这些数据。现在假设您是行政部门的成员,在一个新的会话开始时,您想使用迭代器来宣传课程表。让我们看看如何在接下来的演示中实现它。

让我们假设您有一个名为IIterator ,的迭代器,它在接下来的例子中充当公共接口,它目前支持四个基本方法:First(), Next(), CurrentItem()IsCollectionEnds(),如下所示。

  • 在开始遍历数据结构之前,First()方法将指针重置为指向第一个元素。

  • Next()方法返回容器中的下一个元素。

  • CurrentItem()方法返回迭代器在特定时间指向的容器的当前元素。

  • IsCollectionEnds()验证下一个元素是否可用于进一步处理。所以,这个方法帮助你决定你是否已经到达了你的容器的末端。

这些方法在每个ScienceIteratorArtsIterator类中实现。您将看到CurrentItem()方法在ScienceIteratorArtIterator类中有不同的定义。同样,为了打印课程表,我只使用了其中的两种方法:IsCollectionEnds()Next()。如果您愿意,可以尝试剩下的两种方法,First()currentItem()。我提到了这四种方法,并为它们提供了一些示例实现,因为它们在迭代器模式实现中非常常见。这些示例实现也可以帮助您理解这些示例。

Point to Note

如果你只考虑理科或文科,程序的代码长度可以减半。但是我保留了它们,向您展示迭代器模式可以帮助您在不知道数据在内部是如何存储的情况下进行遍历。对于科学,主题存储在一个链表中,但是对于艺术,主题存储在一个数组中。不过,通过使用这种模式,您可以以统一的方式遍历和打印主题。

类图

图 18-2 显示了类图。

img/463942_2_En_18_Fig2_HTML.jpg

图 18-2

类图

解决方案资源管理器视图

图 18-3 显示了程序的高层结构。这是一个很大的程序,很难在一个屏幕截图中容纳所有的内容,所以我只扩展了科学部门的细节。

img/463942_2_En_18_Fig3_HTML.jpg

图 18-3

解决方案资源管理器视图

演示 1

下面是实现。

using System;
using System.Collections.Generic;
using System.Linq;

namespace IteratorPattern
{
    #region Iterator
    public interface IIterator
    {
        // Reset to first element
        void First();
        // Get next element
        string Next();
        // End of collection check
        bool IsCollectionEnds();
        // Retrieve Current Item
        string CurrentItem();
    }

    /// <summary>
    ///  ScienceIterator
    /// </summary>
    public class ScienceIterator : IIterator
    {
        private LinkedList<string> Subjects;
        private int position;

        public ScienceIterator(LinkedList<string> subjects)
        {
            this.Subjects = subjects;
            position = 0;
        }

        public void First()
        {
            position = 0;
        }

        public string Next()
        {
            return Subjects.ElementAt(position++);
        }

        public bool IsCollectionEnds()
        {
            if (position < Subjects.Count)
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        public string CurrentItem()
        {
            return Subjects.ElementAt(position);
        }
    }
    /// <summary>
    ///  ArtsIterator
    /// </summary>
    public class ArtsIterator : IIterator
    {
        private string[] Subjects;
        private int position;
        public ArtsIterator(string[] subjects)
        {
            this.Subjects = subjects;
            position = 0;
        }
        public void First()
        {
            position = 0;
        }

        public string Next()
        {
            //Console.WriteLine("Currently pointing to the subject: "+ this.CurrentItem());
            return Subjects[position++];
        }

        public bool IsCollectionEnds()
        {
            if (position >= Subjects.Length)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public string CurrentItem()
        {
            return Subjects[position];
        }
    }
    #endregion

    #region Aggregate

    public interface ISubjects
    {
        IIterator CreateIterator();
    }
    public class Science : ISubjects
    {
        private LinkedList<string> Subjects;

        public Science()
        {
            Subjects = new LinkedList<string>();
            Subjects.AddFirst("Mathematics");
            Subjects.AddFirst("Computer Science");
            Subjects.AddFirst("Physics");
            Subjects.AddFirst("Electronics");
        }

        public IIterator CreateIterator()
        {
            return new ScienceIterator(Subjects);
        }
    }
    public class Arts : ISubjects
    {
        private string[] Subjects;

        public Arts()
        {
            Subjects = new[] { "English", "History", "Geography", "Psychology" };
        }

        public IIterator CreateIterator()
        {
            return new ArtsIterator(Subjects);
        }
    }
    #endregion

    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {

            Console.WriteLine("***Iterator Pattern Demonstration.***");
            // For Science
            ISubjects subjects= new Science();
            IIterator iterator = subjects.CreateIterator();
            Console.WriteLine("\nScience subjects :");
            Print(iterator);

            // For Arts
            subjects = new Arts();
            iterator = subjects.CreateIterator();
            Console.WriteLine("\nArts subjects :");
            Print(iterator);

            Console.ReadLine();
        }
        public static void Print(IIterator iterator)
        {
            while (!iterator.IsCollectionEnds())
            {
                Console.WriteLine(iterator.Next());
            }
        }
    }

}

输出

这是输出。

***Iterator Pattern Demonstration.***

Science subjects :
Electronics
Physics
Computer Science
Mathematics

Arts subjects :
English
History
Geography
Psychology

Note

您可以在一个实现中使用两种或多种不同的数据结构来展示这种模式的强大功能。您已经看到,在前面的演示中,我将First (), Next(), IsCollectionEnds(), and CurrentItem()方法用于不同的实现,这些实现因其内部数据结构而异。

注释代码中还显示了CurrentItem()的一种用法。如果您想测试它,您可以取消注释该行。

演示 2

现在让我们看看另一个实现,它使用了C#对迭代器模式的内置支持。我使用了IEnumerable接口,所以不需要定义自定义迭代器。但是要使用这个接口,你需要在程序的开头包含下面一行。

using System.Collections;

如果您看到 Visual Studio 中的定义,它描述了以下内容。

//
// Summary:
//     Exposes an enumerator, which supports a simple iteration over a //     non-generic collection.
[NullableContextAttribute(1)]
public interface IEnumerable
{
 //
 // Summary:
 //     Returns an enumerator that iterates through a collection.
 //
 // Returns:
 //  An System.Collections.IEnumerator object that can be used to iterate
 //  through the collection.
 IEnumerator GetEnumerator();
 }

因此,您可以很容易地预测每个具体的迭代器需要实现GetEnumerator()方法。在下面的实现(演示 2)中,两个具体的迭代器都将其定义如下。

public IEnumerator GetEnumerator()
{
 foreach( string subject in Subjects)
  {
    yield return subject;
  }
}

你可能会对yield return感到好奇。微软在 https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield 讨论。

在语句中使用 yield 上下文关键字时,表示出现该关键字的方法、操作符或 get 访问器是迭代器。当您为自定义集合类型实现IEnumerableIEnumerator模式时,使用 yield 定义迭代器可以消除对显式额外类(保存枚举状态的类,例如参见IEnumerator)的需求。

使用 yield return 语句一次返回一个元素。迭代器方法返回的序列可以通过使用foreach语句或 LINQ 查询来消耗。foreach 循环的每次迭代都调用 iterator 方法。当在迭代器方法中到达 yield return 语句时,返回 expression,并保留代码中的当前位置。下次调用迭代器函数时,从该位置重新开始执行。??

这些评论不言自明。简而言之,GetEnumeratorforeach可以记住上一个yield return之后的位置,并可以给你下一个值。在接下来的演示中,剩余的代码很容易理解。由于整体概念和意图与演示 1 相似,现在可以直接跳到演示 2。下面是完整的实现。

using System;
using System.Collections;
using System.Collections.Generic;

namespace SimpleIterator
{
    public class Arts : IEnumerable
    {
        private string[] Subjects;

        public Arts()
        {
            Subjects = new[] { "English", "History", "Geography", "Psychology" };
        }

        public IEnumerator GetEnumerator()
        {
            foreach (string subject in Subjects)
            {
                yield return subject;
            }
        }
    }

    public class Science : IEnumerable
    {
        private LinkedList<string> Subjects;

        public Science()
        {
            Subjects = new LinkedList<string>();
            Subjects.AddFirst("Mathematics");
            Subjects.AddFirst("Computer Science");
            Subjects.AddFirst("Physics");
            Subjects.AddFirst("Electronics");
        }

        public IEnumerator GetEnumerator()
        {
            foreach (string subject in Subjects)
            {
                yield return subject;
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Iterator Pattern.A simple demonstration using built-in constructs.***");
            Arts artsPapers = new Arts();
            Console.WriteLine("\nArts subjects are as follows:");
            /*
              Consume values from the
              collection's GetEnumerator()
             */
            foreach (string subject in artsPapers)
            {
                Console.WriteLine(subject);
            }

            Science sciencePapers = new Science();
            Console.WriteLine("\nScience subjects are as follows:");
            /*
              Consume values from the
              collection's GetEnumerator()
             */
            foreach (string subject in sciencePapers)
            {
                Console.WriteLine(subject);
            }

        }
    }
}

输出

这是输出。

***Iterator Pattern.A simple demonstration using built-in constructs.***

Arts subjects are as follows:
English
History
Geography
Psychology

Science subjects are as follows:
Electronics
Physics
Computer Science
Mathematics

问答环节

18.1 迭代器模式是用来做什么的?

下面讨论它的一些用法。

  • 你可以遍历一个对象结构而不知道它的内部细节。因此,如果您有一个不同子集合的集合(例如,您的容器混合了数组、列表、链表等等),您仍然可以遍历整个集合,并以一种通用的方式处理元素,而不需要知道它们之间的内部细节或差异。

  • 您可以用不同的方式遍历集合。如果设计得当,多个遍历也可以并行进行。

18.2 与此模式相关的关键 挑战 有哪些?

您必须确保在遍历过程中没有发生意外的修改。

但是要应对前面提到的挑战,你可以简单地做个备份,然后继续。我说得对吗?

进行备份并在以后重新检查是一项成本高昂的操作。

18.4 在代码中,我看到一个区域名为 Aggregate 。这个名字背后有什么原因吗?

一个集合定义了一个接口来创建一个Iterator对象。我采用了 GoF 书里的名字。

在整个讨论中,你都谈到了收藏。什么是收藏?

在 C# 中,当您管理(或创建)一组相关的对象时,您有以下选择。

  • 可以考虑数组。

  • 可以考虑收藏。

在许多情况下,集合是首选,因为它们可以动态增长或收缩。在某些集合中,您甚至可以为对象分配键,以便在以后的阶段使用这些键更有效地检索它们。(例如,字典就是这样一个集合,通常用于快速查找。)最后,集合是一个类,所以在向它添加元素之前,需要创建实例。这里有一个例子。

LinkedList<string> Subjects = new LinkedList<string>();
Subjects.AddLast("Maths");
Subjects.AddLast("Comp. Sc.");
Subjects.AddLast("Physics");

在这个例子中,我没有使用AddFirst()方法,而是使用了AddLast()方法作为变体。这两种方法都可用,并且内置在 C# 中。AddLast()方法在LinkedList<T>,的末尾添加节点,而AddFirst()方法在LinkedList<T>.的开头添加节点

在这个实现中,你可以简单地考虑使用科学或艺术科目来演示迭代器模式的实现,并减少代码大小。这是正确的吗?

是的,我之前提到过。但是当您使用两种不同的数据结构时,您可能会看到迭代器设计模式的真正威力。所以,我把它们都留在了这里。

十九、备忘录模式

这一章涵盖了备忘录模式。

GoF 定义

在不违反封装的情况下,捕获并具体化一个对象的内部状态,以便该对象可以在以后恢复到这个状态。

概念

单词 memento 是对过去事件的提醒。通过遵循面向对象的方法,您还可以跟踪(或保存)对象的状态。因此,每当您想要将对象恢复到它以前的状态时,您可以考虑使用这种模式。

在这种模式中,您通常会看到三个参与者:备忘录、发起人和看管人(通常用作客户)。工作流程可以概括如下:发起者对象有一个内部状态,客户端可以在其中设置一个状态。为了保存发起者的当前内部状态,客户(或看护者)向其请求备忘录。客户端还可以将备忘录(它持有的)传递回发起者以恢复先前的状态。通过遵循正确的方法,这些保存和恢复操作不会违反封装。

真实世界的例子

您可以在有限状态机的状态中看到 Memento 模式的经典示例。这是一个数学模型,但它最简单的应用之一是十字转门。一个十字转门有一些旋转臂,最初是锁定的。当你穿过它的时候(比如放一些硬币进去),锁是开着的,手臂可以转动。一旦你通过,手臂回到锁定状态。

计算机世界的例子

在绘图应用中,您可能需要恢复到较旧的状态。此外,在数据库事务中,您可能需要回滚一些特定的事务。备忘录模式可以用在这些场景中。

履行

以下是 GoF 的一些重要建议。

  • 备忘录保存了发起者的内部状态。

  • 只有发起者应该创建备忘录。稍后,它可以使用备忘录来恢复先前的内部状态。

  • 看守类是备忘录的容器。这个类用于保存备忘录,但是它从不操作或检查备忘录的内容。管理员可以从发起者那里得到备忘录。

Note

在这种模式中,发起者看到的是宽接口,而管理者看到的是窄接口。管理员不允许对备忘录做任何改动。因此,memento 对象应该用作不透明对象。

memento 设计模式可以使用不同的技术实现不同的实现。在本章中,您将看到两个演示。演示 1 相对简单易懂。但是在演示 2 中有所改进。在这两个实现中,我没有使用单独的看守类;相反,我使用客户机代码来扮演看管者的角色。

在演示 1 中,看护者拿着一个Originator物体,并向其索要备忘录。它将备忘录保存在一个列表中。因此,您会在客户端中看到下面几行代码。

Originator originatorObject = new Originator();
Memento currentMemento;
IList<Memento> savedStates = new List<Memento>();
/*
Adding a memento the list. This memento stores
the current state of the Originator.
*/
savedStates.Add(originatorObject.CurrentMemento());

memento 类非常简单,它有一个简单的 getter-setter 来获取或设置发起者的state。类如下。

class Memento
    {
        private string state;
        public string State
        {
            get
            {
                return state;
            }
            set
            {
                state = value;
            }
        }
    }

Note

从 C# 3.0 开始,您可以通过使用自动属性(如公共字符串状态{ get 设置;}.

除了状态之外,Originator类还有一个构造函数和两个名为CurrentMemento()RestoreMemento(...)的方法。第一种是响应看管人的请求提供备忘录,定义如下。

       public Memento CurrentMemento()
        {
            myMemento = new Memento();
            myMemento.State = state;
            return myMemento;
        }

第二种方法将发起者恢复到以前的状态。这种状态包含在来自管理员的备忘录(作为方法参数出现)中。管理员可以发送它之前保存的备忘录。该方法定义如下。

        public void RestoreMemento(Memento restoreMemento)
        {
          this.state = restoreMemento.State;
          Console.WriteLine($"Restored to state : {state}");
        }

剩下的代码很简单,但是请参考注释以获得更好的理解。

类图

图 19-1 为类图。

img/463942_2_En_19_Fig1_HTML.jpg

图 19-1

类图

解决方案资源管理器视图

图 19-2 显示了程序的高层结构。

img/463942_2_En_19_Fig2_HTML.jpg

图 19-2

解决方案资源管理器视图

演示 1

下面是实现。

using System;
using System.Collections.Generic;

namespace MementoPattern
{
/// <summary>
/// Memento class
/// As per GoF:
/// 1.A Memento object stores the snapshot of Originator's  /// internal state.
/// 2.Ideally,only the originator that created a memento is /// allowed to access it.
/// </summary>
    class Memento
    {
        private string state;
        public string State
        {
            get
            {
                return state;
            }
            set
            {
                state = value;
            }
        }
        /*
        C#3.0 onwards, you can use
        automatic properties as follows:
        public string State { get; set; }
        */

    }

///  <summary>
///  Originator class
///  As per GoF:
///  1.It creates a memento that contains a snapshot of
///  its current internal state.
///  2.It uses a memento to restore its internal state.
///  </summary>
    class Originator
    {
        private string state;
        Memento myMemento;
        public Originator()
        {
            //Creating a memento with born state.
            state = "Snapshot #0.(Born state)";
            Console.WriteLine($"Originator's current state is: {state}");

        }
        public string State
        {
            get { return state; }
            set
            {
                state = value;
                Console.WriteLine($"Originator's current state is: {state}");
            }
        }

        /*
        Originator will supply the memento
        (which contains it's current state)
        in respond to caretaker's request.
        */
        public Memento CurrentMemento()
        {
            myMemento = new Memento();
            myMemento.State = state;
            return myMemento;
        }

        // Back to an old state (Restore)
        public void RestoreMemento(Memento restoreMemento)
        {
            this.state = restoreMemento.State;
            Console.WriteLine($"Restored to state : {state}");
        }
    }

/// <summary>
/// The 'Caretaker' class.
/// As per GoF:
/// 1.This class is responsible for memento's safe-keeping.
/// 2.Never operates or Examines the content of a Memento.

/// Additional notes( for your reference):
/// The originator object has an internal state, and a client can set a /// state in it.A client(or, caretaker) requests a memento from the /// originator to save the current internal state of the originator). /// It can also pass a memento back to the originator to restore it /// to a previous state that the memento holds in it.This enables to save /// and restore the internal state of an originator without violating its /// encapsulation.
/// </summary>

    class Client
    {
        static Originator originatorObject;
        static Memento currentMemento;
        static void Main(string[] args)
        {
            Console.WriteLine("***Memento Pattern Demonstration-1.***\n");
            //Originator is initialized.The constructor will create a born state.
            originatorObject = new Originator();
            //Memento currentMemento;
            IList<Memento> savedStates = new List<Memento>();
            /*
             Adding a memento the list.This memento stores
             the current state of the Origintor.
            */
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #1.
            originatorObject.State = "Snapshot #1";
            //Adding this memento as a  restore point
             savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #2.
            originatorObject.State = "Snapshot #2";
            //Adding this memento as a  restore point
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #3.
            originatorObject.State = "Snapshot #3";
            //Adding this memento as a  restore point
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #4\. It is not added as a restore point.
            originatorObject.State = "Snapshot #4";

            //Available restore points
            Console.WriteLine("\nCurrently available restore points are :");
            foreach (Memento m in savedStates)
            {
                Console.WriteLine(m.State);
            }

            //Undo's
            //Roll back starts...
            Console.WriteLine("\nPerforming undo's now.");
            for (int i = savedStates.Count; i > 0; i--)
            {
                //Get a restore point
                currentMemento = savedStates[i - 1];
                originatorObject.RestoreMemento(currentMemento);
            }
            //Redo's
            Console.WriteLine("\nPerforming redo's now.");
            for (int i = 1; i < savedStates.Count; i++)
            {
                currentMemento = savedStates[i];
                originatorObject.RestoreMemento(currentMemento);
            }
            // Wait for user
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Memento Pattern Demonstration-1.***

Originator's current state is: Snapshot #0.(Born state)
Originator's current state is: Snapshot #1
Originator's current state is: Snapshot #2
Originator's current state is: Snapshot #3
Originator's current state is: Snapshot #4

Currently available restore points are :
Snapshot #0.(Born state)
Snapshot #1
Snapshot #2
Snapshot #3

Performing undo's now.
Restored to state : Snapshot #3
Restored to state : Snapshot #2
Restored to state : Snapshot #1
Restored to state : Snapshot #0.(Born state)

Performing redo's now.
Restored to state : Snapshot #1
Restored to state : Snapshot #2
Restored to state : Snapshot #3

分析

使用这个程序的概念,您可以使用三种不同的撤销操作,如下所示。

  • 您可以回到上一个还原点。

  • 您可以返回到指定的还原点(直接使用 index 属性)。例如,要直接返回到快照#2,可以使用下面几行代码:

    //Directly going back to Snapshot #2
     currentMemento = savedStates[2];
     originatorObject.RestoreMemento(currentMemento);
    
    
  • 您可以恢复所有还原点(使用一个for循环和一个索引属性显示)

Note

如果应用使用 Memento 模式,并且有一个可变引用类型的状态,您可能会看到深度复制技术的实现将状态存储在 Memento 对象中。你在第二章学到了深度复制。

问答环节

在前面的例子中,你能使用一个非泛型版本吗,比如 ArrayList?

我喜欢听从专家的建议,他们通常更喜欢通用版本而不是非通用版本。这就是为什么我喜欢数据结构,比如ListDictionary等等,而不是它们的对应物,比如ArrayListHashTable。我在我早期的两本书里详细讨论了泛型:交互式 C# (Apress,2017)和高级 C# 入门(Apress,2020)。

使用 Memento 设计模式的主要优势是什么?

以下是一些优点。

  • 最大的优点是您可以随时丢弃不需要的更改,并将它们恢复到预期的或稳定的状态。

  • 您不会损害与参与此模型的关键对象相关联的封装。

  • 你可以保持很高的凝聚力。

  • 它提供了一种简单的恢复技术。

19.3 备忘录设计模式的主要挑战是什么?

以下是一些缺点。

  • 拥有更多备忘录需要更多的存储空间。此外,它们给看护者增加了额外的负担。

  • 前一点增加了维护成本。

  • 您不能忽略保存这些状态所花费的时间,这会降低应用的整体性能。

请注意,在 C# 或 Java 等语言中,开发人员可能更喜欢使用序列化/反序列化技术,而不是直接实现 Memento 设计模式。这些技术各有利弊,但是您可以在应用中结合使用这两种技术。

我很困惑。为了支持 撤销操作 ,我应该使用哪种模式——Memento 还是 Command?

GoF 说这些是相关的模式。这主要取决于你想如何处理这种情况。假设你正在给一个整数加 25。在此添加操作之后,您可以通过执行反向操作来撤消它。简单来说,50 + 25 = 75,所以 75–25 = 50。在这种类型的操作中,您不需要存储以前的状态。

但是考虑一种情况,您需要在操作之前存储对象的状态。在这种情况下,您使用 Memento。例如,在绘画应用中,通过在执行命令之前存储对象列表,可以避免撤销某些绘画操作的成本。这个存储的列表可以作为备忘录,您可以将这个列表与相关的命令一起保存。类似的概念也适用于一个长期运行的游戏应用,它有多个级别,您可以在其中保存您最后的性能级别。因此,应用可以使用这两种模式来支持撤销操作。

最后,您必须记住,在 memento 模式中存储 Memento 对象是强制性的,这样您就可以恢复到以前的状态。在命令模式中,没有必要存储命令。一旦你执行一个命令,它的工作就完成了。如果您不支持“撤销”操作,您可能根本不会对存储这些命令感兴趣。

我明白管理员不应该在备忘录上做手术。所以,演示 1 没问题。但是我看到在客户端代码中,我可以使用下面几行代码创建一个 Memento 对象并设置一个状态,没有人阻止我。这是正确的吗?

//For Q&A session only(Shouldn't be used)
currentMemento = new Memento();
currentMemento.State = "Arbitrary state set by caretaker";

接得好。这是演示 1 的潜在缺点。对于管理员类,试着记住 GoF 中的以下几点。

  • 这个班负责备忘录的保管。

  • 它从不操作或检查备忘录的内容。

在演示 2 中,我注意到了这几点。所以,穿过它;这是一个相对复杂的例子。

修改的实现

在这个例子中,我试图阻止从客户端代码直接访问备忘录。以下是一些重要的变化。

  • Memento类有一个私有构造函数。因此,这个类不能使用外部的new操作符初始化。

  • Memento类嵌套在Originator类中,放在一个单独的文件中(Originator.cs)。我还制作了Mementointernal

  • 为了适应这些变化,CurrentMemento()方法修改如下:

    public Memento CurrentMemento()
    {
            //Code segment used in Demonstration-1
            //myMemento = new Memento();//error now
            //myMemento.State = state;
            //return myMemento;
    
            //Modified code for Demonstration-2
            return new Memento(this.State);
    }
    
    

看守者(客户端)与演示 1 非常相似,除了这一次,您需要使用发起者。备忘录而不是Memento。现在我们来看演示 2。

类图

图 19-3 显示了修改后的类图。(请注意,关联线可以连接到最外面的形状,但不能连接到 Visual Studio 类图中的嵌套类型。)

img/463942_2_En_19_Fig3_HTML.jpg

图 19-3

演示 2 的类图

解决方案资源管理器视图

图 19-4 显示了修改后的程序高层结构。

img/463942_2_En_19_Fig4_HTML.jpg

图 19-4

演示 2 的解决方案浏览器视图

演示 2

下面是修改后的实现。

//Originator.cs
using System;

namespace MementoPatternDemo2
{
    /// <summary>
    ///  Originator class
    ///  As per GoF:
    ///  1.It creates a memento that contains a snapshot of its current ///  internal state.
    ///  2.It uses a memento to restore its internal state.
    /// </summary>
    class Originator
    {
        private string state;
        //Memento myMemento;//not needed now
        public Originator()
        {
            //Creating a memento with born state.
            state = "Snapshot #0.(Born state)";
            Console.WriteLine($"Originator's current state is: {state}");

        }
        public string State
        {
            get { return state; }
            set
            {
                state = value;
                Console.WriteLine($"Originator's current state is: {state}");
            }
        }

        /*
        Originator will supply the memento
        (which contains it's current state)
        in respond to caretaker's request.
        */
        public Memento CurrentMemento()
        {
            //Code segment used in Demonstration-1
            //myMemento = new Memento();//error now, because of private constructor
            //myMemento.State = state;
            //return myMemento;

            //Modified code for Demonstration-2
            return new Memento(this.State);
        }

        // Back to an old state (Restore)
        public void RestoreMemento(Memento restoreMemento)
        {
            this.state = restoreMemento.State;
            Console.WriteLine($"Restored to state : {state}");
        }
        /// <summary>
        /// Memento class
        /// As per GoF:
        /// 1.A Memento object stores the snapshot of Originator's internal /// state.
        /// 2.Ideally,only the originator that created a memento is allowed /// to access it.
        /// </summary>
        internal class Memento
        {
            private string state;
            //Now Memento class cannot be initialized outside
            private Memento() { }
            public Memento(string state)
            {
                this.state = state;
            }
            public string State
            {
                get
                {
                    return state;
                }
                set
                {
                    state = value;
                }
            }
        }

    }
}
//Client.cs
using System;
using System.Collections.Generic;

namespace MementoPatternDemo2
{
    class Client
    {
        static Originator originatorObject;
        static Originator.Memento currentMemento;
        static void Main(string[] args)
        {
            Console.WriteLine("***Memento Pattern Demonstration-2.***");
            Console.WriteLine("Originator (with nested internal class 'Memento') is maintained in a separate file.\n");
            //Originator is initialized.The constructor will create a //born state.
            originatorObject = new Originator();
            //Cannot create memento inside client code now
            //currentMemento = new Originator.Memento();//error:inaccessible
            //currentMemento.State = "test";//Also error, because previous line cannot be used

            IList<Originator.Memento> savedStates = new List<Originator.Memento>();
            /*
             Adding a memento the list.This memento stores
             the current state of the Origintor.
            */
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #1.
            originatorObject.State = "Snapshot #1";
            //Adding this memento as a  restore point
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #2.
            originatorObject.State = "Snapshot #2";
            //Adding this memento as a  restore point
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #3.
            originatorObject.State = "Snapshot #3";
            //Adding this memento as a  restore point
            savedStates.Add(originatorObject.CurrentMemento());

            //Snapshot #4\. It is not added as a restore point.
            originatorObject.State = "Snapshot #4";

            //Available restore points
            Console.WriteLine("\nCurrently available restore points are :");
            foreach (Originator.Memento m in savedStates)
            {
                Console.WriteLine(m.State);
            }

            //Undo's
            //Roll back starts...
            Console.WriteLine("\nPerforming undo's now.");
            for (int i = savedStates.Count; i > 0; i--)
            {
                //Get a restore point
                currentMemento = savedStates[i - 1];
                originatorObject.RestoreMemento(currentMemento);
            }
            //Redo's
            Console.WriteLine("\nPerforming redo's now.");
            for (int i = 1; i < savedStates.Count; i++)
            {
                currentMemento = savedStates[i];
                originatorObject.RestoreMemento(currentMemento);
            }
            // Wait for user
            Console.ReadKey();
        }
    }
}

输出

这是输出。您可以看到,除了最初的控制台消息之外,演示 1 和演示 2 的输出是相同的,但是从程序上来说,我在这个示例中加入了更多的约束。

***Memento Pattern Demonstration-2.***
Originator (with nested internal class 'Memento') is maintained in a separate file.

Originator's current state is: Snapshot #0.(Born state)
Originator's current state is: Snapshot #1
Originator's current state is: Snapshot #2
Originator's current state is: Snapshot #3
Originator's current state is: Snapshot #4

Currently available restore points are :
Snapshot #0.(Born state)
Snapshot #1
Snapshot #2
Snapshot #3

Performing undo's now.
Restored to state : Snapshot #3
Restored to state : Snapshot #2
Restored to state : Snapshot #1
Restored to state : Snapshot #0.(Born state)

Performing redo's now.
Restored to state : Snapshot #1
Restored to state : Snapshot #2
Restored to state : Snapshot #3

二十、状态模式

本章介绍了状态模式。

GoF 定义

允许对象在其内部状态改变时改变其行为。该对象看起来会改变它的类。

概念

GoF 的定义很容易理解。它简单地说明了一个对象可以根据它的当前状态改变它的行为。

假设您正在处理一个代码库快速增长的大规模应用。结果,情况变得复杂,您可能需要引入许多 if-else 块/switch 语句来保护各种条件。状态模式适合这样的环境。它允许您的对象基于它们的当前状态表现出不同的行为,并且您可以用不同的类定义特定于状态的行为。

在这种模式中,您根据应用的可能状态进行思考,并相应地分离代码。理想情况下,每个状态都独立于其他状态。您跟踪这些状态,并且您的代码根据当前状态的行为做出响应。例如,假设您正在电视机(TV)上观看一个节目。现在,如果您按下电视遥控器上的静音按钮,电视的状态会发生变化。但是如果电视已经处于关闭模式,则没有变化。

因此,基本思想是,如果您的代码可以跟踪应用的当前状态,您就可以集中任务,分离您的代码,并相应地做出响应。

真实世界的例子

考虑一个网络连接的场景,比如 TCP 连接。一个对象可以处于各种状态;例如,连接可能刚刚建立,连接可能已关闭,或者对象正在通过连接进行侦听。当这个连接收到来自其他对象的请求时,它会根据其当前状态做出响应。

交通信号或电视的功能是状态模式的其他例子。例如,如果电视已经处于开机模式,您可以更换频道。如果它处于关闭模式,它不响应频道改变请求。

计算机世界的例子

TCP 连接的例子就属于这一类。考虑另一个例子。假设您有一个作业处理系统,可以一次处理一定数量的作业。当一个新的作业出现时,系统要么处理该作业,要么发出信号表明它正忙于处理当时能够处理的最大数量的作业。这个忙信号仅仅表明它的作业处理能力总数已经达到,新的作业请求不能立即完成。

履行

这个例子模拟了与电视相关的功能,它有一个控制面板来支持开、关和静音操作。为简单起见,假设在任何给定时间,电视处于以下三种状态中的任何一种:开、关或静音。下面显示了一个名为 IPossibleStates 的接口。

   interface IPossibleStates
    {
        //Users can press any of these buttons-On, Off or Mute
        void PressOnButton(TV context);
        void PressOffButton(TV context);
        void PressMuteButton(TV context);
    }

三个具体的类——OnOffMute——实现了这个接口。基本功能可以描述如下。最初,电视处于关闭状态。因此,当您按下控制面板上的“开”按钮时,电视将进入“开”状态,如果您按下“静音”按钮,电视将进入静音状态。

假设您在电视处于关闭状态时按下关闭按钮;如果您在电视处于打开状态时按下 On 按钮;或者,如果您在电视处于静音模式时按下静音按钮,电视的状态不会改变。电视可以从打开状态或静音状态进入关闭状态(当您按下关闭按钮时)。图 20-1 是反映所有可能场景的状态图。

img/463942_2_En_20_Fig1_HTML.jpg

图 20-1

电视的不同状态

Points to Remember

  • 在该图中,我没有将任何状态标记为最终状态,尽管在图 20-1 中,我切换到关闭电视。

  • 为了使设计更简单,假设如果在电视处于关闭状态时按下关闭(或静音)键;或者如果您在电视处于打开状态时按下 On 按钮;或者,如果您在电视处于静音模式时按下静音按钮,电视的状态不会改变。但在现实世界中,遥控器的工作方式可能会有所不同。例如,如果电视当前处于打开状态,您按下静音按钮,电视将进入静音模式;如果再次按下静音按钮,电视可能会返回到打开状态。因此,您可能需要相应地更新您的程序逻辑。

电视有一个控制面板,支持开、关和静音操作。所以,在 TV 类内部,有三种方法:ExecuteOffButton() , ExecuteOnButton(),和ExecuteMuteButton()如下。

       public void ExecuteOffButton()
        {
           Console.WriteLine("You pressed Off button.");
            //Delegating the state behavior
            currentState.PressOffButton(this);
        }
        public void ExecuteOnButton()
        {
            Console.WriteLine("You pressed On button.");
            //Delegating the state behavior
            currentState.PressOnButton(this);
        }
        public void ExecuteMuteButton()
        {
            Console.WriteLine("You pressed Mute button.");
            //Delegating the state behavior
            currentState.PressMuteButton(this);
        }

我授权国家行为。例如,当您按下ExecuteMuteButton()时,控件会根据电视机的当前状态调用PressMuteButton(...)

现在让我们跟随类图。

类图

图 20-2 显示了类图的重要部分。

img/463942_2_En_20_Fig2_HTML.jpg

图 20-2

类图

解决方案资源管理器视图

图 20-3 显示了程序的高层结构。

img/463942_2_En_20_Fig3_HTML.jpg

图 20-3

解决方案资源管理器视图

示范

下面是完整的实现。

using System;
namespace StatePattern
{
    interface IPossibleStates
    {
        //Users can press any of these buttons-On, Off or Mute
        void PressOnButton(TV context);
        void PressOffButton(TV context);
        void PressMuteButton(TV context);
    }
    //Subclasses does not contain any local state.
    //Only one unique instance of IPossibleStates is required.
    /// <summary>
    /// Off state behavior
    /// </summary>
    class Off : IPossibleStates
    {
        public Off()
        {
            Console.WriteLine("---TV is Off now.---\n");
        }

        //TV is Off now, user is pressing On button
        public void PressOnButton(TV context)
        {
            Console.WriteLine("TV was Off.Going from Off to On state.");
            context.CurrentState = new On();
        }
        //TV is Off already, user is pressing Off button again
        public void PressOffButton(TV context)
        {
            Console.WriteLine("TV was already in Off state.So, ignoring this opeation.");
        }
        //TV is Off now, user is pressing Mute button
        public void PressMuteButton(TV context)
        {
            Console.WriteLine("TV was already off.So, ignoring this operation.");
        }
    }
    /// <summary>
    /// On state behavior
    /// </summary>
    class On : IPossibleStates
    {
       public On()
        {
            Console.WriteLine("---TV is On now.---\n");
        }
        //TV is On already, user is pressing On button again
        public void PressOnButton(TV context)
        {
            Console.WriteLine("TV is already in On state.Ignoring repeated on button press operation.");
        }
        //TV is On now, user is pressing Off button
        public void PressOffButton(TV context)
        {
            Console.WriteLine("TV was on.So,switching off the TV.");
            context.CurrentState = new Off();
        }
        //TV is On now, user is pressing Mute button
        public void PressMuteButton(TV context)
        {
            Console.WriteLine("TV was on.So,moving to silent mode.");
            context.CurrentState = new Mute();
        }
    }
    /// <summary>
    /// Mute state behavior
    /// </summary>
    class Mute : IPossibleStates
    {

        public Mute()
        {
            Console.WriteLine("---TV is in Mute mode now.---\n");
        }
        /*
        Users can press any of these buttons at this state-On, Off or Mute.TV is in mute, user is pressing On button.
        */
        public void PressOnButton(TV context)
        {
            Console.WriteLine("TV was in mute mode.So, moving to normal state.");
            context.CurrentState = new On();
        }
        //TV is in mute, user is pressing Off button
        public void PressOffButton(TV context)
        {
            Console.WriteLine("TV was in mute mode. So, switching off the TV.");
            context.CurrentState = new Off();
        }
        //TV is in mute already, user is pressing mute button again
        public void PressMuteButton(TV context)
        {
            Console.WriteLine(" TV is already in Mute mode, so, ignoring this operation.");
        }
    }
    /// <summary>
    /// TV is the context class
    /// </summary>
    class TV
    {
        private IPossibleStates currentState;
        public IPossibleStates CurrentState
        {
            get
            {
                return currentState;
            }
           /*
           Usually this value will be set by the class that implements the interface "IPossibleStates"
           */
            set
            {
                currentState = value;
            }
        }
        public TV()
        {
            //Starting with Off state
            this.currentState = new Off();
        }
        public void ExecuteOffButton()
        {
           Console.WriteLine("You pressed Off button.");
            //Delegating the state behavior
            currentState.PressOffButton(this);
        }
        public void ExecuteOnButton()
        {
            Console.WriteLine("You pressed On button.");
            //Delegating the state behavior
            currentState.PressOnButton(this);
        }
        public void ExecuteMuteButton()
        {
            Console.WriteLine("You pressed Mute button.");
            //Delegating the state behavior
            currentState.PressMuteButton(this);
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***State Pattern Demo***\n");
            //TV is initialized with Off state.
            TV tv = new TV();
            Console.WriteLine("User is pressing buttons in the following sequence:");
            Console.WriteLine("Off->Mute->On->On->Mute->Mute->Off\n");
            //TV is already in Off state
            tv.ExecuteOffButton();
  //TV is already in Off state, still pressing the Mute button
            tv.ExecuteMuteButton();
            //Making the TV on
            tv.ExecuteOnButton();
  //TV is already in On state, pressing On button again
            tv.ExecuteOnButton();
            //Putting the TV in Mute mode
            tv.ExecuteMuteButton();
     //TV is already in Mute, pressing Mute button again
            tv.ExecuteMuteButton();
            //Making the TV off
            tv.ExecuteOffButton();
            // Wait for user
            Console.Read();
        }
    }
}

输出

这是输出。

***State Pattern Demo***

---TV is Off now.---

User is pressing buttons in the following sequence:
Off->Mute->On->On->Mute->Mute->Off

You pressed Off button.
TV was already in Off state.So, ignoring this opeation.
You pressed Mute button.
TV was already off.So, ignoring this operation.
You pressed On button.
TV was Off.Going from Off to On state.
---TV is On now.---

You pressed On button.
TV is already in On state.Ignoring repeated on button press operation.
You pressed Mute button.
TV was on.So,moving to silent mode.
---TV is in Mute mode now.---

You pressed Mute button.
 TV is already in Mute mode, so, ignoring this operation.
You pressed Off button.
TV was in mute mode. So, switching off the TV.
---TV is Off now.---

问答环节

你能详细说明这种模式在现实世界中是如何工作的吗?

心理学家已经多次证明了这样一个事实,即人类在放松的心情下可以发挥出最佳水平。然而,在相反的情况下,当他们的头脑充满紧张时,他们不能产生伟大的结果。这就是为什么他们总是建议你在放松的心情下工作。所以,同样的工作,可以是享受的,也可以是无聊的,看你现在的心情。

你可以再想想我们的演示例子。假设你想看你最喜欢的球队获胜时刻的电视直播。要观看和享受这一时刻,您需要先打开电视。如果此时电视无法正常工作,无法处于打开状态,您就无法享受这一时刻。所以,如果你想通过你的电视享受这一刻,首要的标准就是电视要把它的状态从关变成开。当对象的内部状态改变时,如果您想在对象中设计类似的行为改变,状态模式是很有帮助的。

在这个例子中,你只考虑了电视 的三种状态:开、关和静音。可以有许多其他状态;例如,可能存在处理连接问题或不同显示条件的状态。你为什么忽略了这些问题?

直截了当的回答是,为了简单起见,我忽略了这些状态。如果系统中状态的数量显著增加,那么维护系统就变得很困难(这是与这种设计模式相关的关键挑战之一)。但是如果你理解这个实现,你可以很容易地添加任何你想要的状态。

我注意到 GoF 在他们著名的著作中为国家模式和策略模式 描绘了一个相似的结构。我对此感到困惑。

是的,结构是相似的,但是你需要记住他们的意图是不同的。当你使用策略模式时,你得到了一个子类化的更好的选择。在状态设计模式中,不同类型的行为可以封装在一个状态对象中,并且上下文被委托给这些状态中的任何一个。当上下文的内部状态改变时,它的行为也会改变。因此,状态模式可以被认为是策略模式的动态版本。

在某些情况下,状态模式还可以帮助您避免许多if条件。例如,如果电视处于关闭状态,它就不能进入静音状态。从这个状态,它只能进入 On 状态。因此,如果您不喜欢状态设计模式,您可能需要像这样编写代码。

class TV
{
//Some code before
public void ExecuteOnButton()
{
if(currentState==Off )
{
Console.WriteLine("You pressed On button. Going from Off to OnState");
//Some code after
}
if(currentState==On )
{
Console.WriteLine("You pressed On button. TV is already in on state. So, ignoring this opeation.");
//Some code after
}
else
{
Console.WriteLine("TV was on. Moving into mute mode now.");
}
//Some code after
}

您需要对不同种类的按钮按压重复这些检查(例如,对于ExecuteOffButton()ExecuteMuteButton()方法,您需要重复这些检查并相应地编程)。所以,如果你不从状态的角度考虑,随着时间的推移,用大量的if-else处理不同的条件是非常具有挑战性的,当代码库持续增长时,这可能会很困难。

在你的例子中,你是如何实现 开/关原理 的?

这些 TV 状态中的每一个都被关闭进行修改,但是您可以向 TV 类添加一个新的状态。

20.5 策略模式和状态模式有什么共同特征?

状态模式可以被认为是一种动态策略模式。这两种模式都促进了组合和委托。

在我看来,这些状态对象就像单态对象一样。这是正确的吗?

是的,这是一个很好的观察。在这个例子中,IPossibleStates 的具体子类不包含任何本地状态,因此,在这个应用中,只有一个 state 实例在工作。大多数时候,这种模式的行为是相似的。

20.7 为什么使用上下文作为方法参数?你能在这样的陈述中避免它们吗?

void PressOnButton(TV context);

利用上下文,我在保存状态。此外,IPossibleStates 的具体子类不包含任何本地状态。因此,在这个应用中,只有一个状态实例在工作。所以,这个结构帮助你评估你是在不同的状态之间变化,还是已经处于相同的状态。注意输出。这些上下文帮助您获得如下输出。

"You pressed Mute button.
TV was already off.So, ignoring this operation."

20.8 状态设计模式有哪些利弊?

优点如下。

  • 您已经看到,通过遵循打开/关闭原则,您可以轻松地添加新状态和扩展状态的行为。此外,状态行为可以毫无争议地扩展。例如,在这个实现中,您可以为 TV 类添加新的状态和新的行为,而无需更改 TV 类本身。

  • 它减少了if-else语句。换句话说,条件复杂性降低了。(参见对问题 20.3 的回答。)

使用这种模式有一个缺点。

  • 状态模式也被称为状态的对象,因此您可以假设更多的状态需要更多的代码,并且明显的副作用是维护更加困难。

在这些实现中,TV 是一个具体的类。在这种情况下,你为什么不编程接口?

我假设 TV 类不会改变,所以忽略了这一部分以减少程序的代码量。但是是的,你总是可以从一个界面开始,例如,ITv,,你可以在其中定义合同。

20.10 在 TV 类的构造函数中,你正在用一个关闭的状态初始化电视。所以,状态和上下文类都可以触发状态转换?

是的。