C--设计模式-全-

C# 设计模式(全)

原文:Design Patterns in C#

协议:CC BY-NC-SA 4.0

一、单例模式

这一章涵盖了单例模式。

GoF 定义

确保一个类只有一个实例,并提供对它的全局访问点。

概念

让我们假设您有一个名为A,的类,您需要从它创建一个对象。一般情况下,你会怎么做?您可以简单地使用这一行代码:A obA=new A();

但是让我们仔细看看。如果你使用关键字new十次以上,你将有十个以上的对象,对吗?但是在真实的场景中,不必要的对象创建是一个大问题(特别是当构造函数调用非常昂贵时),所以您需要限制它。在这种情况下,单例模式就派上了用场。它限制了new的使用,并确保您没有一个以上的类实例。

简而言之,这种模式认为一个类应该只有一个实例。如果实例不可用,您可以创建一个;否则,您应该使用现有的实例来满足您的需求。通过遵循这种方法,您可以避免创建不必要的对象。

真实世界的例子

让我们假设你有一个参加比赛的运动队。您的团队需要在整个锦标赛中与多个对手比赛。在每场比赛开始时,按照比赛规则,两队队长必须掷硬币。如果你的球队没有队长,你需要选举一个人在比赛期间担任队长。在每场比赛和每一次掷硬币之前,如果你已经选举了队长,你就不能重复这个过程。

计算机世界的例子

在一些软件系统中,您可能决定只维护一个文件系统,以便可以使用它来集中管理资源。这种方法可以帮助您有效地实现缓存机制。考虑另一个例子。您还可以使用这种模式在多线程环境中维护线程池。

履行

单例模式可以通过多种方式实现。每种方法都有自己的优点和缺点。在下面的演示中,我将向您展示一种简单的方法。这里,这个类被命名为Singleton,,它具有以下特征。在继续之前,您必须仔细阅读它们。

  • 在这个例子中,我使用了一个私有的无参数构造函数。因此,您不能以正常的方式实例化该类型(使用new)。

  • 这门课是密封的。(对于我们即将进行的演示,这不是必需的,但是如果您对这个 Singleton 类进行特定的修改,这可能是有益的。这个在问答环节讨论)。

  • 既然new被阻塞了,怎么获取实例呢?在这种情况下,您可以选择实用方法或属性。在这个例子中,我选择了一个属性,在我的 Singleton 类中,您会看到下面的代码:

    public static Singleton GetInstance
    {
         get
             {
                return Instance;
              }
     }
    
    
  • 如果您喜欢使用表达式体的只读属性(在 C# v6 中提供),您可以用下面的代码行替换该代码段:

  • 我在 Singleton 类中使用了一个静态构造函数。静态构造函数必须是无参数的。按照微软的说法,在 C# 中,它初始化静态数据,并且只执行一次特定的操作。此外,在创建第一个实例或引用任何静态类成员之前,会自动调用静态构造函数。您可以放心地假设我已经充分利用了这些规范。

  • Main()方法中,我使用一个简单的检查来确保我使用的是同一个且唯一可用的实例。

  • 您会在 Singleton 类中看到以下代码行:

public static Singleton GetInstance => Instance;

private static readonly Singleton Instance;

公共静态成员确保了一个全局访问点。它确认实例化过程不会开始,直到您调用类的Instance属性(换句话说,它支持惰性实例化),并且readonly确保赋值过程只发生在静态构造函数中。一旦退出构造函数,就不能给readonly字段赋值。如果您错误地反复尝试分配这个static readonly字段,您将会遇到CS0198编译时错误which says that a static readonly field cannot be assigned (except in a static constructor or a variable initializer)

  • Singleton 类也用 sealed 关键字标记,以防止类的进一步派生(这样它的子类就不能误用它)。

Note

我保留了重要的注释,以帮助您更好地理解。我将对本书中的大多数程序做同样的事情;例如,当您从 Apress 网站下载代码时,您可以在注释行中看到表达式体的只读属性的用法。

类图

图 1-1 是说明单例模式的类图。

img/463942_2_En_1_Fig1_HTML.jpg

图 1-1

类图

解决方案资源管理器视图

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

img/463942_2_En_1_Fig2_HTML.jpg

图 1-2

解决方案资源管理器视图

演示 1

浏览下面的实现,并使用支持性的注释来帮助您更好地理解。

using System;

namespace SingletonPatternUsingStaticConstructor
{
    public sealed class Singleton
    {
        #region Singleton implementation using static constructor

        private static readonly Singleton Instance;
        private static int TotalInstances;
        /*
         * Private constructor is used to prevent
         * creation of instances with the 'new' keyword
         * outside this class.
         */
        private Singleton()
        {
            Console.WriteLine("--Private constructor is called.");
            Console.WriteLine("--Exit now from private constructor.");
        }

        /*
         * A static constructor is used for the following purposes:
         * 1\. To initialize any static data
         * 2\. To perform a specific action only once
         *
         * The static constructor will be called automatically before:
         * i. You create the first instance; or
         * ii.You refer to any static members in your code.
         *
         */

        // Here is the static constructor
        static Singleton()
        {
            // Printing some messages before you create the instance
            Console.WriteLine("-Static constructor is called.");
            Instance = new Singleton();
            TotalInstances++;
            Console.WriteLine($"-Singleton instance is created.Number of instances:{ TotalInstances}");
            Console.WriteLine("-Exit from static constructor.");
        }
        public static Singleton GetInstance
        {
            get
            {
                return Instance;
            }
        }
     /*
      * If you like to use expression-bodied read-only
      * property, you can use the following line (C# v6.0 onwards).
      */
        // public static Singleton GetInstance => Instance;
        #endregion
        /* The following line is used to discuss
        the drawback of the approach. */
        public static int MyInt = 25;
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Singleton Pattern Demonstration.***\n");
            /* The following line is used to discuss
              the drawback of the approach. */
            //Console.WriteLine($"The value of MyInt is :{Singleton.MyInt}");
            // Private Constructor.So, you cannot use the 'new' keyword.
            //Singleton s = new Singleton(); // error
            Console.WriteLine("Trying to get a Singleton instance, called firstInstance.");
            Singleton firstInstance = Singleton.GetInstance;
            Console.WriteLine("Trying to get another Singleton instance, called secondInstance.");
            Singleton secondInstance = Singleton.GetInstance;
            if (firstInstance.Equals(secondInstance))
            {
                Console.WriteLine("The firstInstance and secondInstance are the same.");
            }
            else
            {
                Console.WriteLine("Different instances exist.");
            }
            Console.Read();
        }
    }
}

输出

下面是这个例子的输出。

***Singleton Pattern Demonstration.***

Trying to get a Singleton instance, called firstInstance.
-Static constructor is called.
--Private constructor is called.
--Exit now from private constructor.
-Singleton instance is created.Number of instances:1
-Exit from static constructor.
Trying to get another Singleton instance, called secondInstance.
The firstInstance and secondInstance are the same.

Note

Microsoft 建议静态字段使用 Pascal 命名约定。我在前面的演示中遵循了这一点。

分析

在这一节中,我将讨论与前面的演示相关的两个要点。首先,我向您展示了如何缩短代码长度,然后我讨论了我刚刚采用的方法的一个潜在缺点。我们开始吧。

从相关的注释中,您会发现如果您喜欢使用表达式体的只读属性,您可以替换下面的代码段

public static Singleton GetInstance
        {
            get
            {
                return Instance;
            }
        }

使用下面的代码行。

public static Singleton GetInstance => Instance;

保留现有代码,在Singleton类中添加以下代码段。

        /* The following line is used to discuss
        the drawback of the approach.*/
        public static int MyInt = 25;

添加之后,Singleton类如下。

public sealed class Singleton
    {
        #region Singleton implementation using static constructor
        // Keeping all existing code shown in the previous demonstration
        #endregion
        /* The following line is used to discuss
        the drawback of the approach.*/
        public static int MyInt = 25;
    }

现在假设您使用下面的Main()方法。

static void Main(string[] args)
        {
            Console.WriteLine("***Singleton Pattern Demonstration.***\n");
            Console.WriteLine($"The value of MyInt is :{Singleton.MyInt}");
            Console.Read();
        }

如果您现在执行该程序,您会看到以下输出。

***Singleton Pattern Demonstration.***

-Static constructor is called.
--Private constructor is called.
--Exit now from private constructor.
-Singleton instance is created.Number of instances:1
-Exit from static constructor.
The value of MyInt is :25

虽然您应该只看到输出的最后一行,但是您得到了Singleton类的所有实例化细节,这说明了这种方法的缺点。具体来说,在Main()方法中,您试图使用MyInt静态变量,但是您的应用仍然创建了 Singleton 类的一个实例。因此,当您使用这种方法时,您对实例化过程的控制较少

然而,除了这个问题之外,没有与之相关的显著缺点。您只需承认这是一次性活动,初始化过程不会重复。如果你能容忍这个缺点,你就可以宣称你已经实现了一个简单、漂亮的单例模式。在这里我要重复的是,每种方法都有自己的优点和缺点;没有一种方法是 100%完美的。根据您的需求,您可能会选择其中一个。

接下来,我将介绍这种实现的另一种常见变体。我可以直接使用下面的代码行

private static readonly Singleton Instance = new Singleton();

并避免使用静态构造函数在控制台中打印特殊消息。下面的代码段也演示了单例模式。

public sealed class Singleton
    {
        #region Using static initialization
        private static readonly Singleton Instance = new Singleton();

        private static int TotalInstances;
        /*
         * Private constructor is used to prevent
         * creation of instances with 'new' keyword
         * outside this class.
         */
        private Singleton()
        {
            Console.WriteLine("--Private constructor is called.");
            Console.WriteLine("--Exit now from private constructor.");
        }
        public static Singleton GetInstance
        {
            get
            {
                return Instance;
            }
        }

        #endregion
    }

这种编码通常被称为静态初始化。我想在控制台中打印定制消息,所以我的首选方法如演示 1 所示。

问答环节

你为什么要把事情复杂化?你可以简单地编写你的 单例类 如下。

public class Singleton
    {
        private static Singleton instance;
        private Singleton() { }
        public static Singleton Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }
    }

是的,这种方法可以在单线程环境中工作,但是考虑一个多线程环境,其中两个(或更多)线程可能试图同时评估下面的代码。

if (instance == null)

如果实例尚未创建,每个线程将尝试创建一个新的实例。因此,您可能会得到该类的多个实例。

你能展示一种替代的方法来建模 单例设计模式吗?

有许多方法。每一种都有利弊。

以下代码显示了双重检查锁定。下面的代码段概述了这种方法。

    // Singleton implementation using double checked locking.
    public sealed class Singleton
    {
        /*
         * We are using volatile to ensure
         * that assignment to the instance variable finishes
         * before it's accessed.
        */
        private static volatile Singleton Instance;
        private static object lockObject = new Object();

        private Singleton() { }

        public static Singleton GetInstance
        {
            get
            {
                // First Check
                if (Instance == null)
                {
                    lock (lockObject)
                    {
                        // Second(Double) Check
                        if (Instance == null)
                            Instance = new Singleton();
                    }
                }
                return Instance;
            }
        }
    }

这种方法可以帮助您在需要时创建实例。但你必须记住,一般来说,锁定机制是昂贵的。

除了使用双锁,您还可以使用单锁,如下所示。

//Singleton implementation using single lock
    public sealed class Singleton
    {
        /*
         * We are using volatile to ensure
         * that assignment to the instance variable finishes
         * before it's access.
         */
        private static volatile Singleton Instance;
        private static object lockObject = new Object();

        private Singleton() { }

        public static Singleton GetInstance
        {
            get
            {
                // Locking it first
                lock (lockObject)
                {
                    // Single check
                    if (Instance == null)
                    {
                        Instance = new Singleton();
                    }
                }
                return Instance;
            }
        }
    }

尽管这种方法看起来更简单,但它并不被认为是更好的方法,因为每次请求Singleton实例的一个实例时,您都要获取锁,这会降低应用的性能。

在本章的最后,你会看到另一种使用 C# 内置结构实现单例模式的方法。

Note

当您保持客户端代码不变时,您可以使用您喜欢的方法简单地替换 Singleton 类。我提供了这方面的完整演示,您可以从 Apress 的网站下载。

1.3 为什么在 双重检查锁定 示例中将实例标记为 volatile?

许多开发商认为这是不必要的。NET 2.0 及以上,但有争论。为了简单起见,让我们看看 C# 规范是怎么表述的:“volatile 关键字表示一个字段可能会被同时执行的多个线程修改。出于性能原因,编译器、运行时系统甚至硬件可能会重新安排对内存位置的读写。声明为 volatile 的字段不受这些优化的影响。添加 volatile 修饰符可确保所有线程都将按照执行顺序观察任何其他线程执行的易失性写入。这仅仅意味着volatile关键字有助于提供一种序列化的访问机制,因此所有线程都可以按照它们的执行顺序观察到任何其他线程的变化。它确保最新的值总是出现在字段中。因此,使用 volatile 修饰符使 s 你的代码更加安全。

在这个上下文中,y ou 应该记住volatile关键字不能应用于所有类型,并且有一定的限制。例如,您可以将它应用于类或结构字段,但不能应用于局部变量。

1.4 为什么多重 物体创作 是一个大问题?

这里有两点需要记住。

  • 如果您正在处理资源密集型对象,则对象创建的成本会很高。

  • 在某些应用中,您可能需要将一个公共对象传递到多个位置。

1.5 什么时候应该使用单例模式?

看情况。这里有一些这种模式有用的常见用例。

  • 当使用集中式系统(例如数据库)时

  • 维护公共日志文件时

  • 当在多线程环境中维护线程池时

  • 当实现缓存机制或设备驱动程序时,等等

1.6 为什么使用 sealed 关键字 ?Singleton 类有一个私有构造函数,足以停止派生过程。

接得好。这不是强制性的,但最好清楚地表明你的意图。我用它来保护一种特殊的情况:当你试图使用一个派生的嵌套类,并且你喜欢在私有构造函数内部初始化。为了更好地理解这一点,我们假设您有下面这个类,它不是密封的。在这个类中,不使用静态构造函数;相反,您使用私有构造函数来跟踪实例的数量。我用粗体显示了关键的变化。

public class Singleton
{
private static readonly Singleton Instance = new Singleton();
private static int TotalInstances;
/*
 * Private constructor is used to prevent
 * creation of instances with 'new' keyword
 * outside this class.
 */
private Singleton()
{
    Console.WriteLine("--Private constructor is called.");
    TotalInstances++;
    Console.WriteLine($"-Singleton instance is created. Number of instances:{ TotalInstances}");
    Console.WriteLine("--Exit now from private constructor.");
}

public static Singleton GetInstance
{
    get
    {
     return Instance;
    }
}

// The keyword "sealed" can guard this scenario.
// public class NestedDerived : Singleton { }

}

Main() method ,中,让我们对控制台消息的第一行做一点小小的修改,以区别于原始的输出,但让我们保持其余部分不变。它现在看起来如下。

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Singleton Pattern Q&A***\n");
            Console.WriteLine("Trying to get a Singleton instance, called firstInstance.");
            Singleton firstInstance = Singleton.GetInstance;
            Console.WriteLine("Trying to get another Singleton instance, called secondInstance.");
            Singleton secondInstance = Singleton.GetInstance;
            if (firstInstance.Equals(secondInstance))
            {
                Console.WriteLine("The firstInstance and secondInstance are same.");
            }
            else
            {
                Console.WriteLine("Different instances exist.");
            }
            //Singleton.NestedDerived nestedClassObject1 = new Singleton.NestedDerived();
            //Singleton.NestedDerived nestedClassObject2 = new Singleton.NestedDerived();
            Console.Read();
        }
}

如果您运行该程序,您将得到以下输出。

***Singleton Pattern Q&A***

Trying to get a Singleton instance, called firstInstance

--Private constructor is called.
-Singleton instance is created. Number of instances:1
--Exit now from private constructor.

Trying to get another Singleton instance, called secondInstance

The firstInstance and??。这很简单,类似于我们最初演示的输出。

现在取消 Singleton 类中下面一行的注释。

//public class NestedDerived : Singleton { }

然后在Main()方法中取消下面两行代码的注释。

//Singleton.NestedDerived nestedClassObject1 = new Singleton.NestedDerived();
//Singleton.NestedDerived nestedClassObject2 = new Singleton.NestedDerived();

再次运行应用。这一次,您将获得以下输出。

***Singleton Pattern Q&A***

Trying to get a Singleton instance, called firstInstance.
--Private constructor is called.
-Singleton instance is created.Number of instances:1
--Exit now from private constructor.
Trying to get another Singleton instance, called secondInstance.
The firstInstance and secondInstance are same.
--Private constructor is called.
-Singleton instance is created.Number of instances:2
--Exit now from private constructor.
--Private constructor is called.
-Singleton instance is created.Number of instances:3
--Exit now from private constructor.

您是否注意到实例的总数正在增加?虽然在我最初的演示中,我可以排除使用sealed,但我保留了它来防范这种情况,这种情况可能是由于修改了 Singleton 类的原始实现而出现的。

替代实现

现在我将向您展示另一种使用 C# 内置结构的方法。在本书的前一版本中,我跳过了这一点,因为要理解这段代码,您需要熟悉泛型、委托和 lambda 表达式。如果您不熟悉委托,可以暂时跳过这一部分;否则,我们继续。

在这个例子中,我将向您展示有效使用代码的三种不同方式(使用自定义委托、使用内置Func委托,以及最后使用 lambda 表达式)。让我们看看带有相关注释的 Singleton 类的核心代码段,然后进行分析。

    // Singleton implementation using Lazy<T>
    public sealed class Singleton
    {
        // Custom delegate
        delegate Singleton SingletonDelegateWithNoParameter();
        static SingletonDelegateWithNoParameter myDel = MakeSingletonInstance;

        // Using built-in Func<out TResult> delegate
        static Func<Singleton> myFuncDelegate= MakeSingletonInstance;

        private static readonly Lazy<Singleton> Instance = new Lazy<Singleton>(
            //myDel()  // Also ok. Using a custom delegate
            myFuncDelegate()
            //() => new Singleton() // Using lambda expression
            );

        private static Singleton MakeSingletonInstance()
        {
            return new Singleton();
        }
        private Singleton() { }

        public static Singleton GetInstance
        {
            get
            {
                return Instance.Value;
            }
        }
    }

分析

这段代码最重要的部分是

private static readonly Lazy<Singleton> Instance = new Lazy<Singleton>(
            //myDel()  // Also ok. Using a custom delegate
            myFuncDelegate()
            //() => new Singleton()  // Using lambda expression
     );

这里myDel()被注释掉;当您使用自定义委托时,可以使用它。在使用内置的Func委托的地方myFuncDelegate()已经被执行。如果您想使用 lambda 表达式而不是委托,可以使用最后一行注释。简而言之,当您尝试这些方法中的任何一种时,其他两行应该被注释掉。

如果将鼠标悬停在Lazy<Singleton>上,会看到Lazy<T>支持惰性初始化;在撰写本文时,它有七个重载版本的构造函数,其中一些可以接受一个Func委托实例作为方法参数。现在你知道我为什么在这个例子中使用了Func委托了。图 1-3 是 Visual Studio 截图。

img/463942_2_En_1_Fig3_HTML.jpg

图 1-3

懒惰类的 Visual Studio 截图

在这个例子中,我使用了下面的版本。

public Lazy(Func<T> valueFactory);

虽然Func委托有很多重载版本,但是在这种情况下,你只能使用下面的版本。

public delegate TResult Func<[NullableAttribute(2)] out TResult>();

这个Func版本可以指向一个不接受任何参数但返回一个由TResult参数指定的类型的值的方法,这就是为什么它可以正确地指向下面的方法。

private static Singleton MakeSingletonInstance()
        {
            return new Singleton();
        }

如果您想使用自己的委托,您可以这样做。以下代码段可用于此目的。

// Custom delegate
delegate Singleton SingletonDelegateWithNoParameter();
static SingletonDelegateWithNoParameter myDel = MakeSingletonInstance;

在这种情况下,你需要使用myDel()而不是myFuncDelegate()

最后,如果选择 lambda 表达式,就不需要MakeSingletonInstance()方法,可以直接使用下面这段代码

private static readonly Lazy<Singleton> Instance = new Lazy<Singleton>(

            () => new Singleton()  // Using lambda expression
     );

Note

在所有实现单例模式的方法中,Main()方法本质上是相同的。因此,为了简洁起见,我没有在讨论中包括这一点。

问答环节

1.7 你用了术语。这是什么意思?

**这是一种用来延迟对象创建过程的技术。基本思想是,只有在真正需要时,才应该创建对象。当创建对象是一项开销很大的操作时,此方法很有用。

希望您对单例设计模式有更好的理解。在这种模式中,性能与懒惰总是一个问题,一些开发人员总是质疑这些方面。但事实是,这种模式以各种形式出现在许多应用中。让我们引用 Erich Gamma(瑞士计算机科学家和 GoF 作者之一)在 2009 年的一次采访来结束这一章:“当讨论放弃哪些模式时,我们发现我们仍然热爱它们。不尽然——我赞成放弃辛格尔顿。它的用途几乎总是一种设计气味。”有兴趣看本次面试详情的可以关注链接:https://www.informit.com/articles/article.aspx?p=1404056。**

二、原型模式

本章涵盖了原型模式。

GoF 定义

使用原型实例指定要创建的对象种类,并通过复制该原型来创建新对象。

概念

原型模式提供了另一种方法,通过复制或克隆现有对象的实例来实例化新对象。使用这个概念可以避免创建新实例的开销。如果你观察模式的意图(GoF 定义),你会发现这个模式的核心思想是创建一个基于另一个对象的对象。这个现有对象充当新对象的模板。

当你为这种模式编写代码时,一般来说,你会看到有一个抽象类或接口扮演着抽象原型的角色。这个抽象原型包含一个由具体原型实现的克隆方法。客户可以通过要求原型克隆自己来创建一个新对象。在本章的下一个程序(演示 1)中,我遵循同样的方法。

真实世界的例子

假设你有一份有价值文件的主拷贝。您需要对其进行一些更改,以分析更改的效果。在这种情况下,您可以复印原始文档,并在复印的文档中编辑更改。

计算机世界的例子

让我们假设您已经有了一个稳定的应用。将来,您可能希望对应用进行一些小的修改。您必须从原始应用的副本开始,进行更改,然后进一步分析它。你不想仅仅为了改变而从头开始;这会耗费你的时间和金钱。

英寸 NET 中,ICloneable接口包含一个Clone()方法。在 Visual Studio IDE 中,您可以很容易地找到以下详细信息。

namespace System
{
    //
    // Summary:
    //     Supports cloning, which creates a new instance of a class with     //     the same value
 as an existing instance.
    [NullableContextAttribute(1)]
    public interface ICloneable
    {
        //
        // Summary:
        //     Creates a new object that is a copy of the current instance.
        //
        // Returns:
        //     A new object that is a copy of this instance.
        object Clone();
    }
}

您可以在实现原型模式时使用这个内置的构造,但是在这个例子中,我使用了自己的Clone()方法。

履行

在这个例子中,我遵循图 2-1 所示的结构。

img/463942_2_En_2_Fig1_HTML.jpg

图 2-1

原型示例

这里BasicCar是原型。它是一个抽象类,有一个名为Clone()的抽象方法。NanoFord是具体的类(即具体的原型),它们继承自BasicCar。两个具体的类都实现了Clone()方法。在这个例子中,最初,我用默认价格创建了一个BasicCar对象。后来,我修改了每个型号的价格。Program.cs是实现中的客户端。

BasicCar类内部,有一个名为SetAdditionalPrice()的方法。它生成一个介于 200,000(含)和 500,000(不含)之间的随机值。在我计算汽车的最终onRoad价格之前,这个值被加到基础价格中。在这个例子中,我用印度货币(卢比)提到了这些汽车的价格。

汽车模型的基本价格是由具体原型的建造者设定的。因此,您会看到如下代码段,其中具体的原型(Nano)初始化基本价格。同样,这个类也覆盖了BasicCar中的Clone()方法。

public class Nano : BasicCar
    {
        public Nano(string m)
        {
            ModelName = m;
            // Setting a basic price for Nano.
            basePrice = 100000;
        }
        public override BasicCar Clone()
        {
            // Creating a shallow copy and returning it.
            return this.MemberwiseClone() as Nano;
        }
    }

Ford,另一个混凝土原型,也有类似的结构。在这个例子中,我使用了两个具体的原型(FordNano)。为了更好地理解原型模式,一个具体的原型就足够了。因此,如果您愿意,您可以简单地删除这些具体的原型来减少代码大小。

最后也是最重要的,在接下来的例子中您会看到MemberwiseClone()方法。它在Object类中定义,有如下描述。

// Summary:
//     Creates a shallow copy of the current System.Object.
//
// Returns:
//     A shallow copy of the current System.Object.
[NullableContextAttribute(1)]
protected Object MemberwiseClone();

Note

你可能对术语浅薄感到疑惑。实际上,克隆有两种类型:浅层克隆和深层克隆。这一章包括一个讨论和一个完整的程序来帮助你理解他们的区别。现在,您只需要知道在浅层复制中,类的简单类型字段被复制到克隆的实例中;但是对于引用类型字段,只复制引用。因此,在这种类型的克隆中,原始实例和克隆实例都指向同一个引用,这在某些情况下可能会导致问题。为了克服这一点,您可能需要使用深层拷贝。

类图

图 2-2 显示了类图。

img/463942_2_En_2_Fig2_HTML.jpg

图 2-2

类图

解决方案资源管理器视图

图 2-3 显示了程序各部分的高层结构。

img/463942_2_En_2_Fig3_HTML.jpg

图 2-3

解决方案资源管理器视图

演示 1

下面是实现。

// BasicCar.cs

using System;

namespace PrototypePattern
{
    public abstract class BasicCar
    {
        public int basePrice = 0, onRoadPrice=0;
        public string ModelName { get; set; }

        /*
            We'll add this price before
            the final calculation of onRoadPrice.
        */
        public static int SetAdditionalPrice()
        {
            Random random = new Random();
            int additionalPrice = random.Next(200000, 500000);
            return additionalPrice;
        }
        public abstract BasicCar Clone();
    }
}

// Nano.cs

namespace PrototypePattern
{
    public class Nano : BasicCar
    {
        public Nano(string m)
        {
            ModelName = m;
            // Setting a base price for Nano.
            basePrice = 100000;
        }
        public override BasicCar Clone()
        {
            // Creating a shallow copy and returning it.
            return this.MemberwiseClone() as Nano;
        }
    }
}
// Ford.cs

namespace PrototypePattern
{
    public class Ford : BasicCar
    {
        public Ford(string m)
        {
            ModelName = m;
            // Setting a basic price for Ford.
            basePrice = 500000;
        }

        public override BasicCar Clone()
        {
            // Creating a shallow copy and returning it.
            return this.MemberwiseClone() as Ford;
        }
    }
}
// Client

using System;

namespace PrototypePattern
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Prototype Pattern Demo***\n");
            // Base or Original Copy
            BasicCar nano = new Nano("Green Nano");
            BasicCar ford = new Ford("Ford Yellow");
            BasicCar basicCar;
            // Nano
            basicCar = nano.Clone();
            // Working on cloned copy
            basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
            Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");

            // Ford
            basicCar = ford.Clone();
            // Working on cloned copy
            basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
            Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");

            Console.ReadLine();
        }
    }
}

输出

下面是一个可能的输出。

***Prototype Pattern Demo***

Car is: Green Nano, and it's price is Rs. 368104
Car is: Ford Yellow, and it's price is Rs. 878072

Note

您可能会在系统中看到不同的价格,因为我在BasicCar类的SetAdditionalPrice()方法中生成了一个随机价格。但是我保证了Ford的价格大于Nano

修改的实现

在演示 1 中,在制作克隆之前,客户端按如下方式实例化对象。

BasicCar nano = new Nano("Green Nano");
BasicCar ford = new Ford("Ford Yellow");

这很好,但是在原型模式的一些例子中,您可能会注意到一个额外的参与者创建原型并将它们提供给客户。专家通常喜欢这种方法,因为它向客户端隐藏了创建新实例的复杂性。让我们在演示 2 中看看如何实现这一点。

类图

图 2-4 显示了修改后的类图中的关键变化。

img/463942_2_En_2_Fig4_HTML.jpg

图 2-4

演示 2 的类图中的主要变化

演示 2

为了演示这一点,我在前面的演示中添加了下面这个名为CarFactory的类。

class CarFactory
    {
        private readonly BasicCar nano, ford;

        public CarFactory()
        {
            nano = new Nano("Green Nano");
            ford = new Ford("Ford Yellow");
        }
        public BasicCar GetNano()
        {
           return  nano.Clone();
        }
        public BasicCar GetFord()
        {
            return ford.Clone();
        }
    }

使用这个类,您的客户端代码可以修改如下。

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Prototype Pattern Demo2.***\n");
            CarFactory carFactory = new CarFactory();
            // Get a Nano
            BasicCar basicCar = carFactory.GetNano();
            //Working on cloned copy
            basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
            Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");

            // Get a Ford now
            basicCar = carFactory.GetFord();
            // Working on cloned copy
            basicCar.onRoadPrice = basicCar.basePrice + BasicCar.SetAdditionalPrice();
            Console.WriteLine($"Car is: {basicCar.ModelName}, and it's price is Rs. {basicCar.onRoadPrice}");

            Console.ReadLine();
        }
    }

输出

下面是一个可能的输出。

***Prototype Pattern Demo2.***

Car is: Green Nano, and it's price is Rs. 546365
Car is: Ford Yellow, and it's price is Rs. 828518

分析

这个输出就和之前的输出一样,没有什么魔力。类满足了我们的需求,但是它有一个潜在的缺点。我在CarFactory的构造函数中初始化了汽车。因此,在初始化该类时,它总是创建这两种汽车类型的实例。因此,如果您想实现一个惰性初始化,您可以修改CarFactory类中的GetNano()方法,如下所示。

public BasicCar GetNano()
        {
           if (nano!=null)
            {
                // Nano was created earlier.
                // Returning a clone of it.
                return nano.Clone();
            }
            else
            {
                /*
                  Create a nano for the first
                  time and return it.
                */
                nano = new Nano("Green Nano");
                return nano;
            }
        }

你可以用同样的方法修改GetFord()方法。

Note

当您实现这些更改时,不要忘记移除只读修饰符以避免编译时错误。

下面是修改后的类。

class CarFactory
    {
        private BasicCar nano,ford;
        public BasicCar GetNano()
        {
           if (nano!=null)
            {
                // Nano was created earlier.
                // Returning a clone of it.
                return nano.Clone();
            }
            else
            {
                /*
                  Create a nano for the first
                  time and return it.
                */
                nano = new Nano("Green Nano");
                return nano;
            }
        }
        public BasicCar GetFord()
        {
           if (ford != null)
            {
                // Ford was created earlier.
                // Returning a clone of it.
                return ford.Clone();
            }
            else
            {
                /*
                  Create a nano for the first
                  time and return it.
                */
                ford = new Ford("Ford Yellow");
                return ford;
            }
        }
    }

最后,这不是最终的修改。在第一章中,你了解到在多线程环境中,当你检查 if 条件时,可能会产生额外的对象。由于你在第一章中学习了可能的解决方案,所以我不会在这次讨论或接下来的讨论中关注它们。我相信您现在应该对这种模式的意图有了清晰的认识。

问答环节

2.1 使用原型设计模式的 优势 有哪些?

以下是一些重要的用法。

  • 您不希望修改现有对象并在其上进行实验。

  • 您可以在运行时包含或丢弃产品。

  • 在某些情况下,您可以以更低的成本创建新的实例。

  • 您可以专注于关键活动,而不是复杂的实例创建过程。例如,一旦您忽略了复杂的对象创建过程,您就可以简单地从克隆或复制对象开始,并实现其余部分。

  • 您希望在完全实现新对象之前,先感受一下它的行为。

2.2 与使用原型设计模式相关的 挑战 有哪些?

以下是一些挑战。

  • 每个子类都需要实现克隆或复制机制。

  • 如果所考虑的对象不支持复制或者存在循环引用,那么实现克隆机制可能会很有挑战性。

在这个例子中,我使用了MemberwiseClone()成员方法,它提供了一个浅层拷贝。这是一个非常简单的技术,可以满足你的基本需求。但是,如果您需要为一个复杂的对象提供深度复制实现,这可能会很昂贵,因为您不仅需要复制对象,还需要处理所有的引用,这可能会形成一个非常复杂的图。

2.3 能否详细说明一下 C# 中浅拷贝和深拷贝的区别?

下一节解释了它们的区别。

浅层拷贝与深层拷贝

浅层复制创建一个新对象,然后将非静态字段从原始对象复制到新对象。如果原始对象中存在值类型字段,则执行逐位复制。但是如果该字段是引用类型,则该方法复制引用,而不是实际的对象。让我们试着用一个简单的图表来理解这个机制(见图 2-5 )。假设您有一个对象X1,它有一个对另一个对象Y1的引用。此外,假设对象Y1具有对对象Z1的引用。

img/463942_2_En_2_Fig5_HTML.jpg

图 2-5

在引用的浅拷贝之前

通过对X1的浅层复制,一个新的对象(比如说X2)被创建,它也引用了Y1(见图 2-6 )。

img/463942_2_En_2_Fig6_HTML.jpg

图 2-6

在引用的浅拷贝之后

我在实现中使用了MemberwiseClone()。它执行浅层复制。

对于X1的深层副本,创建一个新对象(比如说,X3),并且X3具有对新对象Y3的引用,该新对象是Y1的副本。此外,Y3又引用了另一个新对象Z3,它是Z1的副本(见图 2-7 )。

img/463942_2_En_2_Fig7_HTML.jpg

图 2-7

在引用的深层副本之后

现在考虑下面的演示,以便更好地理解。

演示 3

这个简单的演示向您展示了浅层拷贝和深层拷贝之间的区别。它还向您展示了为什么深层副本在某些情况下很重要。以下是该计划的主要特点。

  • 有两类:EmployeeEmpAddress

  • EmpAddress只有一个读写属性,叫做Address。它设置一个雇员的地址,但是Employee类有三个读写属性:Id, Name,EmpAddress.

  • 要形成一个Employee对象,需要传递一个 ID 和员工的名字,同时还需要传递地址。因此,您会看到如下代码段。

    EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
    Employee emp = new Employee(1, "John", initialAddress);
    
    
  • 在客户端代码中,首先创建一个Employee对象(emp),然后通过克隆创建另一个对象empClone。您会看到下面几行代码。

    Console.WriteLine("Making a clone of emp1 now.");
    Employee empClone = (Employee)emp.Clone();
    
    
  • 稍后,您更改empClone中的值。

当使用浅层拷贝时,这种变化的副作用是emp对象的地址也发生了变化,这是不希望的。(原型模式很简单;在处理对象的克隆副本时,不应更改原始对象)。

在下面的示例中,深层副本的代码最初是注释的,因此您只能看到浅层副本的效果。

现在来看一下演示。

using System;

namespace ShallowVsDeepCopy
{
    class EmpAddress
    {
        public string Address { get; set; }

        public EmpAddress(string address)
        {
            this.Address = address;
        }

        public override string ToString()
        {
            return this.Address;
        }

        public object CloneAddress()
        {
            // Shallow Copy
            return this.MemberwiseClone();
        }
    }
    class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public EmpAddress EmpAddress { get; set; }

        public Employee(int id, string name, EmpAddress empAddress)

        {
            this.Id = id;
            this.Name = name;
            this.EmpAddress = empAddress;
        }

        public override string ToString()
        {
            return string.Format("Employee Id is : {0},Employee Name is : {1}, Employee Address is : {2}", this.Id,this.Name,this.EmpAddress);
        }

        public object Clone()
        {
            // Shallow Copy
            return this.MemberwiseClone();

            #region For deep copy

            //Employee employee = (Employee)this.MemberwiseClone();
            //employee.EmpAddress = (EmpAddress)this.EmpAddress.//CloneAddress();

            /*
             * NOTE:
             * Error: MemberwiseClone() is protected, you cannot access it via a qualifier of type EmpAddress. The qualifier must be Employee or its derived type.
             */
            //employee.EmpAddress = (EmpAddress)this.EmpAddress.MemberwiseClone(); // error

            // return employee;
            #endregion

        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Shallow vs Deep Copy Demo.***\n");
            EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
            Employee emp = new Employee(1, "John", initialAddress);

            Console.WriteLine("The original object is emp1 which is as follows:");
            Console.WriteLine(emp);

            Console.WriteLine("Making a clone of emp1 now.");
            Employee empClone = (Employee)emp.Clone();
            Console.WriteLine("empClone object is as follows:");
            Console.WriteLine(empClone);

            Console.WriteLine("\n Now changing the name, id and address of the cloned object ");
            empClone.Id=10;
            empClone.Name="Sam";
            empClone.EmpAddress.Address= "221, xyz Road, Canada";

            Console.WriteLine("Now emp1 object is as follows:");
            Console.WriteLine(emp);
            Console.WriteLine("And emp1Clone object is as follows:");
            Console.WriteLine(empClone);
        }

    }
}

浅层拷贝的输出

以下是程序的输出。

***Shallow vs Deep Copy Demo.***

The original object is emp1 which is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Making a clone of emp1 now.
empClone object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA

 Now changing the name, id and address of the cloned object
Now emp1 object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 221, xyz Road, Canada
And emp1Clone object is as follows:
Employee Id is : 10,Employee Name is : Sam, Employee Address is : 221, xyz Road, Canada

分析

有一个不想要的副作用。在前面的输出中,原始对象(emp)的地址由于修改克隆对象(empClone)而被修改。发生这种情况是因为原始对象和克隆对象指向同一个地址,并且它们不是 100%分离的。图 2-8 描述了该场景。

img/463942_2_En_2_Fig8_HTML.jpg

图 2-8

浅拷贝

现在让我们用深度复制实现来做实验。让我们修改Employee类的Clone方法如下。(我取消了深层副本的代码注释,并注释掉了浅层副本中的代码。)

public Object Clone()
        {
            // Shallow Copy
            //return this.MemberwiseClone();

            #region For deep copy

            Employee employee = (Employee)this.MemberwiseClone();
            employee.EmpAddress = (EmpAddress)this.EmpAddress.CloneAddress();

            /*
             * NOTE:
             Error: MemberwiseClone() is protected, you cannot access it via a qualifier of type EmpAddress.The qualifier must be Employee or its derived type.
            */
            //employee.EmpAddress = (EmpAddress)this.EmpAddress.MemberwiseClone();//error

            return employee;
            #endregion

        }

深层拷贝的输出

下面是修改后的输出。

***Shallow vs Deep Copy Demo***

The original object is emp1 which is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
Making a clone of emp1 now.
empClone object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA

Now changing the name, id and address of the cloned object
Now emp1 object is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
And emp1Clone object is as follows:
Employee Id is : 10,Employee Name is : Sam, Employee Address is : 221, xyz Road, Canada

分析

这一次,您不会看到由于修改empClone对象而产生的不必要的副作用。这是因为原始对象和克隆对象彼此不同且相互独立。图 2-9 描述了这个场景。

img/463942_2_En_2_Fig9_HTML.jpg

图 2-9

深层拷贝

问答环节

2.4 什么时候你应该选择浅层拷贝而不是 深层拷贝 (反之亦然)?

以下是主要原因。

  • 浅层拷贝速度更快,成本更低。如果您的目标对象只有基本字段,那么使用总是更好。

  • 深层拷贝开销大,速度慢,但是如果目标对象包含许多引用其他对象的字段,它就很有用。

2.5 在 C# 中,如果我需要复制一个对象,我需要使用 MemberwiseClone() 方法 。这是正确的吗?

不,还有其他选择。例如,在实现深度复制时,可以选择序列化机制,或者可以编写自己的复制构造函数,等等。每种方法都有其优点和缺点。因此,最终,开发人员有权决定哪种方法最适合他的需求。许多对象非常简单,它们不包含对其他对象的引用。因此,要从这些对象复制,一个简单的浅层复制机制就足够了。

你能给我举个例子演示一下 复制构造器 的用法吗?

由于 C# 不支持默认的复制构造函数,您可能需要编写自己的复制构造函数。演示 4 供您参考。

演示 4

在这个例子中,EmployeeEmpAddress类都有与演示 3 几乎相同的描述。唯一的不同是,这一次,你注意到在Employee类中出现了一个复制构造函数,而不是Clone()方法。我们继续吧。

这一次,使用下面的实例构造函数,

// Instance Constructor
public Employee(int id, string name, EmpAddress empAddress)
{
        this.Id = id;
        this.Name = name;
        this.EmpAddress = empAddress;
}

你可以如下创建一个Employee的对象。

EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
Employee emp = new Employee(1, "John",initialAddress);

在这个Employee类中,还有一个用户自定义的复制构造函数,如下。

// Copy Constructor
public Employee(Employee originalEmployee)
{
    this.Id = originalEmployee.Id;
    this.Name = originalEmployee.Name;
    //this.EmpAddress = (EmpAddress)this.EmpAddress.CloneAddress(); // ok
    this.EmpAddress = originalEmployee.EmpAddress.CloneAddress() as EmpAddress; // also ok
}

您可以看到,通过使用复制构造函数,我复制了简单类型(Id, Name)和引用类型(EmpAddress)。因此,一旦创建了像emp这样的Employee对象,就可以使用下面的代码从它创建另一个empClone对象。

Employee empClone= new Employee(emp);

和前面的演示一样,一旦我从现有的对象(emp)创建了一个副本(empClone),我就为了验证的目的对复制的对象进行了修改,使其更容易理解。这是完整的代码。

using System;

namespace UserdefinedCopyConstructorDemo
{
    class EmpAddress
    {
        public string Address { get; set; }

        public EmpAddress(string address)
        {
            this.Address = address;
        }

        public override string ToString()
        {
            return this.Address;
        }

        public object CloneAddress()
        {
            // Shallow Copy
            return this.MemberwiseClone();
        }
    }
    class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public EmpAddress EmpAddress { get; set; }

        // Instance Constructor
        public Employee(int id, string name, EmpAddress empAddress)
        {
            this.Id = id;
            this.Name = name;
            this.EmpAddress = empAddress;
        }
        // Copy Constructor

        public Employee(Employee originalEmployee)

        {
            this.Id = originalEmployee.Id;
            this.Name = originalEmployee.Name;
            //this.EmpAddress = (EmpAddress)this.EmpAddress.CloneAddress(); // ok
            this.EmpAddress = originalEmployee.EmpAddress.CloneAddress() as EmpAddress; // Also ok
        }
        public override string ToString()
        {
            return string.Format("Employee Id is : {0},Employee Name is : {1}, Employee Address is : {2}", this.Id, this.Name, this.EmpAddress);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***A simple copy constructor demo***\n");
            EmpAddress initialAddress = new EmpAddress("21, abc Road, USA");
            Employee emp = new Employee(1, "John",initialAddress);
            Console.WriteLine("The details of emp is as follows:");
            Console.WriteLine(emp);
            Console.WriteLine("\n Copying from emp1 to empClone now.");
            Employee empClone= new Employee(emp);
            Console.WriteLine("The details of empClone is as follows:");
            Console.WriteLine(empClone);
            Console.WriteLine("\nNow changing the id,name and address of empClone.");
            empClone.Name = "Sam";
            empClone.Id = 2;
            empClone.EmpAddress.Address= "221, xyz Road, Canada";
            Console.WriteLine("The details of emp is as follows:");
            Console.WriteLine(emp);
            Console.WriteLine("The details of empClone is as follows:");
            Console.WriteLine(empClone);
            Console.ReadKey();
        }
    }
}

输出

这是示例输出。

***A simple copy constructor demo***

The details of emp is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA

 Copying from emp1 to empClone now.
The details of empClone is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA

Now changing the id,name and address of empClone.
The details of emp is as follows:
Employee Id is : 1,Employee Name is : John, Employee Address is : 21, abc Road, USA
The details of empClone is as follows:
Employee Id is : 2,Employee Name is : Sam, Employee Address is : 221, xyz Road, Canada

分析

请注意输出的最后部分。它反映出只对复制的对象进行了适当的更改。

本章向您展示了原型设计模式的多种实现,并讨论了浅拷贝和深拷贝之间的区别。您还了解了用户定义的复制构造函数。现在你可以进入下一章,学习构建器模式。

三、构建器模式

本章涵盖了构建器模式。

GoF 定义

将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。

概念

构建器模式对于创建包含多个部分的复杂对象非常有用。对象创建过程应该独立于这些部分;换句话说,构建过程并不关心这些部分是如何组装的。此外,根据定义,您应该能够使用相同的构造过程来创建对象的不同表示。

根据 GoF,这种模式涉及四个不同的玩家,他们的关系如图 3-1 所示。

img/463942_2_En_3_Fig1_HTML.jpg

图 3-1

构建器模式示例

这里,Product是考虑中的复杂对象,是最终输出。Builder是一个接口,包含构建最终产品部件的方法。ConcreteBuilder实现了Builder接口,并组装了一个Product对象的不同部分。ConcreteBuilder对象构建了Product实例的内部表示,它有一个方法可以被调用来获得这个Product实例。Director负责使用Builder接口创建最终对象。值得注意的是Director是决定构建产品的步骤顺序的类/对象。所以,你可以放心地假设一个Director对象可以用来改变生产不同产品的顺序。

在演示 1 中,IBuilder表示Builder接口;CarMotorcycle分别是ConcreteBuilder s. ProductDirector类有它们通常的含义。

真实世界的例子

订购计算机时,会根据客户的喜好组装不同的硬件部件。例如,一个客户可以选择采用英特尔处理器的 500 GB 硬盘,另一个客户可以选择采用 AMD 处理器的 250 GB 硬盘。这里计算机是最终产品,客户扮演导演的角色,销售人员/组装人员扮演具体建造者的角色.

计算机世界的例子

当您想要将一种文本格式转换为另一种文本格式时,例如从 RTF 转换为 ASCII,可以使用这种模式。

履行

这个例子有以下几个部分:IBuilderCarMotorCycleProductDirectorIBuilder创建Product对象的一部分,其中Product代表正在构建的复杂对象。CarMotorCycleIBuilder接口的具体实现。(是的,IVehicle可能是比IBuilder,更好的命名,但我选择了后者,以强调它是一个构建器接口。)它们实现了IBuilder接口,其表示如下。****

   interface IBuilder
    {
        void StartUpOperations();
        void BuildBody();
        void InsertWheels();
        void AddHeadlights();
        void EndOperations();
        Product GetVehicle();
    }

这就是为什么CarMotorcycle需要为以下方法供应身体:StartUpOperations()BuildBody()InsertWheels()AddHeadlights()EndOperations()GetVehicle()。前五种方法很简单;他们在开始时执行各种操作,构建车辆的车身,添加车轮和大灯,并在结束时执行一项操作。(比方说,制造商想要添加一个标志或打磨车辆,等等。在接下来的例子中,我通过为摩托车画一条简单的线,为汽车画一条虚线,使操作变得非常简单。)方法GetVehicle()返回最终的乘积。Product类非常容易理解,虽然我在其中使用了 LinkedList 数据结构,但是您可以出于类似的目的使用任何您喜欢的数据结构。

最后,Director类负责使用IBuilder接口构建这些产品的最终部分。(参见图 3-1 中 GoF 定义的结构。)因此,在我们的代码中,Director类如下所示。

class Director
    {
        IBuilder builder;
        /*
         * A series of steps.In real life, these steps
         * can be much more complex.
         */
        public void Construct(IBuilder builder)
        {
            this.builder = builder;
            builder.StartUpOperations();
            builder.BuildBody();
            builder.InsertWheels();
            builder.AddHeadlights();
            builder.EndOperations();
        }
    }

一个Director对象调用这个Construct()方法来创建不同类型的车辆。

现在让我们浏览一下代码,看看不同的部分是如何组装成这个模式的。

类图

图 3-2 显示了类图。

img/463942_2_En_3_Fig2_HTML.jpg

图 3-2

类图

解决方案资源管理器视图

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

img/463942_2_En_3_Fig3_HTML.jpg

图 3-3

解决方案资源管理器视图

Note

长话短说,我没有扩展汽车和摩托车类。这些类实现了IBuilder,很容易理解。如果需要的话也可以参考类图(见图 3-2 )。对于这本书的其他一些截图,我遵循了相同的机制;就是当一个截图真的很大的时候,我只显示重要的部分。

演示 1

在这个例子中,我为所有不同的玩家使用不同的文件。下面是完整的实现。

// IBuilder.cs

namespace BuilderPatternSimpleExample
{
    // The common interface
    interface IBuilder
    {
        void StartUpOperations();
        void BuildBody();
        void InsertWheels();
        void AddHeadlights();
        void EndOperations();
        Product GetVehicle();
    }
}

// Car.cs

namespace BuilderPatternSimpleExample
{
    // Car is a ConcreteBuilder
    class Car : IBuilder
    {
        private string brandName;
        private Product product;
        public Car(string brand)
        {
            product = new Product();
            this.brandName = brand;
        }
        public void StartUpOperations()
        {   // Starting with brandname
            product.Add("-----------");
            product.Add($"Car model name :{this.brandName}");
        }
        public void BuildBody()
        {
            product.Add("This is a body of a Car");
        }
        public void InsertWheels()
        {
            product.Add("4 wheels are added");
        }

        public void AddHeadlights()
        {
            product.Add("2 Headlights are added");
        }
        public void EndOperations()
        {
            product.Add("-----------");
        }
        public Product GetVehicle()
        {
            return product;
        }
    }
}

// Motorcycle.cs

namespace BuilderPatternSimpleExample
{
    // Motorcycle is another ConcreteBuilder
    class Motorcycle : IBuilder
    {
        private string brandName;
        private Product product;
        public Motorcycle(string brand)
        {
            product = new Product();
            this.brandName = brand;
        }
        public void StartUpOperations()
        {
            product.Add("_________________");
        }

        public void BuildBody()
        {
            product.Add("This is a body of a Motorcycle");
        }

        public void InsertWheels()
        {
            product.Add("2 wheels are added");
        }

        public void AddHeadlights()
        {
            product.Add("1 Headlights are added");
        }
        public void EndOperations()
        {
            // Finishing up with brandname
            product.Add($"Motorcycle model name :{this.brandName}");
            product.Add("_________________");
        }
        public Product GetVehicle()
        {
            return product;
        }
    }
}

// Product.cs

using System;
using System.Collections.Generic; // For LinkedList

namespace BuilderPatternSimpleExample
{
    // "Product"
    class Product
    {
        /*
        You can use any data structure that you prefer     e.g.List<string> etc.
        */
        private LinkedList<string> parts;
        public Product()
        {
            parts = new LinkedList<string>();
        }

        public void Add(string part)
        {
            // Adding parts
            parts.AddLast(part);
        }

        public void Show()
        {
            Console.WriteLine("\nProduct completed as below :");
            foreach (string part in parts)
                Console.WriteLine(part);
        }
    }
}

// Director.cs

namespace BuilderPatternSimpleExample
{
    // "Director"
    class Director
    {
        private IBuilder builder;
        /*
         * A series of steps.In real life, these steps
         * can be much more complex.
         */
        public void Construct(IBuilder builder)
        {
            this.builder = builder;
            builder.StartUpOperations();
            builder.BuildBody();
            builder.InsertWheels();
            builder.AddHeadlights();
            builder.EndOperations();
        }
    }
}

// Client (Program.cs)

using System;
namespace BuilderPatternSimpleExample
{
   class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Builder Pattern Demo.***");
            Director director = new Director();

            IBuilder b1 = new Car("Ford");
            IBuilder b2 = new Motorcycle("Honda");
            // Making Car
            director.Construct(b1);
            Product p1 = b1.GetVehicle();
            p1.Show();

            // Making Motorcycle
            director.Construct(b2);
            Product p2 = b2.GetVehicle();
            p2.Show();

            Console.ReadLine();
        }
    }
}

输出

这是输出。

***Builder Pattern Demo.***

Product completed as below :
-----------
Car model name :Ford
This is a body of a Car
4 wheels are added
2 Headlights are added
-----------

Product completed as below :
_________________
This is a body of a Motorcycle
2 wheels are added
1 Headlights are added
Motorcycle model name :Honda
_________________

分析

Main(),内部,一个Director实例创建了两个不同的产品,因为我在Construct()方法中传递了两个不同的构建器,该方法只是依次调用了StartUpOperations(), BuildBody(), InsertWheels(), AddHeadlights()EndOperations()方法。此外,不同的构建者对这些方法有不同的实现。

问答环节

3.1 使用构建器模式的 优势 有哪些?

以下是一些优点。

  • 您指导构建器一步一步地构建对象,并且通过隐藏复杂的构建过程的细节来促进封装。当整个建造过程结束时,导演可以从建造者那里取回最终产品。一般来说,在一个高层次上,你似乎只有一个方法来制作完整的产品,但是其他的内部方法也参与了创建过程。因此,你可以更好地控制施工过程。

  • 使用这种模式,相同的构建过程可以产生不同的产品。

  • 您还可以改变产品的内部表示。

3.2 与构建器模式相关的 有哪些缺点?

以下是一些缺点。

  • 如果你想处理易变的对象(可以在以后修改),它是不合适的。

  • 您可能需要复制部分代码。这些重复在某些情况下可能会产生重大影响。

  • 要创建不同类型的产品,您需要创建不同类型的混凝土建筑商。

3.3 在这个模式的例子中,你可以用一个 抽象类 来代替接口吗?

是的。在这个例子中,你可以使用一个抽象类来代替接口。

3.4 如何决定在应用中使用抽象类还是接口?

如果你想要集中的或者默认的行为,抽象类是更好的选择。在这些情况下,您可以提供一些默认的实现。另一方面,接口实现从零开始,并指示规则/契约,如要做什么,但它不会将“如何做”的部分强加于您。此外,当您试图实现多重继承的概念时,最好使用接口。

请记住,如果您需要在一个接口中添加一个新方法,那么您需要跟踪该接口的所有实现,并且您需要将该方法的具体实现放在所有这些地方。在这种情况下,抽象类是更好的选择,因为您可以在具有默认实现的抽象类中添加新方法,并且现有代码可以顺利运行。但是 C# v8 在。NET Core 3.0 也引入了默认接口方法的概念。因此,如果您使用的是 C# v8.0 以上的遗留版本,建议的最后几行是最好的。

以下是 MSDN 社区的一些重要建议。

  • 当你有多个版本的组件时,使用一个抽象类。一旦更新了基类,所有派生类都会自动更新。另一方面,接口一旦创建就不应该更改。

  • 当功能分布在不同的/不相关的对象中时,使用接口。抽象类应该用于共享公共功能的紧密相关的对象。

  • 抽象类允许您部分实现您的类,而接口不包含任何成员的实现(忽略 C# v8.0 中的默认接口方法)。

3.5 在汽车示例中,型号名称添加在开头,但对于摩托车,型号名称添加在结尾。这是故意的吗?

是的。我这样做是为了证明这样一个事实,即每个混凝土建造者都可以决定如何生产最终产品的各个部分。他们有这种自由。

3.6 你为什么为导演 使用单独的班级?您可以使用客户端代码来扮演导演的角色。

没有人强迫你这样做。在前面的实现中,我想在实现中将这个角色与客户端代码分开。但是在接下来的演示中,我使用客户端作为导演。

3.7 什么叫 客户代码

包含Main()方法的类是客户端代码。

你几次提到不同的步骤。你能演示一个用不同的变化和步骤创建最终产品的实现吗?

接得好。您是在要求我展示构建器模式的真正威力。让我们考虑下一个例子。

一种替代实施方式

让我们考虑一个替代实现。它给你更多的灵活性。下面是修改后的实现的主要特征。

  • 为了关注核心设计,在这个实现中,让我们把汽车看作最终产品。

  • 在这个实现中,客户机代码本身扮演着一个指挥者的角色。

  • 和前面的例子一样,IBuilder表示构建器接口,但是这次,我没有使用GetVehicle()方法,而是将其重命名为ConstructCar()

  • 如演示 1 所示,Car类已经实现了接口中定义的所有方法,定义如下:

    interface IBuilder
        {
            /*
             * All these methods return type is IBuilder.
             * This will help us to apply method chaining.
             * I'm also providing values for default arguments.
             */
            IBuilder StartUpOperations(string optionalStartUpMessage = " Making a car for you.");
            IBuilder BuildBody(string optionalBodyType = "Steel");
            IBuilder InsertWheels(int optionalNoOfWheels = 4);
            IBuilder AddHeadlights(int optionalNoOfHeadLights = 2);
            IBuilder EndOperations(string optionalEndMessage = "Car construction is completed.");
            /*Combine the parts and make the final product.*/
            Product ConstructCar();
        }
    
    

请注意,这些方法与前面演示中的方法相似,但是有两个主要的变化:它们的返回类型是IBuilder,并且它们接受可选参数。这为您提供了灵活性——您可以向它们传递参数,也可以简单地忽略它们。但最重要的是,由于返回类型是IBuilder,现在您可以应用方法链接,这就是为什么您会在Main()中看到如下代码段。

  • 在前面的部分中,我没有向EndOperations方法传递任何参数。同样,在我调用StartUpOperations方法之前,我调用了InsertWheelsAddHeadlights方法。这给了客户对象(在这种情况下是导演)自由,他想如何创建最终产品。

  • 最后,Product类如下。

    sealed class Product
        {
            /*
             * You can use any data structure that you prefer
             * e.g. List<string> etc.
             */
            private LinkedList<string> parts;
            public Product()
            {
              parts = new LinkedList<string>();
            }
    
            public void Add(string part)
            {
              // Adding parts
              parts.AddLast(part);
            }
    
            public void Show()
            {
              Console.WriteLine("\nProduct completed as below :");
              foreach (string part in parts)
                    Console.WriteLine(part);
            }
        }
    
    
  • 这次我做了Productsealed,因为我想防止继承。像前面的演示一样,parts 属性是private,,并且在类中没有 setter 方法。所有这些构造都可以帮助您提高不变性(这在接下来的演示中是可选的),这在您使用构建器模式时是经常需要的。您甚至可以从部件声明中排除private修饰符,因为默认情况下类成员拥有私有访问权。

  • 你可以注意到另一点。在客户端代码内部,我使用了customCar and CustomCar2来制造汽车。这些是Product类实例。第一个是静态场,第二个是非静态场。我保留了这两个来给你展示Main()Product类的用法变化。

Product customCar2 = new Car("Sedan")
.InsertWheels(7)
.AddHeadlights(6)
.StartUpOperations("Sedan creation in progress")
.BuildBody()
.EndOperations()//will take default end message
.ConstructCar();
customCar2.Show();

类图

图 3-4 显示了演示 2 中替代实现的修改后的类图。

img/463942_2_En_3_Fig4_HTML.jpg

图 3-4

备选实现的类图

解决方案资源管理器视图

图 3-5 显示了新的解决方案浏览器视图。

img/463942_2_En_3_Fig5_HTML.jpg

图 3-5

解决方案资源管理器视图

演示 2

下面是构建器模式的另一个实现。

using System;
using System.Collections.Generic;

namespace BuilderPatternSecondDemonstration
{
    // The common interface
    interface IBuilder
    {
        /*
         * All these methods return types are IBuilder.
         * This will help us to apply method chaining.
         * I'm also providing values for default arguments.
         */
        IBuilder StartUpOperations(string optionalStartUpMessage = "Making a car for you.");
        IBuilder BuildBody(string optionalBodyType = "Steel");
        IBuilder InsertWheels(int optionalNoOfWheels = 4);
        IBuilder AddHeadlights(int optionalNoOfHeadLights = 2);
        IBuilder EndOperations(string optionalEndMessage = "Car construction is complete.");
        // Combine the parts and make the final product.
        Product ConstructCar();
    }

    // Car class
    class Car : IBuilder
    {
        Product product;

        private string brandName;
        public Car(string brand)
        {
            product = new Product();
            this.brandName = brand;
        }
        public IBuilder StartUpOperations(string optionalStartUpMessage = " Making a car for you.")
        {   // Starting with brandname
            product.Add(optionalStartUpMessage);
            product.Add($"Car model name :{this.brandName}");
            return this;
        }
        public IBuilder BuildBody(string optionalBodyType = "Steel")
        {

            product.Add(($"Body type:{optionalBodyType}"));
            return this;
        }

        public IBuilder InsertWheels(int optionalNoOfWheels = 4)
        {

            product.Add(($"Wheels:{optionalNoOfWheels.ToString()}"));
            return this;
        }

        public IBuilder AddHeadlights(int optionalNoOfHeadLights = 2)
        {

            product.Add(($"Headlights:{optionalNoOfHeadLights.ToString()}"));
            return this;
        }

        public IBuilder EndOperations(string optionalEndMessage = "Car construction is completed.")
        {
            product.Add(optionalEndMessage);
            return this;
        }
        public Product ConstructCar()
        {
            return product;
        }

    }

    // Product class
    /*
     * Making the class sealed. The attributes are also private and
     * there is no setter methods. These are used to promote immutability.
     */

    sealed class Product
    {
        /* You can use any data structure that you prefer e.g.List<string> etc.*/
        private LinkedList<string> parts;
        public Product()
        {
            parts = new LinkedList<string>();
        }

        public void Add(string part)
        {
            // Adding parts
            parts.AddLast(part);
        }

        public void Show()
        {
            Console.WriteLine("\nProduct completed as below :");
            foreach (string part in parts)
                Console.WriteLine(part);

        }

    }
    // Director class (Client Code)
    class Program
    {
        static Product customCar;
        static void Main(string[] args)
        {
            Console.WriteLine("***Builder Pattern alternative implementation.***");
            /* Making a custom car (through builder)
               Note the steps:
               Step1:Get a builder object with required parameters
               Step2:Setter like methods are used.They will set the optional fields also.
               Step3:Invoke the ConstructCar() method to get the final car.
             */
            customCar = new Car("Suzuki Swift").StartUpOperations()//will take default message
                    .AddHeadlights(6)
                    .InsertWheels()//Will consider default value
                    .BuildBody("Plastic")
                    .EndOperations("Suzuki construction Completed.")
                    .ConstructCar();

            customCar.Show();
             /*
             Making another custom car (through builder) with a different sequence and steps.
             */
            // Directly using the Product class now.
            // (Just for a variation of usage)
            Product customCar2 = new Car("Sedan")
                     .InsertWheels(7)
                     .AddHeadlights(6)
                     .StartUpOperations("Sedan creation in progress")
                     .BuildBody()
                     .EndOperations() // will take default end message
                     .ConstructCar();
            customCar2.Show();
        }
    }

}

输出

这是新的输出。粗体行是为了让您注意输出中的差异。

***Builder Pattern alternative implementation.***

Product completed as below :
Making a car for you.
Car model name :Suzuki Swift
Headlights:6
Wheels:4
Body type:Plastic
Suzuki construction Completed.

Product completed as below :
Wheels:7
Headlights:6
Sedan creation in progress
Car model name :Sedan
Body type:Steel
Car construction is completed.

分析

仔细看一下Main()方法。您可以看到,主管(客户)可以使用构建器创建两个不同的产品,并且每次都遵循不同的步骤序列。这使得您的应用非常灵活。

问答环节

你在试图推广不变性。与 不可变对象 相关的关键好处是什么?

一旦构造好,就可以安全地共享它们,最重要的是,它们是线程安全的,并且在多线程环境中可以节省同步成本。

3.10 什么时候我应该考虑使用构建器模式?

如果您需要制作一个复杂的对象,它涉及到构建过程的各个步骤,同时,产品需要是不可变的,那么 Builder 模式是一个不错的选择。***

四、工厂方法模式

本章涵盖了工厂方法模式。

Note

为了更好地理解这种模式,我建议你首先阅读第二十四章,它涵盖了简单工厂模式。简单工厂模式没有直接落入四人组设计模式,所以它出现在本书的第二部分;然而,如果您首先理解简单工厂模式的优点和缺点,工厂方法模式会更有意义。

GoF 定义

定义一个创建对象的接口,但是让子类决定实例化哪个类。工厂方法让一个类将实例化推迟到子类。

概念

这里,您从定义应用基本结构的抽象 creator 类开始,子类(从这个抽象类派生)负责执行实际的实例化过程。当你分析下面的例子时,这个概念就会对你有意义。

真实世界的例子

简单工厂模式的例子也适用于此。例如,在餐馆中,根据顾客的喜好,厨师可以在最终产品的准备过程中添加更多(或更少)的香料、油等。

让我们看另一个例子。假设一家汽车制造公司每年生产不同型号的汽车。根据他们的市场调查,他们决定一个模型,并开始生产。基于汽车的模型,不同的零件被制造和组装。一家公司应该随时准备好应对客户未来可能选择更好模式的变化。如果公司需要为只需要几个新功能的新模型创建一个全新的设置,这可能会极大地影响公司的利润率。因此,该公司应该以这样一种方式建立工厂,它可以很容易地为即将到来的模型生产零件。

计算机世界的例子

在数据库编程中,您可能需要支持不同的数据库用户。例如,一个用户可能使用 SQL Server,而另一个用户可能选择 Oracle。当你需要向你的数据库中插入数据时,你首先要创建一个连接对象,比如SqlConnection或者OracleConnection,然后才能继续。如果您将代码放入一个if-else块(或switch语句),您可能需要重复许多相似的代码,这不容易维护。此外,每当您决定开始支持一种新的连接类型时,您需要重新打开代码并进行一些修改。使用工厂方法模式可以解决这类问题。

履行

接下来的例子提供了一个名为AnimalFactory的抽象 creator 类来定义基本结构。根据定义,实例化过程是通过从这个抽象类派生的子类来执行的。这个例子里有很多小类。我可以为每个类创建单独的文件,这种方法经常被许多开发人员所鼓励。但是这些类非常简短、简单和直接。所以,我把它们放在一个文件里。对于本书中类似的例子,我遵循同样的原则。

类图

图 4-1 为类图。

img/463942_2_En_4_Fig1_HTML.jpg

图 4-1

类图

解决方案资源管理器视图

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

img/463942_2_En_4_Fig2_HTML.jpg

图 4-2

解决方案资源管理器视图

演示 1

下面是实现。类似于第二十四章中的简单工厂模式,我使用相同的继承层次结构;也就是说,这一次,您看到DogTiger类都实现了它们的父接口IAnimalAboutMe()方法。因此,您会在示例的开头看到下面的代码段。

public interface IAnimal
    {
        void AboutMe();
    }
    public class Dog : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The dog says: Bow-Wow. I prefer barking.");
        }
    }
    public class Tiger : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The tiger says: Halum. I prefer hunting.");
        }
    }

您可以看到另一个继承层次结构,其中两个具体的类——称为DogFactoryTigerFactory—创建了 dog 和 tiger 对象。它们中的每一个都继承自一个抽象类AnimalFactory。这两个具体的类推迟了实例化过程。我加入了支持性的评论来帮助你更好地理解。下面的代码段描述了它。

public abstract class AnimalFactory
    {
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." The following method will create a tiger or a dog object, but at this point it does not know whether it will get a dog or a tiger. It will be decided by
        the subclasses i.e. DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal();
    }
    // DogFactory is used to create dog
    public class DogFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Dog
            return new Dog();
        }
    }
    // TigerFactory is used to create tigers
    public class TigerFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Tiger
            return new Tiger();
        }
    }

这是完整的演示。

using System;

namespace FactoryMethodPattern
{
    #region Animal Hierarchy
    /*
     * Both the Dog and Tiger classes will
     * implement the IAnimal interface method.
     */
    public interface IAnimal
    {
        void AboutMe();
    }
    // Dog class
    public class Dog : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The dog says: Bow-Wow. I prefer barking.");
        }
    }
    //Tiger class
    public class Tiger : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The tiger says: Halum. I prefer hunting.");
        }
    }
    #endregion

    #region Factory Hierarchy

    // Both DogFactory and TigerFactory will use this.
    public abstract class AnimalFactory
    {
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." The following method will create a Tiger
        or a Dog, but at this point it does not know whether
        it will get a dog or a tiger. It will be decided by
        the subclasses i.e. DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal();
    }
    // DogFactory is used to create dog
    public class DogFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Dog
            return new Dog();
        }
    }
    // TigerFactory is used to create tigers
    public class TigerFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal()
        {
            // Creating a Tiger
            return new Tiger();
        }
    }
    #endregion
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Factory Pattern Demo.***\n");
            // Creating a Tiger Factory
            AnimalFactory tigerFactory = new TigerFactory();
            // Creating a tiger using the Factory Method
            IAnimal tiger = tigerFactory.CreateAnimal();
            tiger.AboutMe();

            // Creating a DogFactory
            AnimalFactory dogFactory = new DogFactory();
            // Creating a dog using the Factory Method
            IAnimal dog = dogFactory.CreateAnimal();
            dog.AboutMe();

            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Factory Pattern Demo.***

The tiger says: Halum. I prefer hunting.
The dog says: Bow-Wow. I prefer barking.

修改后的实现 1

现在让我们来看看您可以对演示 1 进行的两个重要修改。

在第一个修改的实现中,更多的灵活性被添加到我们先前的实现中。注意,AnimalFactory类是一个抽象类,所以你可以利用它。假设您希望一个子类遵循一个可以从其父类(或基类)强加的规则。为简单起见,让我们通过控制台消息来实施该规则,如下面的演示所示。

部分演示 1

在修改后的实现中,我在AnimalFactory class中引入了一个叫做MakeAnimal()的新方法。

// Modifying the AnimalFactory class.
public abstract class AnimalFactory
    {
      public IAnimal MakeAnimal()
            {
                 Console.WriteLine("AnimalFactory.MakeAnimal()-You cannot ignore parent rules.");
                 IAnimal animal = CreateAnimal();
                 animal.AboutMe();
                 return animal;
            }
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." Following method will create a Tiger
        or a Dog class, but at this point it does not know whether
        it will get a dog or a tiger. It will be decided by
        the subclasses i.e.DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal();
    }

客户端代码采用了这些更改;也就是说,不是先调用CreateAnimal()再使用AboutMe()。我只是在下面的代码段中调用了MakeAnimal()。旧代码被注释以供参考,并与新代码进行比较。

class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Factory Pattern Modified Demo.***\n");
            // Creating a Tiger Factory
            AnimalFactory tigerFactory = new TigerFactory();
            // Creating a tiger using the Factory Method
            //IAnimal tiger = tigerFactory.CreateAnimal();
            //tiger.AboutMe();
            IAnimal tiger = tigerFactory.MakeAnimal();

            // Creating a DogFactory
            AnimalFactory dogFactory = new DogFactory();
            // Creating a dog using the Factory Method
            //IAnimal dog = dogFactory.CreateAnimal();
            //dog.AboutMe();
            IAnimal dog = dogFactory.MakeAnimal();

            Console.ReadKey();
        }
    }

输出

下面是修改后的输出。

***Factory Pattern Modified Demo.***

AnimalFactory.MakeAnimal()-You cannot ignore parent rules.
The tiger says: Halum. I prefer hunting.

AnimalFactory.MakeAnimal()-You cannot ignore parent rules.
The dog says: Bow-Wow. I prefer barking.

分析

现在,在每种情况下,您都会看到以下警告:“…您不能忽略父规则。”这是对演示 1 的增强。

问答环节

4.1 为什么将 CreateAnimal() 方法 从客户端代码中分离出来?

我做这件事只有一个目的。我想让子类创建专门化的对象。如果你仔细观察,你会发现只有这个“创造性部分”在不同的产品中有所不同。我在第二十四章的“问答环节”中详细讨论了这一点。

使用这样的工厂有哪些 优势

以下是一些关键优势。

  • 您将变化的代码与不变化的代码分开(换句话说,使用简单工厂模式的优势仍然存在),这有助于您轻松地维护代码。

  • 代码不是紧密耦合的,所以您可以在系统中随时添加新的类,如LionBear等,而无需修改现有的架构。换句话说,我遵循了“修改时封闭,扩展时开放”的原则。

4.3 使用这样的工厂有哪些 挑战

如果您需要处理许多不同类型的对象,那么系统的整体性能会受到影响。

4.4 工厂方法模式支持两个平行的层次结构。这是正确的吗?

接得好。是的,从类图来看,很明显这种模式支持并行的类层次结构(见图 4-3 )。

img/463942_2_En_4_Fig3_HTML.jpg

图 4-3

本例中的两个类层次结构

在这个例子中,AnimalFactoryDogFactoryTigerFactory被放置在一个层次中,而IAnimalDogTiger被放置在另一个层次中。因此,你可以看到创作者和他们的创作/产品是并行运行的两个层次。

你应该总是用一个抽象的关键字来标记工厂方法,这样子类就可以完成它们。这是正确的吗?

不。当创建者没有子类时,您可能会对默认的工厂方法感兴趣。在这种情况下,不能用关键字abstract标记工厂方法。

然而,要看到工厂方法模式的真正威力,您可能需要遵循这里在大多数情况下实现的设计。

看来工厂方法模式与简单工厂模式没有太大的不同。这是正确的吗?

如果你看看这两章的例子中的子类,你可能会发现一些相似之处。但是你不应该忘记工厂方法模式的主要目的;它为你提供了一个框架,通过这个框架,不同的子类可以生产不同的产品。在简单工厂模式中,您不能以类似的方式改变产品。您可以将简单的工厂模式视为一次性交易,但最重要的是,您的创造性部分不会因为修改而关闭。每当您想要添加新的东西时,您需要在简单工厂模式的工厂类中添加一个if-else块或一个switch语句。

在这种情况下,永远记住 GoF 定义,它说,“工厂方法模式让一个类延迟实例化到子类。”仔细看看修改后的实现。你可以看到CreateAnimal()通过AnimalFactory的适当子类创建了一只狗或一只老虎。所以,CreateAnimal()是这个设计中抽象的工厂方法。当MakeAnimal()在其体内使用CreateAnimal()时,它不知道是对狗还是对老虎有效。AnimalFactory的子类只知道为这个应用创建具体的实现(一只狗或一只老虎)。

Note

System.Web.WebRequest类中,你可以看到Create方法,它有两个重载。在这个方法中,您可以传递一个统一资源标识符(URI)。此方法为请求确定适当的协议并返回适当的子类,例如,HttpWebRequest(如果 URI 以 http://或 https://开头)、FtpWebRequest(如果 URI 以 ftp://开头)等等。如果 URI 从 HTTP 更改为 FTP,底层代码不需要更改,调用者也不需要担心协议的细节。这种架构促进了工厂模式的使用,但是对于新的开发不推荐使用 HttpWebRequest。微软建议你使用系统。请改用. Net.Http.HttpClient 类。

修改后的实现 2

本章以对我们的初始实现的额外更新结束。现在让我们通过使用方法参数来更新演示 1。我们继续吧。当您从 Apress 网站下载代码时,您可以获得完整的实现。为了简洁起见,这里只展示了部分演示。

部分演示 2

这段代码表明,如果在CreateAnimal() .中使用方法参数,可以使原来的实现变得更好,这种方法提供了一个好处。您可以只创建一个具体的工厂类,而不是创建DogFactory, TigerFactory等等,如下所示。

#region Factory Hierarchy

    // Both DogFactory and TigerFactory will use this.
    public abstract class AnimalFactory
    {
        /*
        Remember the GoF definition which says
        "....Factory method lets a class defer instantiation
        to subclasses." Following method will create a Tiger
        or a Dog, but at this point it does not know whether
        it will get a dog or a tiger. It will be decided by
        the subclasses i.e.DogFactory or TigerFactory.
        So, the following method is acting like a factory
        (of creation).
        */
        public abstract IAnimal CreateAnimal(string animalType);
    }
    /*
     * ConcreteAnimalFactory is used to create dogs or tigers
     * based on method parameter of CreateAnimal() method.
     */
    public class ConcreteAnimalFactory : AnimalFactory
    {
        public override IAnimal CreateAnimal(string animalType)
        {
            if (animalType.Contains("dog"))
            {
                // Creating a Dog
                return new Dog();
            }
            else
            if (animalType.Contains("tiger"))
            {
                // Creating a Dog
                return new Tiger();
            }
            else
            {
                throw new ArgumentException("You need to pass either a dog or a tiger as an argument.");
            }
        }
    }

    #endregion

现在你可以在CreateAnimal(...)方法中传递一个“狗”字符串或一个“老虎”字符串来创建一个Dog或一个Tiger实例。为了适应这些变化,您可以按如下方式更新客户端代码。(这一次,animalFactory创建了DogTiger实例。每个人都知道“编程到接口”有这种好处。)

class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Factory Pattern Demo.***");
            Console.WriteLine("***It's a modified version using method parameter(s).***\n");
            // Creating a factory that can produce animals
            AnimalFactory animalFactory = new ConcreteAnimalFactory();
            // Creating a tiger using the Factory Method
            IAnimal tiger = animalFactory.CreateAnimal("tiger");
            tiger.AboutMe();
            // Now creating a dog.
            IAnimal dog = animalFactory.CreateAnimal("dog");
            dog.AboutMe();

            Console.ReadKey();
        }
    }

输出

现在,如果您执行这个程序,您可以得到以下输出。

***Factory Pattern Demo.***
***It's a modified version using method parameter(s).***

The tiger says: Halum. I prefer hunting.
The dog says: Bow-Wow. I prefer barking.

我希望您现在对如何实现工厂方法模式有了更好的理解。提供两个修改的实现作为参考。(Apress 网站上提供了完整的实现。).您可以决定是否要在您的程序中使用这些修改中的一个(或两个)。但是您应该记住,工厂方法应该在幕后为客户机创建适当的对象,这是该模式的终极座右铭。

五、抽象工厂模式

本章涵盖了抽象工厂模式。

GoF 定义

提供创建相关或依赖对象系列的接口,而无需指定它们的具体类。

Note

如果你理解简单工厂模式(第章第二十四部分)和工厂方法模式(第章第四部分),抽象工厂模式对你来说会更有意义。简单工厂模式并不直接属于四人组设计模式,所以对该模式的讨论出现在本书的第二部分。我建议你先阅读第四章和第二十四章,然后再开始阅读这一章。

概念

抽象工厂通常被称为工厂中的工厂。这种模式提供了一种封装一组具有共同主题的单个工厂的方法。在这个过程中,你不直接实例化一个类;相反,您实例化一个具体的工厂,然后使用该工厂创建产品。

在我们接下来的例子中,实例化了一个工厂实例(animalFactory)。通过使用这个工厂实例,我创建了狗和老虎实例(狗和老虎是最终产品),这就是为什么您会在客户端代码中看到下面的代码段。

// Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();

当产品相似,但产品系列不同时,这种模式最适合(例如,家养的狗和野生的狗很不一样)。这种模式有助于您交换特定的实现,而无需更改使用它们的代码,甚至在运行时也是如此。但是,这可能会导致不必要的复杂性和额外的工作。在某些情况下,甚至调试也变得很困难。

真实世界的例子

假设你用两种不同类型的桌子装饰你的房间;一个是木制的,另一个是钢制的。对于木制的,你需要拜访一个木匠,对于其他类型的,你可能需要去一个金属商店。所有这些都是桌子工厂。所以,基于需求,你决定你需要什么样的工厂。

计算机世界的例子

ADO.NET 实现了类似的概念来建立到数据库的连接。

履行

维基百科描述了这种模式的典型结构,类似于图 5-1 (见 https://en.wikipedia.org/wiki/Abstract_factory_pattern )所示。

img/463942_2_En_5_Fig1_HTML.jpg

图 5-1

抽象工厂模式

在本章的实现中,我遵循类似的结构。在这个例子中,有两种动物:宠物和野生动物。Program.cs客户正在寻找一些动物(在本例中是野狗、宠物狗、野生老虎和宠物老虎)。在这个实现中,您将探索宠物和野生动物的构造过程。

IAnimalFactory是一个抽象工厂。名为WildAnimalFactoryPetAnimalFactory的两个具体工厂继承自这个抽象工厂。你可以看到这些混凝土工厂负责创造狗和老虎的混凝土产品。顾名思义,WildAnimalFactory创造野生动物(野狗、野虎),而PetAnimalFactory创造宠物(宠物狗、宠物虎)。下面总结了参与者及其角色。

  • IAnimalFactory:抽象工厂

  • WildAnimalFactory:实现IAnimalFactory的混凝土工厂;它创造了野狗和野生老虎

  • PetAnimalFactory:实现IAnimalFactory,的混凝土工厂,但是这个工厂制造宠物狗和宠物老虎

  • ITigerIDog:抽象产品

  • PetTigerPetDogWildTigerWildDog:混凝土制品。PetTigerWildTiger实现ITiger接口。PetDogWildDog实现IDog接口。IDogITiger接口只有一个方法AboutMe(),在简单工厂模式和工厂方法模式中都使用。

  • 客户端代码中使用了一个名为FactoryProvider的静态类,如下所示:

    // Making a wild dog and wild tiger through
    // WildAnimalFactory
    IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
    IDog dog = animalFactory.GetDog();
    ITiger tiger = animalFactory.GetTiger();
    dog.AboutMe();
    tiger.AboutMe();
    
    
  • 从前面代码段中的粗线可以看出,我是而不是直接实例化工厂实例;相反,我使用FactoryProvider静态类来获取工厂实例。(这个类的结构类似于在工厂方法模式中使用具体工厂时的结构。)FactoryProvider根据在GetAnimalFactory(...)方法中传递的参数提供合适的工厂。

类图

图 5-2 显示了类图。

img/463942_2_En_5_Fig2_HTML.jpg

图 5-2

类图

解决方案资源管理器视图

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

img/463942_2_En_5_Fig3_HTML.jpg

图 5-3

解决方案资源管理器视图

演示 1

这是完整的程序。

using System;

namespace AbstractFactoryPattern
{
    // Abstract Factory
    public interface IAnimalFactory
    {
        IDog GetDog();
        ITiger GetTiger();
    }

    // Abstract Product-1
    public interface ITiger
    {
        void AboutMe();
    }
    // Abstract Product-2
    public interface IDog
    {
        void AboutMe();
    }

    // Concrete product-A1(WildTiger)
    class WildTiger : ITiger
    {
        public void AboutMe()
        {
            Console.WriteLine("Wild tiger says: I prefer hunting in jungles. Halum.");
        }
    }
    // Concrete product-B1(WildDog)
    class WildDog : IDog
    {
        public void AboutMe()
        {
            Console.WriteLine("Wild dog says: I prefer to roam freely in jungles. Bow-Wow.");
        }
    }

    // Concrete product-A2(PetTiger)
    class PetTiger : ITiger
    {
        public void AboutMe()
        {
            Console.WriteLine("Pet tiger says: Halum. I play in an animal circus.");
        }
    }

    // Concrete product-B2(PetDog)
    class PetDog : IDog
    {
        public void AboutMe()
        {
            Console.WriteLine("Pet dog says: Bow-Wow. I prefer to stay at home.");
        }
    }
    // Concrete Factory 1-Wild Animal Factory
    public class WildAnimalFactory : IAnimalFactory
    {

        public ITiger GetTiger()
        {
            return new WildTiger();
        }
        public IDog GetDog()
        {
            return new WildDog();
        }
    }
    // Concrete Factory 2-Pet Animal Factory
    public class PetAnimalFactory : IAnimalFactory
    {
        public IDog GetDog()
        {
            return new PetDog();
        }

        public ITiger GetTiger()
        {
            return new PetTiger();
        }
    }
    // Factory provider
    class FactoryProvider
    {
        public static IAnimalFactory GetAnimalFactory(string factoryType)
        {
            if (factoryType.Contains("wild"))
            {
                // Returning a WildAnimalFactory
                return new WildAnimalFactory();
            }
            else
           if (factoryType.Contains("pet"))
            {
                // Returning a PetAnimalFactory
                return new PetAnimalFactory();
            }
            else
            {
                throw new ArgumentException("You need to pass either wild or pet as argument.");
            }
        }
    }

    // Client
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Abstract Factory Pattern Demo.***\n");            // Making a wild dog and wild tiger through WildAnimalFactory
            IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
            IDog dog = animalFactory.GetDog();
            ITiger tiger = animalFactory.GetTiger();
            dog.AboutMe();
            tiger.AboutMe();

            Console.WriteLine("******************");

            // Making a pet dog and pet tiger through PetAnimalFactory now.
            animalFactory = FactoryProvider.GetAnimalFactory("pet");
            dog = animalFactory.GetDog();
            tiger = animalFactory.GetTiger();
            dog.AboutMe();
            tiger.AboutMe();

            Console.ReadLine();
        }
    }
}

输出

这是输出。

***Abstract Factory Pattern Demo.***

Wild dog says: I prefer to roam freely in jungles. Bow-Wow.
Wild tiger says: I prefer hunting in jungles. Halum.
******************
Pet dog says: Bow-Wow.I prefer to stay at home.
Pet tiger says: Halum.I play in an animal circus.

问答环节

5.1IDog ITiger 接口 都包含同名的方法。例如,两个接口都包含 AboutMe() 方法 。这是强制性的吗?

不,你可以为你的方法使用不同的名字。此外,这些接口中方法的数量可以不同。然而,在第二十四章中,我介绍了简单工厂模式,在第四章中,我介绍了工厂方法模式。在这一章中,我继续例子,这就是为什么我保持同样的方法。

使用这样一个抽象工厂有哪些挑战

**抽象工厂中的任何变化都会迫使您将修改传播到具体工厂。标准设计理念建议你对接口编程,而不是对实现编程。这是开发人员应该始终牢记的关键原则之一。在大多数情况下,开发人员不想改变他们的抽象工厂。

此外,整体架构很复杂,这就是为什么在某些情况下调试非常具有挑战性。

如何区分简单工厂模式 和工厂方法模式或者抽象工厂模式?

我将在第四章的“问答”部分讨论简单工厂模式和工厂方法模式的区别。

让我们修改一下客户端代码如何使用这些工厂,如下图所示。下面是简单工厂模式的代码片段。

IAnimal preferredType = null;
SimpleFactory simpleFactory = new SimpleFactory();
#region The code region that can vary based on users preference
/*
* Since this part may vary, we're moving the
* part to CreateAnimal() in SimpleFactory class.
*/
preferredType = simpleFactory.CreateAnimal();
#endregion

#region The codes that do not change frequently.
preferredType.AboutMe();
#endregion

图 5-4 显示了简单的工厂模式。

img/463942_2_En_5_Fig4_HTML.jpg

图 5-4

简单工厂模式

下面是工厂方法模式的代码片段。

// Creating a Tiger Factory
AnimalFactory tigerFactory = new TigerFactory();
// Creating a tiger using the Factory Method
IAnimal tiger = tigerFactory.CreateAnimal();
tiger.AboutMe();

// Creating a DogFactory
AnimalFactory dogFactory = new DogFactory();
// Creating a dog using the Factory Method
IAnimal dog = dogFactory.CreateAnimal();
dog.AboutMe();

图 5-5 显示了工厂方法模式。

img/463942_2_En_5_Fig5_HTML.jpg

图 5-5

工厂方法模式

下面是抽象工厂模式的代码片段。

// Making a wild dog and wild tiger through WildAnimalFactory
IAnimalFactory animalFactory = FactoryProvider.GetAnimalFactory("wild");
IDog dog = animalFactory.GetDog();
ITiger tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();

Console.WriteLine("******************");

// Making a pet dog and pet tiger through PetAnimalFactory now.
animalFactory = FactoryProvider.GetAnimalFactory("pet");
dog = animalFactory.GetDog();
tiger = animalFactory.GetTiger();
dog.AboutMe();
tiger.AboutMe();

图 5-6 显示了抽象工厂模式。

img/463942_2_En_5_Fig6_HTML.jpg

图 5-6

抽象工厂模式

简而言之,使用简单工厂模式,您可以将不同于其他代码的代码分离出来(基本上,您可以将客户端代码解耦)。这种方法有助于您更轻松地管理代码。这种方法的另一个主要优点是客户端不知道对象是如何创建的。因此,它同时促进了安全性和抽象性。

但是,这种方法会违反开闭原则。使用工厂方法模式可以克服这个缺点,工厂方法模式允许子类决定如何完成实例化过程。简而言之,您将对象创建委托给实现工厂方法来创建对象的子类。

抽象工厂基本上是工厂的工厂。它创建了一系列相关的对象,但是它不依赖于具体的类。在这个模式中,您封装了一组具有共同主题的独立工厂。在这个过程中,你不直接实例化一个类;取而代之的是,您得到一个具体的工厂(我为此使用了一个提供者),然后,使用该工厂创建产品。

最后,我试图保持例子简单。工厂方法促进继承,它的子类需要实现工厂方法来创建对象。抽象工厂模式可以通过使用工厂接口中公开的方法创建相关对象来促进对象组合。最后,所有的工厂都通过减少对具体类的依赖来促进松散耦合。**

六、代理模式

本章介绍代理模式。

GoF 定义

为另一个对象提供代理或占位符,以控制对它的访问。

概念

您需要支持这种设计,因为在许多情况下,与原始对象的直接通信并不总是可能的。这是由许多因素造成的,包括安全性和性能问题、资源限制、最终产品处于开发阶段等等。代理可以是不同的类型,但从根本上说,它是原始对象的替代物(或占位符)。因此,当客户端与代理对象交互时,看起来它是在直接与实际对象对话。因此,使用这种模式,您可能希望使用一个可以作为原始类的接口的类。

真实世界的例子

在教室里,当一个学生缺席时,他最好的朋友可能会在点名时试图模仿他的声音,让老师认为他的朋友在那里。除了这个例子,您还可以考虑另一个领域的例子,例如,ATM。ATM 实现可以保存远程服务器上的银行信息的代理对象。

计算机世界的例子

在真实的编程世界中,创建一个复杂对象的多个实例可能成本很高,因为您可能需要不容易获得或分配的资源。在这种情况下,您可以创建多个可以指向原始对象的代理对象。这种机制可以帮助您节省计算机/系统内存并提高应用的性能。

代理的另一个常见用途是当用户不想公开他/她的机器的真实 IP 地址并使其匿名时。

在 WCF 应用中,您可能会注意到 WCF 客户端代理,客户端应用使用它与服务进行通信。您还可以配置一个 REST API 在代理服务器后面工作,以促进授权的通信。

履行

在这个程序中,Subject是一个抽象类,它有一个名为DoSomeWork() .的抽象方法,如下所示。

public abstract class Subject
    {
        public abstract void DoSomeWork();
    }

ConcreteSubject是一个继承自Subject的具体类,完成了DoSomeWork()方法。所以,看起来是这样的。

public class ConcreteSubject : Subject
    {
        public override void DoSomeWork()
        {
            Console.WriteLine("I've processed your request.");
        }
    }

让我们假设您想要限制客户端直接调用ConcreteSubject中的方法。(考虑一下计算机世界例子中讨论的案例,这背后有一些原因。)所以,你做了一个名为Proxy的代理类。在我们的实现中,Proxy类还包含一个名为DoSomeWork()的方法,客户端可以通过一个Proxy实例来使用这个方法。当客户端调用代理对象的DoSomeWork()方法时,这个调用又被传播到ConcreteSubject对象中的DoSomeWork()方法。这让客户感觉好像他们直接调用了来自ConcreteSubject的方法,这就是为什么Proxy类看起来像下面这样。

public class Proxy : Subject
    {
        Subject subject;

        public override void DoSomeWork()
        {
            Console.WriteLine("Welcome, my client.");
            /*
            Lazy initialization:We'll not instantiate until
            the method is called.
            */
            if (subject == null)
            {
                subject = new ConcreteSubject();
            }
            subject.DoSomeWork();
        }
    }

类图

图 6-1 为类图。

img/463942_2_En_6_Fig1_HTML.jpg

图 6-1

类图

解决方案资源管理器视图

图 6-2 显示了程序的高层结构。(注意,您可以将代理类分离到一个不同的文件中,但是由于本例中的各个部分都很小,所以我将所有内容都放在一个文件中。同样的评论也适用于本书中的其他程序。)

img/463942_2_En_6_Fig2_HTML.jpg

图 6-2

解决方案资源管理器视图

演示 1

下面是完整的实现。

using System;

namespace ProxyPatternDemo
{
    /// <summary>
    /// Abstract class Subject
    /// </summary>
    public abstract class Subject
    {
        public abstract void DoSomeWork();
    }
    /// <summary>
    /// ConcreteSubject class
    /// </summary>
    public class ConcreteSubject : Subject
    {
        public override void DoSomeWork()
        {
            Console.WriteLine("I've processed your request.");
        }
    }
    /// <summary>
    /// Proxy class
    /// </summary>
    public class Proxy : Subject
    {
        Subject subject;

        public override void DoSomeWork()
        {
            Console.WriteLine("Welcome, my client.");
            /*
             Lazy initialization:We'll not instantiate the object until the method is called.
            */
            if (subject == null)
            {
                subject = new ConcreteSubject();
            }
            subject.DoSomeWork();
        }
    }
    /// <summary>
    /// Client class
    /// </summary>

    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Proxy Pattern Demo.***\n");
            Subject proxy = new Proxy();
            proxy.DoSomeWork();
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Proxy Pattern Demo.***

Welcome, my client.
I've processed your request.

问答环节

6.1 有哪些不同类型的代理人

这些是代理的常见类型。

  • 远程代理:这些代理可以隐藏位于不同地址空间的对象。

  • 虚拟代理:这些代理执行优化技术,比如按需创建一个重对象。

  • 保护代理:这些代理一般处理不同的访问权限。

  • 智能引用:当客户端访问对象时执行额外的内务处理。典型的操作可以包括计算在某一时刻对对象的引用次数。

6.2 您可以在代理类构造函数中创建 ConcreteSubject 实例 ,如下所示。

/// <summary>
/// Proxy class
/// </summary>
public class Proxy : Subject
        {
        Subject subject;
        public Proxy()
        {
                // Instantiating inside the constructor
                subject = new ConcreteSubject();
        }
        public override void DoSomeWork()
        {
                Console.WriteLine("Proxy call happening now..");
                cs.DoSomeWork();
        }
}

这是正确的吗?

是的,你可以这样做。但是不要忘记代理类可以有不依赖于ConcreteSubject的额外方法。因此,如果您需要来自代理类的这些方法,并且遵循您提出的设计,无论何时您实例化一个代理对象,您也实例化了一个ConcreteSubject类的对象。因此,这最终可能会创建不必要的对象。

6.3 使用这个 惰性实例化过程 ,你可能会在多线程应用中创建不必要的对象。这是正确的吗?

是的。这是一个简单的插图,让您了解实际模式背后的核心思想。在第一章对单例模式的讨论中,我们分析了一些告诉你如何在多线程环境中工作的替代方法。在这种情况下,你可以参考这些讨论。(例如,在这种情况下,您可以实现一个智能代理,以确保在授予对象访问权限之前锁定该对象)。

6.4 能否举一个 远程代理 的例子?

假设您想要调用一个对象的方法,但是该对象正在不同的地址空间中运行(例如,在不同的位置或不同的计算机上)。你是如何进行的?在远程代理的帮助下,您可以调用代理对象上的方法,该方法又将调用转发给远程计算机上运行的实际对象。(如果实际的方法存在于不同的计算机上,并且您通过网络上的代理对象连接到它,那么演示 1 就是这种情况下的一个例子。)这种类型的需求可以通过不同的公知机制来实现,例如 ASP.NET、CORBA、COM/DCOM 或 Java 的 RMI。在 C# 应用中,您可以使用 WCF(.NET Framework 版及更高版本)或。NET web 服务/remoting(主要用于早期版本)。值得注意的是。NET remoting 不被支持。NET Core,微软未来也不打算增加这种支持(见 https://docs.microsoft.com/en-us/dotnet/core/porting/net-framework-tech-unavailable#:~:text=NET%20Remoting%20isn't%20supported 、IO)。

图 6-3 显示了一个简单的远程代理结构。

img/463942_2_En_6_Fig3_HTML.jpg

图 6-3

一个简单的远程代理图

6.5 你什么时候使用 虚拟代理

虚拟代理保护内存不被分配给对象。如果实际的对象创建是一个昂贵的操作,您可以创建一个包含最重要细节的目标对象的简单副本,并将其提供给用户。昂贵的对象只有在真正需要的时候才会被创建。例如,您可以使用这个概念来避免加载不必要的超大图像,以获得更好的应用性能。

6.6 什么时候使用 保护代理

在组织中,安全团队可以实施保护代理来阻止对特定网站的 Internet 访问。

考虑下面的例子,它是前面描述的代理模式实现的修改版本。为了简单起见,让我们假设只有三个注册用户可以使用DoSomeWork()代理方法。如果一个不需要的用户(名为 Robin)试图调用该方法,系统会拒绝他的访问请求。当系统拒绝这种不需要的访问时,创建代理对象就没有意义了。在接下来的例子中,这些注册用户在代理类构造函数中初始化,但是我避免了在其中实例化一个ConcreteSubject对象。它帮助我避免为未授权用户创建不必要的对象。

现在让我们来看一下修改后的实现。

演示 2

下面是修改后的实现。

using System;
using System.Linq; // For Contains() method below

namespace ProxyPatternQAs
{
    /// <summary>
    /// Abstract class Subject
    /// </summary>
    public abstract class Subject
    {
        public abstract void DoSomeWork();
    }
    /// <summary>
    /// ConcreteSubject class
    /// </summary>
    public class ConcreteSubject : Subject
    {
        public override void DoSomeWork()
        {
            Console.WriteLine("I've processed your request.\n");
        }
    }
    /// <summary>
    /// Proxy class
    /// </summary>
    public class Proxy : Subject
    {
        Subject subject;
        string[] registeredUsers;
        string currentUser;
        public Proxy(string currentUser)
        {
            /*
             * Avoiding to instantiate ConcreteSubject
             * inside the Proxy class constructor.
             */
            //subject = new ConcreteSubject();

            // Registered users
            registeredUsers = new string[] { "Admin", "Rohit", "Sam" };
            this.currentUser = currentUser;
        }
        public override void DoSomeWork()
        {
            Console.WriteLine($"{currentUser} wants to access into the system.");
            if (registeredUsers.Contains(currentUser))
            {
                Console.WriteLine($"Welcome, {currentUser}.");
                /* Lazy initialization: We'll not instantiate until the method is called through an authorized user. *.
                if (subject == null)

                {
                    subject = new ConcreteSubject();
                }
                subject.DoSomeWork();
            }
            else
            {
                Console.WriteLine($"Sorry {currentUser}, you do not have access into the system.");
            }
        }
    }
    /// <summary>
    /// Client
    /// </summary>

    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Proxy Pattern Demo2.***\n");
            // Authorized user-Admin
            Subject proxy = new Proxy("Admin");
            proxy.DoSomeWork();
            // Authorized user-Sam
            proxy = new Proxy("Sam");
            proxy.DoSomeWork();
            // Unauthorized User-Robin
            proxy = new Proxy("Robin");
            proxy.DoSomeWork();
            Console.ReadKey();
        }
    }
}

输出

这是修改后的输出。

***Proxy Pattern Demo2.***

Admin wants to access into the system.
Welcome, Admin.
I've processed your request.

Sam wants to access into the system.
Welcome, Sam.
I've processed your request.

Robin wants to access into the system.
Sorry Robin, you do not have access into the system.

看起来代理人就像装饰者一样(见第七章)。这是正确的吗?

有时候代理实现和装饰器有一些相似之处,但是你不应该忘记代理的真正意图。装饰者专注于增加职责,而代理者专注于控制对对象的访问。所以,如果你记得它们的用途,在大多数情况下,你可以区分代理和装饰者。

什么时候我应该考虑设计一个代理?

下面是代理可以帮助您的一些重要用例。

  • 您正在为一个仍处于开发阶段或很难重现的场景编写测试用例。例如,当您想要评估应用中只能在客户环境中看到的行为,但您也认识到当应用正在运行时,获得该行为的概率非常低。在这种情况下,您可以在您的代理对象中模拟客户环境行为,并执行您的测试用例来评估该行为的正确性。您不希望您的客户端直接与目标对象对话。

  • 你想隐藏复杂性,增强系统的安全性。

6.9 与代理相关的 缺点有哪些?

使用这种模式时,您应该记住以下一些因素。

  • 总的响应时间可能是一个问题,因为您没有直接与实际的对象对话。

  • 您需要为代理维护额外的层。

  • 代理可以隐藏对象的实际响应,这在某些情况下可能会造成混乱。

七、装饰模式

本章涵盖了装饰模式。

GoF 定义

动态地将附加责任附加到对象上。Decorators 为扩展功能提供了子类化的灵活替代方案。

概念

从 GoF 定义来看,很明显这种模式使用了子类化的替代方法(即继承)。如果不允许继承,怎么进行?是的,你猜对了。它规定你用合成代替继承。

通过遵循 SOLID 原则,这种模式推广了这样一个概念,即类对修改是封闭的,但对扩展是开放的。(如果你想了解更多坚实的原理,去 https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) 。)使用这种模式,您可以在不改变底层类的情况下向特定对象添加特殊功能。

装饰器就像一个包装器(或顶层),包裹着原始对象,并为其添加额外的功能。这就是装饰模式也被称为包装模式的原因。当您动态添加装饰者时,这种模式是最有效的。由于 decorators 通常是动态添加的,所以如果您不希望在开发的后期阶段使用它们,这完全没问题,因为原始对象仍然可以工作。

真实世界的例子

假设你有一栋单层的房子,你决定在上面加建一层。您可能不希望更改底层的架构,但是您可能希望为新添加的楼层采用一种新的设计,以适合现有架构的顶部。

图 7-1 、 7-2 和 7-3 说明了这个概念。

img/463942_2_En_7_Fig3_HTML.jpg

图 7-3

在已有装饰者的基础上再加一个装饰者,并修改房子(现在粉刷房子)

img/463942_2_En_7_Fig2_HTML.jpg

图 7-2

有装修工的原始房屋(附加楼层建在原始结构的顶部)

img/463942_2_En_7_Fig1_HTML.jpg

图 7-1

原始房屋

Note

图 7-3 所示的情况是可选的。您可以使用现有的 decorator 对象来增强行为,或者您可以创建一个新的 decorator 对象并将新的行为添加到其中。在第二步中,你也可以直接油漆原来的房子。一旦添加了新地板,您就不需要开始粉刷了。

计算机世界的例子

假设您想在基于 GUI 的工具包中添加边框属性。您可以使用继承来做到这一点,但这不能被视为最终的解决方案,因为您可能无法从一开始就对所有事情拥有绝对的控制权。因此,这种技术本质上是静态的。

在这种情况下,装饰者可以为你提供一种灵活的方法。他们推广动态选择的概念。例如,您可以将组件包装在另一个对象中(类似于图 7-2 和 7-3 )。封闭对象被称为装饰器,它必须符合它所装饰的组件的接口。它将请求转发给原始组件,并可以在这些请求之前或之后执行附加操作。事实上,这个概念允许您添加无限数量的责任。

履行

在这个例子中,涉及五个玩家:AbstractHome, ConcreteHome, AbstractDecorator, FloorDecoratorPaintDecorator

AbstractHome定义如下。

    abstract class AbstractHome
    {
        public double AdditionalPrice { get; set; }
        public abstract void MakeHome();
    }

AbstractHome的具体实现者必须实现MakeHome()方法。除此之外,您可以通过使用AdditionalPrice属性来设置价格。这就是为什么从AbstractHome,继承的一个叫ConcreteHome的具体类完成了原来的结构,看起来像下面这样(我假设家一旦建好,不需要立即修改;所以,AdditionalPrice初始设置为 0)。

    class ConcreteHome : AbstractHome
    {
        public ConcreteHome()
        {
            AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            Console.WriteLine($"Original House is constructed.Price for this 10000$");
        }
    }

此时,你可以选择在现有的房屋上加建一层,或者你可以粉刷房屋,或者两者兼而有之。于是,FloorDecoratorPaintDecorator both出现了。虽然并不严格要求共享公共代码,但是两个装饰器都继承了AbstractDecorator,它具有以下结构。

abstract class AbstractDecorator : AbstractHome
    {
        protected AbstractHome home;
        public AbstractDecorator(AbstractHome home)
        {
            this.home = home;
            this.AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            home.MakeHome();
        }
    }

请注意,AbstractDecorator保存了对AbstractHome的引用。因此,具体的装饰者(本例中的FloorDecoratorPaintDecorator)正在装饰AbstractHome的一个实例。

现在我们来看一个混凝土装饰工的结构,FloorDecorator,如下。

    // Floor Decorator used to add a floor
    class FloorDecorator : AbstractDecorator
    {
        public FloorDecorator(AbstractHome home) : base(home)
        {

            this.AdditionalPrice = 2500;
        }
        public override void MakeHome()
        {
            base.MakeHome();
            // Adding a floor on top of original house.
            AddFloor();
        }
        private void AddFloor()
        {
            Console.WriteLine($"-Additional Floor added.Pay additional {AdditionalPrice}$ for it .");
        }
    }

你可以看到FloorDecorator可以加一层楼(使用AddFloor()的方法),使用的时候必须额外支付 2500 美元的额外建设费用。更重要的是,在添加楼层之前,它调用了AbstractHome类的MakeHome()方法,后者又从AbstractHome(即ConcreteHome)的一个具体实现中调用了MakeHome()方法。

类似的行为,但你要为此付出更多。(是的,我假设你正在为你的家使用奢华的油漆。)

类图

图 7-4 显示了类图中最重要的部分。

img/463942_2_En_7_Fig4_HTML.jpg

图 7-4

类图。这里没有显示客户端类。

解决方案资源管理器视图

图 7-5 显示了程序的高层结构。

img/463942_2_En_7_Fig5_HTML.jpg

图 7-5

解决方案资源管理器视图

示范

下面是完整的实现,它测试了两个场景(用#region 标记)。在场景 1 中,我在现有房屋上添加了一层,然后对其进行了粉刷。在场景 2 中,我粉刷了原来的家,然后在现有建筑的顶部添加了两层。

using System;

namespace DecoratorPatternDemo
{
    abstract class AbstractHome
    {
        public double AdditionalPrice { get; set; }
        public abstract void MakeHome();
    }
    class ConcreteHome : AbstractHome
    {
        public ConcreteHome()
        {
            AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            Console.WriteLine($"Original House is constructed.Price for this $10000");
        }
    }
    abstract class AbstractDecorator : AbstractHome
    {
        protected AbstractHome home;
        public AbstractDecorator(AbstractHome home)
        {
            this.home = home;
            this.AdditionalPrice = 0;
        }
        public override void MakeHome()
        {
            home.MakeHome();//Delegating task
        }
    }

    // Floor Decorator is used to add a floor

    class FloorDecorator : AbstractDecorator
    {

        public FloorDecorator(AbstractHome home) : base(home)
        {
            //this.home = home;
            this.AdditionalPrice = 2500;
        }
        public override void MakeHome()
        {
            base.MakeHome();
            // Adding a floor on top of original house.
            AddFloor();
        }
        private void AddFloor()
        {
            Console.WriteLine($"-Additional Floor added.Pay additional ${AdditionalPrice} for it .");
        }
    }

    // Paint Decorator used to paint the home.

    class PaintDecorator : AbstractDecorator
    {

        public PaintDecorator(AbstractHome home):base(home)
        {
            //this.home = home;
            this.AdditionalPrice = 5000;
        }
        public override void MakeHome()
        {
            base.MakeHome();
            // Painting home.
            PaintHome();
        }
        private void PaintHome()
        {
            Console.WriteLine($"--Painting done.Pay additional ${AdditionalPrice} for it .");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Decorator pattern Demo***\n");

            #region Scenario-1
            Console.WriteLine("\n**Scenario-1:");
            Console.WriteLine("**Building home.Adding floor and then painting it.**");

            AbstractHome home = new ConcreteHome();
            Console.WriteLine("Current bill breakups are as follows:");
            home.MakeHome();

            // Applying a decorator
            // Adding a floor
            home = new FloorDecorator(home);
            Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
            home.MakeHome();

            // Working on top of the previous decorator.
            // Painting the home
            home = new PaintDecorator(home);
            Console.WriteLine("\nPaint applied.Current bill breakups are as follows:");
            home.MakeHome();
            #endregion

            #region Scenario-2
            Console.WriteLine("\n**Scenario-2:");
            Console.WriteLine("**Building home,painting it and then adding two additional floors on top of it.**");
            // Fresh start once again.
            home = new ConcreteHome();
            Console.WriteLine("\nGoing back to original home.Current bill breakups are as follows:");
            home.MakeHome();

            // Applying paint on original home.
            home = new PaintDecorator(home);
            Console.WriteLine("\nPaint applied.Current bill breakups are as follows:");
            home.MakeHome();

            // Adding a floor on the painted home.
            home = new FloorDecorator(home);
            Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
            home.MakeHome();

            // Adding another floor on the current home.
            home = new FloorDecorator(home);
            Console.WriteLine("\nFloor added.Current bill breakups are as follows:");
            home.MakeHome();
            #endregion

            Console.ReadKey();
        }
    }
}

输出

***Decorator pattern Demo***

**Scenario-1:
**Building home. Adding floor and then painting it.**
Current bill breakups are as follows:
Original House is constructed. Price for this $10000

Floor added. Current bill breakups are as follows:
Original House is constructed.Price for this $10000
-Additional Floor added.Pay additional $2500 for it.

Paint applied. Current bill breakups are as follows:
Original House is constructed.Price for this $10000
-Additional Floor added. Pay additional $2500 for it.
--Painting done. Pay additional $5000 for it.

**Scenario-2:
**Building home, painting it and then adding two additional floors on top of it.**

Going back to original home. Current bill breakups are as follows:
Original House is constructed. Price for this $10000

Paint applied. Current bill breakups are as follows:
Original House is constructed. Price for this $10000
--Painting done. Pay additional $5000 for it.

Floor added.Current bill breakups are as follows:
Original House is constructed.Price for this $10000
--Painting done.Pay additional $5000 for it.
-Additional Floor added. Pay additional $2500 for it.

Floor added.Current bill breakups are as follows:
Original House is constructed. Price for this $10000
--Painting done.Pay additional $5000 for it.
-Additional Floor added.Pay additional $2500 for it.
-Additional Floor added.Pay additional $2500 for it.

问答环节

7.1 你能解释一下合成是如何促进一种继承所不能的动态行为的吗?

当派生类从基类继承时,它只继承基类当时的行为。尽管不同的子类可以以不同的方式扩展基类或父类,但这种类型的绑定在编译时是已知的。所以这个方法是静态的。但是通过使用组合的概念,就像前面的例子一样,您可以获得动态行为。

当你设计一个父类时,你可能没有足够的洞察力去了解你的客户在以后的某个阶段可能需要什么样的额外职责。由于约束是您不能修改现有的代码,在这种情况下,对象组合不仅远远超过继承,而且还确保您不会在旧的架构中引入错误。

最后,在这种情况下,你必须记住一个关键的设计原则,即类应该对扩展开放,但对修改关闭。

使用装潢师的关键 优势 是什么?

以下是一些关键优势。

  • 现有的结构是原封不动的,所以你不能在那里引入错误。

  • 新的功能可以很容易地添加到现有的对象。

  • 你不仅可以在界面上添加行为,还可以改变行为。

  • 您不需要立即预测/实现所有支持的功能(例如,在初始设计阶段)。你可以增量开发。例如,您可以逐个添加装饰器对象来支持您的需求。你必须承认,如果你先创建一个复杂的类,然后想扩展它的功能,这将是一个乏味的过程。

7.3 整体设计模式与 传承 有何不同?

您可以通过简单地附加或分离 decorators 来添加、改变或删除职责。但是使用简单的继承技术,您需要为新的职责创建新的类。所以,你最终可能会得到一个复杂的系统。

再次考虑这个例子。假设你想加一层新地板,粉刷房子,做一些额外的工作。为了满足这一需求,您可以从FloorDecorator开始,因为它已经提供了添加地板的支持,然后使用PaintDecorator来粉刷房子。然后,您需要添加一个简单的包装器来完成这些额外的职责。

但是如果你从继承开始,然后你可能有多个子类;比如一个加层,一个粉刷房子,如图 7-6 (一个层次继承)。

img/463942_2_En_7_Fig6_HTML.jpg

图 7-6

等级继承

因此,如果你需要一个带有额外功能的额外油漆地板,你可能需要最终得到如图 7-7 所示的设计。

img/463942_2_En_7_Fig7_HTML.jpg

图 7-7

一个类(额外特性)需要从多个基类继承

现在你感受到了“钻石效应”的热度,因为在许多编程语言中,包括 C#,多个基类是不允许的。

您还会发现,与装饰模式相比,继承机制不仅更具挑战性和耗时,而且可能会在应用中产生重复代码。最后,不要忘记继承只促进编译时绑定(不是动态绑定)。

7.4 你为什么要创建一个职责单一的类?你可以创建一个子类,简单地添加一个地板,然后进行绘画。在这种情况下,您可能会得到更少的子类。这是正确的吗?

如果你熟悉固体原理,你就知道有一个原理叫单责。这个原则背后的思想是,每个类应该对软件中提供的功能的一个部分负责。当您使用单一责任原则时,装饰模式是有效的,因为您可以简单地动态添加或删除责任。

7.5 与此模式相关的 缺点 有哪些?

我相信如果你小心的话,没有显著的缺点。但是如果你在系统中创建了太多的装饰器,那么维护和调试将会很困难。所以,在这种情况下,他们会制造不必要的混乱。

7.6 例子中的 AbstractDecorator 类 是抽象的,但是里面没有抽象方法。这怎么可能?

在 C# 中,一个类可以是抽象的,而不包含抽象方法,但反之则不然。换句话说,如果一个类包含至少一个抽象方法,就意味着这个类是不完整的,你被迫用abstract关键字来标记它。

同样,如果你阅读了图 7-8 中的注释,你正在将任务委托给一个具体的装饰者,在这种情况下,因为你只想使用和实例化具体的装饰者。

img/463942_2_En_7_Fig8_HTML.jpg

图 7-8

抽象类:AbstractDecorator

因此,在这个例子中,您不能简单地实例化一个AbstractDecorator实例,因为它用abstract关键字标记。

下面一行创建了一个编译错误。

AbstractDecorator abstractDecorator = new AbstractDecorator();
saying “CS0144    Cannot create an instance of the abstract class or interface 'AbstractDecorator'”

7.7 装饰器是否只用于 动态绑定

不。您可以将这个概念用于静态和动态绑定。但是动态绑定是它的强项,所以我在这里集中讨论这一点。GoF 定义也只关注动态绑定。

Note

中的 I/O 流实现。NET 框架,。NET 核心,Java 使用装饰模式。例如,BufferedStream类继承自Stream类。注意这个类中存在两个重载的构造函数;它们每个都以一个Stream(父类)作为参数(就像演示 1 一样)。当您看到这种结构时,您可能会看到装饰模式的一个例子。BufferedStream在. NET 中表现得像个装潢师。

八、适配器模式

本章介绍适配器模式。

GoF 定义

将一个类的接口转换成另一个客户期望的接口。适配器允许类一起工作,否则由于不兼容的接口而不能。

概念

从 GoF 定义中,您可以猜测这种模式处理至少两个不兼容的继承层次结构。在特定领域的系统中,客户习惯于如何调用软件中的方法。这些方法可以遵循继承层次结构。现在假设您需要升级系统,并且需要实现一个新的继承层次结构。当你这样做的时候,你不想强迫你的客户学习访问软件的新方法。那么,你能做什么?解决方案很简单:编写一个适配器,接受客户机请求,并以新层次结构中的方法可以理解的形式翻译这些请求。因此,客户可以享受更新的软件,没有任何麻烦。

下面的例子也可以帮助你更好地理解这些模式。

真实世界的例子

这种模式的一个常见应用是在国际旅行中使用电源插座适配器/交流电源适配器。这些适配器可以充当中间人,以便电子设备(如接受美国电源的笔记本电脑)可以插入欧洲电源插座。

考虑另一个例子。假设你需要给手机充电。但是你看到电源插座和你的充电器不兼容。在这种情况下,您可能需要使用适配器。即使是一个将一种语言转换成另一种语言的译者在现实生活中也遵循这种模式。

让我们考虑这样一种情况,您有两个不同的形状(例如,形状 1 和形状 2),它们都不是矩形,它们看起来像图 8-1 。

img/463942_2_En_8_Fig1_HTML.jpg

图 8-1

在使用适配器之前

让我们进一步假设,将这两种不同的形状结合起来,你需要形成一个矩形。你是如何进行的?一个简单的解决方法是再带一个有界的 X 形图形(填充颜色),如图 8-2 所示。

img/463942_2_En_8_Fig2_HTML.jpg

图 8-2

电源适配器

然后贴上三个形状,如图 8-3 所示。

img/463942_2_En_8_Fig3_HTML.jpg

图 8-3

使用适配器后

在编程中,你可以把 Shape1 和 Shape2 想象成两个不同的接口,除非你用这个 X 形的图形把它们组合起来形成一个矩形,否则它们是不能一起工作的。在这个场景中,X 形图形扮演着适配器的角色。

计算机世界的例子

假设您有一个应用,可以大致分为两部分:用户界面(UI 或前端)和数据库(后端)。通过用户界面,客户端可以传递一些特定类型的数据或对象。您的数据库与那些对象兼容,可以顺利地存储它们。随着时间的推移,你可能会意识到你需要升级你的软件来让你的客户满意。因此,您可能希望允许一些其他类型的对象也通过 UI。但是在这种情况下,第一个问题来自您的数据库,因为它不能存储这些新类型的对象。在这种情况下,可以使用适配器将这些新对象转换成现有数据库可以接受和存储的兼容形式。

履行

在接下来的例子中,有两个层次结构:一个用于Rectangle,一个用于TriangleIRectangle接口有两个方法叫做CalculateArea()AboutMe()Rectangle类实现了IRectangle接口,并形成了第一个层次,如下所示。

class Rectangle : IRectangle
    {
        double length;
        public double width;
        public Rectangle(double length, double width)
        {
            this.length = length;
            this.width = width;
        }

        public double CalculateArea()
        {
            return length * width;
        }

        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

ITriangle接口有两个方法:CalculateAreaOfTriangle()AboutTriangle()Triangle类实现了ITriangle接口并形成了另一个层次结构,如下所示。

class Triangle : ITriangle
    {
        double baseLength; // base
        double height; // height
        public Triangle(double length, double height)
        {
            this.baseLength = length;
            this.height = height;
        }
        public double CalculateAreaOfTriangle()
        {
            return 0.5 * baseLength * height;
        }
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle.");
        }
    }

这两个层次很容易理解。现在,让我们来看一个问题,在这个问题中,您需要使用矩形层次结构来计算三角形的面积。

你是如何进行的?您可以使用适配器来解决这个问题,如下例所示。

/*
 * RectangleAdapter is implementing IRectangle.
 * So, it needs to implement all the methods
 * defined in the target interface.
 */
class RectangleAdapter : IRectangle
{
        ITriangle triangle;
        public RectangleAdapter(ITriangle triangle)
        {
                this.triangle = triangle;
        }

        public void AboutMe()
        {
                triangle.AboutTriangle();
        }

        public double CalculateArea()
        {
                return triangle.CalculateAreaOfTriangle();
        }
}

注意使用适配器的好处。您没有对任何层次结构进行任何更改,从高层次上看,似乎通过使用IRectangle方法,您可以计算一个三角形的面积。这是因为您在高层使用了IRectangle接口的AboutMe()CalculateArea()方法,但是在这些方法内部,您调用了ITriangle方法。

除了这个优点,您还可以扩展使用适配器的好处。例如,假设您需要在一个应用中有大量的矩形,但是对您创建的矩形的数量有一个限制。(为了简单起见,让我们假设在一个应用中,您最多可以创建五个矩形和十个三角形,但是当应用运行时,在某些情况下,您可能需要提供十个矩形。)在这些情况下,使用这种模式,您可以使用一些行为类似矩形对象的三角形对象。怎么会?嗯,当使用适配器时,你调用的是CalculateArea(),,但它调用的是CalculateAreaOfTriangle() .,所以你可以根据需要修改方法体。例如,在您的应用中,假设每个矩形对象的长度为 20 个单位,宽度为 10 个单位,而每个三角形对象的底边为 20 个单位,高度为 10 个单位。因此,每个矩形对象的面积为 2010=200 平方单位,每个三角形对象的面积为 0.520*10=100 平方单位。因此,您可以简单地将每个三角形面积乘以 2,以获得一个等效的矩形面积,并在需要矩形面积的地方替换(或使用)它。我希望这对你有意义。

最后,您需要记住,当您处理不完全相同但非常相似的对象时,这种技术最适合。

Note

在前一点的上下文中,您不应该尝试将圆形区域转换为矩形区域(或进行类似类型的转换),因为它们是不同的形状。在这个例子中,我谈论三角形和矩形是因为它们有相似之处。

类图

图 8-4 显示了程序重要部分的类图。

img/463942_2_En_8_Fig4_HTML.jpg

图 8-4

类图。这里没有显示客户端类。

解决方案资源管理器视图

图 8-5 显示了程序的高层结构。

img/463942_2_En_8_Fig5_HTML.jpg

图 8-5

解决方案资源管理器视图

演示 1

下面是实现。

using System;

namespace AdapterPatternDemonstration
{
    interface IRectangle
    {
        void AboutMe();
        double CalculateArea();
    }
    class Rectangle : IRectangle
    {
        double length;
        public double width;
        public Rectangle(double length, double width)
        {
            this.length = length;
            this.width = width;
        }

        public double CalculateArea()
        {
            return length * width;
        }

        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

    interface ITriangle
    {
        void AboutTriangle();
        double CalculateAreaOfTriangle();
    }
    class Triangle : ITriangle
    {
        double baseLength; // base
        double height; // height
        public Triangle(double length, double height)
        {
            this.baseLength = length;
            this.height = height;
        }
        public double CalculateAreaOfTriangle()
        {
            return 0.5 * baseLength * height;
        }
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle.");
        }
    }

    /*
     * RectangleAdapter is implementing IRectangle.
     * So, it needs to implement all the methods
     * defined in the target interface.
     */
    class RectangleAdapter : IRectangle
    {
        ITriangle triangle;
        public RectangleAdapter(ITriangle triangle)
        {
            this.triangle = triangle;
        }

        public void AboutMe()
        {
            triangle.AboutTriangle();
        }

        public double CalculateArea()
        {
            return triangle.CalculateAreaOfTriangle();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Adapter Pattern  Demo***\n");
            IRectangle rectangle = new Rectangle(20, 10);
            Console.WriteLine("For initial verification purposes, printing the areas of both shapes.");
            Console.WriteLine("Rectangle area is:{0} Square unit", rectangle.CalculateArea());
            ITriangle triangle = new Triangle(20, 10);
            Console.WriteLine("Triangle area is:{0} Square unit", triangle.CalculateAreaOfTriangle());

            Console.WriteLine("\nNow using the adapter.");
            IRectangle adapter = new RectangleAdapter(triangle);
            Console.Write("True fact : ");
            adapter.AboutMe();
            Console.WriteLine($" and my area is : {adapter.CalculateArea()} square unit.");

            // Alternative way:
            Console.WriteLine("\nUsing the adapter in a different way now.");
            // Passing a Triangle instead of a Rectangle
            Console.WriteLine($"Area of the triangle using the adapter is :{GetDetails(adapter)} square unit.");
            Console.ReadKey();
        }
        /*
         * The following method does not know
         * that through the adapter, it can
         * actually process a
         * Triangle instead of a Rectangle.
         */
        static double GetDetails(IRectangle rectangle)
        {
            rectangle.AboutMe();
            return rectangle.CalculateArea();
        }
    }
}

输出

这是输出。

***Adapter Pattern  Demo***

For initial verification purposes, printing the areas of both shapes.
Rectangle area is:200 Square unit
Triangle area is:100 Square unit

Now using the adapter.
True fact : Actually, I am a Triangle.
 and my area is : 100 square unit.

Using the adapter in a different way now.
Actually, I am a Triangle.
Area of the triangle using the adapter is :100 square unit.

分析

注意下面的代码段,在Main()方法中有注释,如下所示。

/*
 * The following method does not know
 * that through the adapter, it can
 * actually process a
 * Triangle instead of a Rectangle.
 */
static double GetDetails(IRectangle rectangle)
{
        rectangle.AboutMe();
        return rectangle.CalculateArea();
}

此部分是可选的。我保留它是为了向您展示在哪里可以在一次调用中调用这两个 adaptee 方法。

适配器的类型

GoF 描述了两种适配器:类适配器和对象适配器。

对象适配器

对象适配器通过对象组合进行适配,如图 8-6 所示。因此,到目前为止讨论的适配器是对象适配器的一个例子。

img/463942_2_En_8_Fig6_HTML.jpg

图 8-6

对象适配器

在我们的例子中,RectangleAdapter是实现IRectangle (Target interface). ITriangle的适配器,它是Adaptee接口。适配器保存被适配器实例。

类别适配器

类适配器通过子类化来适应,并支持多重继承。但是你知道在 C# 中,不支持通过类的多重继承。(你需要接口来实现多重继承的概念。)

图 8-7 显示了支持多重继承的类适配器的典型类图。

img/463942_2_En_8_Fig7_HTML.jpg

图 8-7

类别适配器

问答环节

8.1 如何在 C# 中实现一个 类适配器设计模式

您可以子类化现有的类并实现所需的接口。演示 2 向您展示了一个完整的输出示例。

演示 2

这个演示展示了一个类适配器。为了使这个例子简单明了,我只用一种方法制作了IRectangleITriangle接口。IRectangle只有AboutMe()方法, and the Rectangle类实现了IRectangle接口,这样就形成了下面的层次结构。

interface IRectangle
    {
        void AboutMe();
    }
    class Rectangle : IRectangle
    {
        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

ITriangle有了AboutTriangle()方法.``Triangle类实现了这个接口,下面的层次结构就形成了。

interface ITriangle
    {
        void AboutTriangle();
    }
    class Triangle : ITriangle
    {
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle");
        }
    }

现在是我们的类适配器,它使用了多重继承的概念,使用了一个具体的类和一个接口。附加的注释有助于您更好地理解代码。

    /*
     * RectangleAdapter is implementing IRectangle.
     * So, it needs to implement all the methods
     * defined in the target interface.
     */
    class RectangleAdapter : Triangle, IRectangle
    {
        public void AboutMe()
        {
            // Invoking the adaptee method
            AboutTriangle();
        }
    }

现在您可以进行完整的演示,如下所示。

using System;

namespace AdapterPatternAlternativeImplementationDemo
{
    interface IRectangle
    {
        void AboutMe();
    }
    class Rectangle : IRectangle
    {
        public void AboutMe()
        {
            Console.WriteLine("Actually, I am a Rectangle");
        }
    }

    interface ITriangle
    {
        void AboutTriangle();
    }
    class Triangle : ITriangle
    {
        public void AboutTriangle()
        {
            Console.WriteLine("Actually, I am a Triangle");
        }
    }

    /*
     * RectangleAdapter is implementing IRectangle.
     * So, it needs to implement all the methods
     * defined in the target interface.
     */
    class RectangleAdapter : Triangle, IRectangle
    {
        public void AboutMe()
        {
            // Invoking the adaptee method
            AboutTriangle();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Adapter Pattern Alternative Implementation Technique Demo.***\n");
            IRectangle rectangle = new Rectangle();
            Console.WriteLine("For initial verification purposes, printing the details from of both shapes.");
            Console.WriteLine("The rectangle.AboutMe() says:");
            rectangle.AboutMe();
            ITriangle triangle = new Triangle();
            Console.WriteLine("The triangle.AboutTriangle() says:");
            triangle.AboutTriangle();

            Console.WriteLine("\nNow using the adapter.");
            IRectangle adapter = new RectangleAdapter();
            Console.Write("True fact : ");
            adapter.AboutMe();
        }
    }
}

输出

这是输出。

***Adapter Pattern Alternative Implementation Technique Demo.***

For initial verification purposes, printing the details from of both shapes.
The rectangle.AboutTriangle() says:
Actually, I am a Rectangle.
The triangle.AboutTriangle() says:
Actually, I am a Triangle.

Now using the adapter.
True fact : Actually, I am a Triangle.

分析

这种方法可能不适用于所有情况。例如,您可能需要采用 C# 接口中没有指定的方法。在这些情况下,对象适配器更好。

问答环节

8.2 你更喜欢哪个——****类适配器 还是 对象适配器

在大多数情况下,我更喜欢组合而不是继承。对象适配器使用组合,更加灵活。在许多情况下,当您需要从 adaptee 接口修改特定方法时,实现真正的类适配器是一项挑战,但是在目标接口中没有与之非常匹配的方法。除此之外,如果 adaptee 类(在我们的例子中是 Triangle)是密封的,那么您不能从它继承。

8.3 你说过,“……当你需要从一个被适配者接口适配一个特定的方法时,实现一个真正的类适配器是具有挑战性的,但是在目标接口中没有与之非常匹配的方法。”你能详细说明一下吗?

在我的例子中,目标接口方法和适配器接口方法是相似的。比如在IRectangle里有AboutMe()法,在ITriangle里有AboutTriangle()法。他们是做什么的?他们指出它是矩形还是三角形。

现在假设在IRectangle,中没有这个叫做AboutMe()的方法,但是在ITriangle中仍然存在AboutTriangle()。所以,在这种情况下,如果你需要采用AboutTriangle()方法,你需要分析如何进行。在我们的例子中,AboutTriangle()是一个简单的方法,但是在现实世界的编程中,这个方法要复杂得多,并且可能存在与之相关的依赖关系。因此,当您没有相应的目标方法时,您可能会发现从一个被适应者那里适应该方法是一个挑战。

我明白客户不应该知道他们正在使用适配器。这是正确的吗?

没错。我做这个实现是为了向您展示,客户机不需要知道它们的请求通过适配器被转换到被适配器。如果您希望它们显示任何消息,您只需在演示 2 中的适配器中添加一个控制台消息,如下所示。

class RectangleAdapter : Triangle, IRectangle
{
        public void AboutMe()
        {
                // Invoking the adaptee method
                // For Q&A
                Console.WriteLine("You are using an adapter now.");
                AboutTriangle();
        }
}

8.5 如果 目标接口 adaptee 接口方法 签名不同会怎样?

一点问题都没有。如果一个适配器方法有几个参数,您可以用一些额外的伪参数调用 adaptee 方法。在构建器模式中(第三章中的演示 2),您看到了可选参数。您可以在这里使用相同的概念。

在相反的情况下(如果适配器方法比 adaptee 方法有更多的参数),通过使用这些额外的参数,您可以在将调用转移到 adaptee 方法之前添加功能。

最后,如果方法参数不兼容,您可能需要进行转换(如果可能的话)。

8.6 与此模式相关的 弊端 有哪些?

我看不出有什么重大挑战。我认为适配器的工作简单明了,但是您需要编写一些额外的代码。然而,回报是巨大的,特别是对于那些不能改变的遗留系统,但是您仍然希望使用它们的稳定性和简单性。

九、外观模式

本章涵盖了外观模式。

GoF 定义

为子系统中的一组接口提供统一的接口。Facade 定义了一个更高级的接口,使得子系统更容易使用。

概念

这种模式支持松耦合。使用这种模式,您可以通过公开一个简单的接口来强调抽象并隐藏复杂的细节。

考虑一个简单的例子。假设在一个应用中,有多个类,每个类由多个方法组成。客户可以使用这些类中的方法组合来制作产品,但是他需要记住选择哪些类,哪些方法用于这些构造的调用序列。这没什么,但是如果这些产品之间有很多差异,客户的日子就不好过了。

为了克服这一点,Facade 模式很有用。它为客户提供了一个用户友好的界面,因为所有内在的复杂性都被隐藏起来了。因此,客户可以简单地专注于他需要做的事情。

真实世界的例子

假设你要举办一个有 300 名客人的生日聚会。现在,你可以雇佣一个聚会组织者,让他们知道聚会的类型、日期和时间、参加人数等关键信息。组织者会为您完成剩下的工作。你不需要考虑他们如何装饰聚会房间,他们如何管理食物,等等。

考虑另一个例子。假设客户向银行申请贷款。在这种情况下,客户只对贷款能不能批下来感兴趣;他不关心在后端进行的内部背景验证过程。

计算机世界的例子

想想当你使用一个库中的方法时(在编程语言的上下文中)。该方法在库中是如何实现的并不重要,您只需调用该方法以便于使用。下面的例子更清楚地说明了这一点。

履行

在这个例子中,一个客户可以请求得到不同种类的机器人和他喜欢的颜色。为了达到这个目的,只有两个类。第一个是RobotBody,制作机器人的身体。第二类是RobotColor,给机器人上色。

RobotBody有一个参数化的构造函数,有两个方法叫做MakeRobotBodyDestroyRobotBody。这些方法负责制造一个机器人和摧毁一个机器人。我用一个计数器来记录机器人的数量。如果系统中没有机器人,销毁请求将被忽略。如果您愿意,可以忽略计数器,将注意力完全集中在描述该模式重要方面的部分。现在我们来看看RobotBody级。

  class RobotBody
    {
        string robotType;
        /*
        * To keep a count of number of robots.
        * This operation is optional for you.
       */
        static int count = 0;
        public RobotBody(string robotType)
        {
            this.robotType = robotType;
        }
        public void MakeRobotBody()
        {
          Console.WriteLine($"Constructing one {robotType} robot.");
          Console.WriteLine("Robot creation finished.");
          Console.WriteLine($"Total number of robot created at this moment={++count}");
        }
        public void DestroyRobotBody()
        {
            if (count > 0)
            {
                --count;
                Console.WriteLine("Robot's destruction process is over.");
            }
            else
            {
                Console.WriteLine("All robots are destroyed.");
                Console.WriteLine("Color removal operation will not continue.");
            }
        }
    }

RobotColor很容易理解。它有一个参数化的构造函数和两个方法——SetColor()RemoveColor()——来给机器人上色或从机器人身上移除颜料。下面的代码段是针对RobotColor的。

    public class RobotColor
    {
        string color;
        public RobotColor(string color)
        {
            this.color = color;
        }
        public void SetColor()
        {
            if (color == "steel")
            {
                Console.WriteLine($"The default color {color} is set for the robot.");
            }
            else
            {
                Console.WriteLine($"Painting the robot with your favourite {color} color.");
            }
        }
        public void RemoveColor()
        {
            Console.WriteLine("Attempting to remove the colors from the robot.");
        }
    }

现在是最重要的部分。您可以看到,客户端可以通过向对象RobotBody提供所需的字符串参数来创建机器人,调用MakeRobotBody() ,,然后使用RobotColor类的SetColor()来绘制机器人。因此,可以使用下面几行。

// Without Facade pattern
RobotBody robotBody = new RobotBody("Milano");
robotBody.MakeRobotBody();
RobotColor robotColor = new RobotColor("green");
robotColor.SetColor();

但是,如果一个客户端有一个名为RobotFacade的类,并且像下面这样调用,会发生什么呢?

RobotFacade facade = new RobotFacade("Milano","green");
facade.ConstructRobot();

或者,你允许他像下面这样打电话(通过提供默认颜色)?

// Making a robonaut robot with default steel color.
facade = new RobotFacade("Robonaut");
facade.ConstructRobot();

你知道答案:客户会很高兴;在这些情况下,他不需要记住创建机器人的步骤。为了简单起见,示例中只使用了两个类,但是在现实世界中,您可能需要使用大量的类和方法来制作这样的产品。在这种情况下,Facade 模式更加强大。你可以告诉你的客户使用RobotFacade类来创建和销毁机器人,而不是像RobotBodyRobotColor那样调用每个类。

现在我们来看看RobotFacade。当我使用这个类的ConstructRobot()DestroyRobot()方法时,我将RobotBodyRobotColor组合到其中,并将任务委托给相应的组件。从现在开始,RobotBodyRobotColor在这个例子中可以称为子系统类

这里是门面类。

class RobotFacade
    {
        RobotBody robotBody;
        RobotColor robotColor;
        public RobotFacade(string robotType, string color = "steel")
        {
            robotBody = new RobotBody(robotType);
            robotColor = new RobotColor(color);
        }
        public void ConstructRobot()
        {
            Console.WriteLine("Robot creation through facade starts...");
            robotBody.MakeRobotBody();
            robotColor.SetColor();
            Console.WriteLine();
        }

        public void DestroyRobot()
        {
            Console.WriteLine("Making an attempt to destroy one robot using  the facade now.");
            robotColor.RemoveColor();
            robotBody.DestroyRobotBody();
            Console.WriteLine();
        }
    }

类图

图 9-1 显示了类图。

img/463942_2_En_9_Fig1_HTML.jpg

图 9-1

类图

解决方案资源管理器视图

图 9-2 显示了程序的高层结构。从 Solution Explorer 中,您可以看到,在较高的层次上,我将子系统类与外观类和客户端代码分离开来。子系统类放在 RobotParts 文件夹中。

img/463942_2_En_9_Fig2_HTML.jpg

图 9-2

解决方案资源管理器视图

示范

下面是完整的实现。

// RobotBody.cs

using System;

namespace FacadePattern.RobotParts
{
    class RobotBody
    {
        string robotType;
        /*
        * To keep a count of number of robots.
        * This operation is optional for you.
       */
        static int count = 0;
        public RobotBody(string robotType)
        {
            this.robotType = robotType;
        }
        public void MakeRobotBody()
        {
          Console.WriteLine($"Constructing one {robotType} robot.");
          Console.WriteLine("Robot creation finished.");
          Console.WriteLine($"Total number of robot created at this moment={++count}");
        }
        public void DestroyRobotBody()
        {
            if (count > 0)
            {
                --count;
                Console.WriteLine("Robot's destruction process is over.");
            }
            else
            {
                Console.WriteLine("All robots are destroyed.");
                Console.WriteLine("Color removal operation will not continue.");
            }
        }
    }
}

// RobotColor.cs

using System;

namespace FacadePattern.RobotParts
{
    public class RobotColor
    {
        string color;
        public RobotColor(string color)
        {
            this.color = color;
        }
        public void SetColor()
        {
            if (color == "steel")
            {
                Console.WriteLine($"The default color {color} is set for the robot.");
            }
            else
            {
                Console.WriteLine($"Painting the robot with your favourite {color} color.");
            }
        }
        public void RemoveColor()
        {
            Console.WriteLine("Attempting to remove the colors from the robot.");
        }
    }
}

 // RobotFacade.cs

using System;

namespace FacadePattern.RobotParts
{
    class RobotFacade
    {
        RobotBody robotBody;
        RobotColor robotColor;
        public RobotFacade(string robotType, string color = "steel")
        {
            robotBody = new RobotBody(robotType);
            robotColor = new RobotColor(color);
        }
        public void ConstructRobot()
        {
            Console.WriteLine("Robot creation through facade starts...");
            robotBody.MakeRobotBody();
            robotColor.SetColor();
            Console.WriteLine();
        }

        public void DestroyRobot()
        {
            Console.WriteLine("Making an attempt to destroy one robot using the facade now.");
            robotColor.RemoveColor();
            robotBody.DestroyRobotBody();
            Console.WriteLine();
        }
    }
}

 // Program.cs

using System;
using FacadePattern.RobotParts;

namespace FacadePattern
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Facade Pattern Demo.***\n");
            // Making a Milano robot with green color.
            RobotFacade facade = new RobotFacade("Milano","green");
            facade.ConstructRobot();
            // Making a robonaut robot with default steel color.
            facade = new RobotFacade("Robonaut");
            facade.ConstructRobot();
            // Destroying one robot
            facade.DestroyRobot();
            // Destroying another robot
            facade.DestroyRobot();
            // This destrcution attempt should fail.
            facade.DestroyRobot();
            Console.ReadLine();

        }
    }
}

输出

这是输出。

***Facade Pattern Demo.***

Robot creation through facade starts...
Constructing one Milano robot.
Robot creation finished.
Total number of robot created at this moment=1
Painting the robot with your favourite green color.

Robot creation through facade starts...
Constructing one Robonaut robot.
Robot creation finished.
Total number of robot created at this moment=2
The default color steel is set for the robot.

Making an attempt to destroy one robot using the facade now.
Attempting to remove the colors from the robot.
Robot's destruction process is over.

Making an attempt to destroy one robot using the facade now.
Attempting to remove the colors from the robot.
Robot's destruction process is over.

Making an attempt to destroy one robot using the facade now.
Attempting to remove the colors from the robot.
All robots are destroyed.
Color removal operation will not continue.

问答环节

9.1 使用 Facade 模式的关键 优势 有哪些?

以下是一些优点。

  • 如果您的系统由许多子系统组成,那么管理这些子系统就变得很困难,并且客户端发现很难与这些子系统中的每一个单独通信。在这种情况下,外观模式非常方便。您向客户呈现了一个简化的界面,而不是复杂的子系统。这种方法还通过将客户端代码与子系统分离来支持弱耦合。

  • 它还可以帮助减少客户端需要处理的对象数量。

在这个例子中,facade 类使用了 组合 。有这个必要吗?

是的。使用这种方法,您可以访问每个子系统中的预期方法。当我使用这个类的ConstructRobot()DestroyRobot()方法时,我将任务委托给了相应的组件。

9.3 您现在可以直接访问每个子系统了吗?

是的,你可以。Facade 模式并不限制您这样做。在介绍 facade 类之前,我已经向您展示了这一点。但是在这种情况下,代码可能看起来很脏,并且您可能会失去与 Facade 模式相关的好处。在这个上下文中,您可以注意到,由于客户端可以直接访问子系统,所以它被称为透明外观。但是,当您限制这种使用并强制他们只能通过 RobotFacade 创建机器人时,您可以将 Facade 称为不透明 facade。

9.4 Facade 与适配器设计模式 有何不同?

在适配器模式中,您试图改变一个接口,以便您的客户机看不到接口之间的任何差异。相比之下,Facade 模式简化了界面。它为客户端提供了一个简单的交互界面(而不是复杂的子系统)。

9.5 一个 复杂子系统 应该只有一个门面。这是正确的吗?

一点也不。您可以为特定子系统创建任意数量的外观。

9.6 可以用 facade 添加新的东西或者附加代码吗?

是的,你可以。您可以看到,在将调用委托给实际组件之前,我在RobotFacade类的ConstructRobot()中使用了下面一行代码。

Console.WriteLine("Robot creation through facade starts...");

同样的,DestroyRobot()在试图摧毁一个机器人之前,有如下一行。

Console.WriteLine("Making an attempt to destroy one robot using  the facade now.");

9.7 与门面格局相关的 挑战 有哪些?

这里有一些挑战。

  • 子系统连接到外观层。因此,您需要关注额外的编码层(增加您的代码库)。

  • 当子系统的内部结构发生变化时,您也需要将变化合并到外观层中。

  • 一些开发人员可能需要了解这个新层,但是他们中的一些人知道如何有效地使用子系统/API。

9.8 我能让 facade 类成为静态的吗?

在许多例子中,只有一个 facade,您可能不需要初始化 facade 类。在这些情况下,如果让 facade 类成为静态的,就很有意义。

十、享元模式

这一章涵盖了 Flyweight 模式。

GoF 定义

使用共享来有效地支持大量细粒度的对象。

概念

这种模式可能看起来很简单,但是如果您没有确定核心概念,实现可能会显得很复杂。在实现这个模式之前,让我们从一个基本但详细的解释开始。

有时你需要处理许多非常相似但又不完全相同的对象。限制是您不能创建所有的文件来减少资源和内存的使用。Flyweight 模式就是用来处理这些场景的。

现在的问题是如何去做?为了理解这一点,让我们快速回顾一下面向对象编程的基础。一个类是一个模板或蓝图,一个对象是它的一个实例。一个对象可以有状态和行为。例如,如果你熟悉足球(或在美国被称为足球,你可以说 Ronaldo 或 Beckham 是Footballer类的对象。您可能会注意到,它们有“播放状态”或“非播放状态”这样的状态在玩耍状态下,他们可以展示不同的技能(或行为)——他们可以跑,可以踢,可以传球,等等。从面向对象编程开始,您可以提出以下问题。

  • 我的对象可能有哪些状态?

  • 在这些状态下,它们可以执行哪些不同的功能(行为)?

一旦你得到了这些问题的答案,你就可以继续了。现在回到享元模式。在这里你的工作是识别。

  • 我的对象的状态是什么?

  • 这些状态的哪一部分是可以改变的?

一旦你确定了答案,你就把状态分成两部分,称为内在的(不变的)和外在的(会变的)。现在你明白了,如果你制造的对象具有所有对象都可以共享的内在状态。对于外在部分,用户或客户需要传递信息。所以,无论何时你需要一个对象,你都可以得到具有内在状态的对象,然后你可以通过传递外在状态来动态地配置对象。遵循这种技术,您可以减少不必要的对象创建和内存使用。

现在让我们在下面的段落中验证你的知识,这是极其重要的。让我们看看 GoF 对享元是怎么说的。

flyweight 是一个可以同时在多个上下文中使用的共享对象。在每个上下文中,flyweight 充当一个独立的对象——它与未共享的对象实例没有区别。Flyweights 不能对它们运行的环境做出假设。这里的关键概念是内在和外在状态之间的区别。内在状态存储在 flyweight 中;它由独立于 flyweight 上下文的信息组成,因此可以共享。外在状态依赖于 flyweight 的上下文并随其变化,因此不能共享。客户端对象负责在需要时将外部状态传递给 flyweight。

真实世界的例子

假设你有一支笔。你可以用不同的墨水笔芯写不同的颜色。因此,在这个例子中,没有笔芯的笔可以被认为是具有内在数据的享元,而笔芯可以被认为是外在数据。

计算机世界的例子

假设在一个电脑游戏中,你有大量的参与者,他们的核心结构是相同的,但他们的外观各不相同(例如,他们可能有不同的状态、颜色、武器等等)。因此,如果你想存储所有具有所有变化/状态的对象,内存需求将是巨大的。因此,不需要存储所有的对象,您可以用这样一种方式设计应用,即您创建这些实例中的一个,这些实例的状态在对象之间没有变化,并且您的客户端可以维护剩余的变化/状态。如果您能够在设计阶段成功地实现这个概念,那么您已经在应用中遵循了 Flyweight 模式。

考虑另一个例子。假设一家公司需要为员工打印名片。在这种情况下,出发点是什么?企业可以创建一个通用模板,在该模板上已经打印了公司徽标、地址等(内部),然后公司将特定员工的信息(外部)放在卡片上。

这种模式的另一个常见用途是在文字处理程序中用图形表示字符,或者在应用中处理字符串时使用。

履行

下面的例子展示了三种不同类型车辆的用法:CarBus,FutureVehicle(我假设 2050 年使用)。在这个应用中,我假设客户可能想要使用这些类中的大量对象,这些对象具有他们喜欢的不同颜色。我还假设汽车(或公共汽车等)的基本结构。)不变。

当客户端请求特定的车辆时,如果应用先前创建了该类型车辆的实例,则它不会从头开始创建对象;相反,它会准备好现有的(没有color)来满足他的需求。就在交付产品之前,它会给车辆涂上客户喜欢的color。现在让我们看看实现策略。

首先,为 flyweights 创建一个接口。这个接口提供了接受 flyweights 的外部状态的通用方法。在我们的例子中,color是由客户提供的;因此,这被视为一种外在状态,这就是为什么你会看到下面的代码段。

/// <summary>
/// The 'Flyweight' interface
/// </summary>
interface IVehicle
    {
        /*
         * Client will supply the color.
         * It is extrinsic state.
         */
        void AboutMe(string color);
    }

最常见的情况是,你看到一个工厂为客户提供飞锤。这个工厂缓存 flyweights 并提供获取它们的方法。在一个共享的 flyweight 对象中,如果需要的话,可以添加内部状态并实现方法。你也可以有不共享的飞锤。在这些情况下,您可以忽略客户端传递的外部状态。

在接下来的例子中,VehicleFactory是为 flyweights 提供内在状态的工厂。一个Dictionary对象存储key/value对以存储特定类型的车辆。最初,工厂内部没有对象,但是一旦它开始接收车辆请求,它就创建车辆并缓存这些车辆以备将来使用。请注意,“创建一辆汽车”、“创建一辆公共汽车”和“创建 2050 辆汽车”是在对象创建阶段由工厂在 flyweight 对象中提供的。这些是这些车辆的固有状态,不会因产品而异。下面的代码段显示了这个工厂类。

/// <summary>
/// The factory class for flyweights.
/// </summary>
class VehicleFactory
{
    private Dictionary<string, IVehicle> vehicles = new Dictionary<string, IVehicle>();
    public int TotalObjectsCreated
    {
        get { return vehicles.Count; }
    }

    public IVehicle GetVehicleFromVehicleFactory(string vehicleType)
    {
        IVehicle vehicleCategory = null;
        if (vehicles.ContainsKey(vehicleType))
        {
               vehicleCategory = vehicles[vehicleType];

            }
            else
            {
                switch (vehicleType)
                {
                    case "car":
                        vehicleCategory = new Car("One car is created");
                        vehicles.Add("car", vehicleCategory);
                        break;
                    case "bus":
                        vehicleCategory = new Bus("One bus is created");
                        vehicles.Add("bus", vehicleCategory);
                        break;
                    case "future":
                        vehicleCategory = new FutureVehicle("Vehicle 2050 is created");
                        vehicles.Add("future", vehicleCategory);
                        break;
                    default:
                        throw new Exception("Vehicle Factory can give you cars and buses only.");
                }

            }
            return vehicleCategory;
        }
    }

现在让我们来看一个具体的享元类。下面是其中的一个类(其他的都差不多)。相关注释帮助您理解AboutMe()方法如何包含车辆的内在状态和外在状态。

    /// <summary>
    /// A 'ConcreteFlyweight' class called Car
    /// </summary>
    class Car : IVehicle
    {
        /*
         * It is intrinsic state and
         * it is independent of flyweight context.
         * this can be shared.So, our factory method will supply
         * this value inside the flyweight object.
         */
        private string description;
        /*
         * Flyweight factory will supply this
         * inside the flyweight object.
         */
        public Car(string description)
        {
            this.description = description;
        }
        // Client will supply the color
        public void AboutMe(string color)
        {
            Console.WriteLine($"{description} with {color} color.");
        }
    }

从这段代码中,您可以看到,description是在对象创建过程中提供的(Flyweight 工厂会这样做),但是color是由客户端提供的。在这个例子中,我使用一种叫做GetRandomColor()的方法随机绘制颜色。因此,在Main()中,您会看到下面的代码:

vehicle.AboutMe(GetRandomColor());

只读属性TotalObjectsCreated计算任意给定时刻不同类型的车辆;在工厂类中理解下面的代码是非常容易的。

public int TotalObjectsCreated
{
        get
        {
            return vehicles.Count;
        }
}

最后,在这个例子中,FutureVehicle被认为是一个非共享的 flyweight。所以,在这个类中,AboutMe(...)方法忽略了string参数。因此,它总是生产蓝色的车辆,并忽略客户的喜好。

// Client cannot choose color for FutureVehicle
//since it's unshared flyweight,ignoring client's input
     public void AboutMe(string color)
     {
         Console.WriteLine($"{description} with blue color.");
     }

类图

图 10-1 为类图。

img/463942_2_En_10_Fig1_HTML.jpg

图 10-1

类图

解决方案资源管理器视图

图 10-2 显示了程序各部分的高层结构。

img/463942_2_En_10_Fig2_HTML.jpg

图 10-2

解决方案资源管理器视图

演示 1

下面是完整的实现。参考评论帮助你更好的理解。

using System;
using System.Collections.Generic;//Dictionary is used here

namespace FlyweightPattern
{
    /// <summary>
    /// The 'Flyweight' interface
    /// </summary>
    interface IVehicle
    {
        /*
         * Client will supply the color.
         * It is extrinsic state.
         */
        void AboutMe(string color);
    }
    /// <summary>
    /// A 'ConcreteFlyweight' class called Car
    /// </summary>
    class Car : IVehicle
    {
        /*
         * It is intrinsic state and
         * it is independent of flyweight context.
         * this can be shared.So, our factory method will supply
         * this value inside the flyweight object.
         */
        private string description;
        /*
         * Flyweight factory will supply this
         * inside the flyweight object.
         */
        public Car(string description)
        {
            this.description = description;
        }
        // Client will supply the color
        public void AboutMe(string color)
        {
            Console.WriteLine($"{description} with {color} color.");
        }
    }
    /// <summary>
    /// A 'ConcreteFlyweight' class called Bus
    /// </summary>
    class Bus : IVehicle
    {
        /*
         * It is intrinsic state and
         * it is independent of flyweight context.
         * this can be shared.So, our factory method will supply
         * this value inside the flyweight object.
         */
        private string description;
        public Bus(string description)
        {
            this.description = description;
        }
        // Client will supply the color
        public void AboutMe(string color)
        {
            Console.WriteLine($"{description} with {color} color.");
        }
    }
    /// <summary>
    /// A 'ConcreteFlyweight' class called FutureVehicle
    /// </summary>
    class FutureVehicle : IVehicle
    {
        /*
         * It is intrinsic state and
         * it is independent of flyweight context.
         * this can be shared.So, our factory method will supply
         * this value inside the flyweight object.
         */
        private string description;
        public FutureVehicle(string description)
        {
            this.description = description;
        }
        // Client cannot choose color for FutureVehicle
        // since it's unshared flyweight,ignoring client's input
        public void AboutMe(string color)
        {
            Console.WriteLine($"{description} with blue color.");
        }
    }

    /// <summary>
    /// The factory class for flyweights.
    /// </summary>
    class VehicleFactory
    {
        private Dictionary<string, IVehicle> vehicles = new Dictionary<string, IVehicle>();
         /*
          * To count different types of vehicles
          * in a given moment.
          */
        public int TotalObjectsCreated
        {
            get
            {
                return vehicles.Count;
            }
        }
        public IVehicle GetVehicleFromVehicleFactory(string vehicleType)
        {
            IVehicle vehicleCategory = null;
            if (vehicles.ContainsKey(vehicleType))
            {
                vehicleCategory = vehicles[vehicleType];
            }
            else
            {
                switch (vehicleType)
                {
                    case "car":
                        vehicleCategory = new Car("One car is created");
                        vehicles.Add("car", vehicleCategory);
                        break;
                    case "bus":
                        vehicleCategory = new Bus("One bus is created");
                        vehicles.Add("bus", vehicleCategory);
                        break;
                    case "future":
                        vehicleCategory = new FutureVehicle("Vehicle 2050 is created");
                        vehicles.Add("future", vehicleCategory);
                        break;
                    default:
                        throw new Exception("Vehicle Factory can give you cars and buses only.");
                }
            }
            return vehicleCategory;
        }
    }

    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Flyweight Pattern Demo.***\n");
            VehicleFactory vehiclefactory = new VehicleFactory();
            IVehicle vehicle;
            /*
            * Now we are trying to get the 3 cars. Note that:we need not create additional cars if we have already created one of this category.
            */
            for (int i = 0; i < 3; i++)
            {
                vehicle = vehiclefactory.GetVehicleFromVehicleFactory("car");
                vehicle.AboutMe(GetRandomColor());
            }
            int numOfDistinctRobots = vehiclefactory.TotalObjectsCreated;
            Console.WriteLine($"\n Now, total numbers of distinct vehicle object(s) is = {numOfDistinctRobots}\n");
            /*
            Here we are trying to get the 5 more buses.Note that: we need not create additional buses if we have already created one of this category.
            */

            for (int i = 0; i < 5; i++)
            {
                vehicle = vehiclefactory.GetVehicleFromVehicleFactory("bus");
                vehicle.AboutMe(GetRandomColor());
            }
            numOfDistinctRobots = vehiclefactory.TotalObjectsCreated;
            Console.WriteLine($"\n Now, total numbers of distinct vehicle object(s) is = {numOfDistinctRobots}\n");
            /*
             Here we are trying to get the 2 future vehicles.Note that: we need not create additional future vehicle if we have already created one of this category.
             */
            for (int i = 0; i < 2; i++)
            {
                vehicle = vehiclefactory.GetVehicleFromVehicleFactory("future");
                vehicle.AboutMe(GetRandomColor());
            }
            numOfDistinctRobots = vehiclefactory.TotalObjectsCreated;
            Console.WriteLine($"\n Now, total numbers of distinct vehicle object(s) is = {numOfDistinctRobots}\n");
            Console.ReadKey();
        }

        private static string GetRandomColor()
        {
            Random r = new Random();
            /*
             You can supply any number of your choice in nextInt argument.we are simply checking the random number generated is an even number or an odd number. And based on that we are choosing the color. For simplicity, we'll use only two colors-red and green.
             */
            int random = r.Next(100);
            if (random % 2 == 0)
            {
                return "red";
            }
            else
            {
                return "green";
            }
        }
    }
}

输出

下面是一个可能的输出(因为color是随机生成的)。这是在我的机器上第一次运行的时候。

***Flyweight Pattern Demo.***

One car is created with green color.
One car is created with red color.
One car is created with green color.

 Now, total numbers of distinct vehicle object(s) is = 1

One bus is created with green color.
One bus is created with red color.
One bus is created with green color.
One bus is created with red color.
One bus is created with red color.

 Now, total numbers of distinct vehicle object(s) is = 2

Vehicle 2050 is created with blue color.
Vehicle 2050 is created with blue color.

 Now, total numbers of distinct vehicle object(s) is = 3

这是另一个可能的输出。这是我的机器第二次运行的结果。

***Flyweight Pattern Demo.***

One car is created with red color.
One car is created with red color.
One car is created with red color.

 Now, total numbers of distinct vehicle object(s) is = 1

One bus is created with red color.
One bus is created with green color.
One bus is created with red color.
One bus is created with green color.
One bus is created with red color.

 Now, total numbers of distinct vehicle object(s) is = 2

Vehicle 2050 is created with blue color.
Vehicle 2050 is created with blue color.

 Now, total numbers of distinct vehicle object(s) is = 3

Note

输出会有变化,因为在这个例子中我随机选择了颜色。

分析

当且仅当对象当时不可用时,应用才创建对象。此后,它将缓存该对象以供将来重用。

问答环节

你能指出单例模式 和享元模式的主要区别吗?

Singleton 帮助你最多维护一个系统中需要的对象。换句话说,一旦创建了所需的对象,就不能再创建更多的对象了。您需要重用现有的对象。

Flyweight 模式通常与重但相似的对象(其中状态不相同)有关,因为它们可能会占用大量内存。因此,您尝试创建一个较小的模板对象集,可以动态地配置这些模板对象来制作这些重对象。这些更小的可配置对象被称为享元对象。当您处理许多大型对象时,可以在应用中重用它们。这种方法有助于减少大块内存的消耗。基本上,flyweight 使一个看起来像许多,这就是为什么 GoF 声明:“一个 flyweight 是一个共享对象,可以同时在多个上下文中使用。flyweight 在每个上下文中都是一个独立的对象——它与未共享的对象实例没有什么区别。”

图 10-3 向你展示了在使用 Flyweight 之前,如何可视化 Flyweight 模式的核心概念。

img/463942_2_En_10_Fig3_HTML.jpg

图 10-3

在使用飞锤之前

图 10-4 显示了使用飞锤后的设计。

img/463942_2_En_10_Fig4_HTML.jpg

图 10-4

使用飞锤后

因此,从图 10-4 中,你可以看到当我们将配置-1 应用于 Flyweight 对象时,创建了 Heavy-Object1,同样,当我们将配置-2 应用于 Flyweight 对象时,创建了 Heavy-Object2。您可以看到,特定于实例的内容(如我们的演示 1 中的颜色)可以传递给 flyweights 来生成这些重对象。在这个例子中,flyweight 对象就像一个公共模板,可以根据需要进行配置。

10.2****多线程 有什么影响?

如果在多线程环境中使用 new 运算符创建对象,最终可能会创建多个不需要的对象。这类似于单例模式,补救措施也类似。

10.3 使用 Flyweight 设计模式有什么好处?

以下是一些优点。

  • 您可以减少可同等控制的重物的内存消耗。

  • 您可以减少系统中的对象总数。

  • 您可以维护许多“虚拟”对象的集中状态。

10.4 使用 Flyweight 设计模式的相关挑战是什么?

这里有一些挑战。

  • 在这种模式中,您需要花一些时间来配置这些 flyweights。这些配置时间会影响应用的整体性能。

  • 要创建 flyweights,您需要从现有对象中提取一个公共模板类。这个额外的编程层可能很棘手,有时很难调试和维护。

10.5 能不能有一个 不可共享的 flyweight 接口

是的,flyweight 接口并不强制要求它总是可共享的。因此,在某些情况下,您可能有不可共享的 flyweight,具体的 flyweight 对象作为子对象。在演示 1 中,FutureVehicle 就是为此而生的。你可以看到它总是由蓝色组成,对于这辆车,无论客户提供什么颜色(红色或绿色)作为外在状态都没有关系。

10.6 既然 flyweights 的 内在数据 相同,可以试着分享一下。这是正确的吗?

是的。请注意,“创建一辆汽车”、“创建一辆公共汽车”和“创建 2050 辆汽车”是由工厂在 flyweight(具有内在状态)对象创建阶段在 flyweight 内部提供的。

10.7 客户如何处理这些蝇量级的 外来数据

当他们需要使用这个概念时,他们需要将这些信息(状态)传递给 flyweights。

10.8 外部数据不可共享。这是正确的吗?

是的。在实现这个模式之前,理解它是非常重要的。

10.9****车辆厂 在此次实施中的作用是什么?

它缓存了 flyweights,并提供了获取它们的方法。在本例中,有多个具有内在状态的对象可以共享。因此,将它们存放在一个中心位置总是一个好主意。

10.10 我可以将工厂类作为单例实现吗?

是的,你可以。事实上,在很多应用中,你可能会看到这一点。演示 2 描述了它。

演示 2

在这个例子中,VehicleFactory工厂类是作为单例实现的。因此,您可以用下面的代码替换演示 1 中的工厂类。

/// <summary>
/// The factory class for flyweights implemented as singleton.
/// </summary>
class VehicleFactory
{
    private static readonly VehicleFactory Instance = new VehicleFactory();
        private Dictionary<string, IVehicle> vehicles = new Dictionary<string, IVehicle>();

        private VehicleFactory()
        {
          vehicles.Add("car", new Car("One car is created"));
          vehicles.Add("bus", new Bus("One bus is created"));
          vehicles.Add("future", new FutureVehicle("Vehicle 2050 is created"));
        }
        public static VehicleFactory GetInstance
        {
            get
            {
                return Instance;
            }
        }
        /*
        * To count different types of vehicles
        * in a given moment.
        */
        public int TotalObjectsCreated
        {
            get
            {
                return vehicles.Count;
            }
        }

        public IVehicle GetVehicleFromVehicleFactory(string vehicleType)
        {
            IVehicle vehicleCategory = null;
            if (vehicles.ContainsKey(vehicleType))
            {
                vehicleCategory = vehicles[vehicleType];
                return vehicleCategory;
            }
            else
            {
               throw new Exception("Currently, the vehicle factory can have cars and buses only.");
            }
        }
    }

现在,在客户端代码中,您需要使用新的代码行来适应前面的更改,而不是使用下面的代码行(它被注释掉了)。

//VehicleFactory vehiclefactory = new VehicleFactory();
VehicleFactory vehiclefactory = VehicleFactory.GetInstance;

输出

当您使用这些新代码段运行应用时,您可能会得到(因为颜色是随机生成的)如下所示的输出。

***Flyweight Pattern Demo.***

One car is created with red color.
One car is created  with red color.
One car is created with red color.

 Now, total numbers of distinct vehicle object(s) is = 3

One bus is created with green color.
One bus is created with green color.
One bus is created with green color.
One bus is created with red color.
One bus is created with red color.

 Now, total numbers of distinct vehicle object(s) is = 3

Vehicle 2050 is created with blue color.
Vehicle 2050 is created with blue color.

 Now, total numbers of distinct vehicle object(s) is = 3

分析

注意,在这个实现中,我在开始时在构造函数中初始化了所有不同类型的车辆。因此,我一开始就使用了三个不同的车辆对象。因此,如果 2050 年我不需要任何公共汽车、汽车或交通工具,我就为这些对象浪费了内存。相反,在演示 1 中,如果这些对象中有任何一个不可用,那么工厂类会创建它并缓存它以备将来使用。所以,我投票支持演示 1,除非你修改演示 2,记住这个潜在的缺点。简而言之,无论何时使用这种模式,都要创建一个对象,填充所有必需的状态信息,并将其提供给客户机。每次客户端请求一个对象时,你的应用应该检查它是否可以重用一个现有的对象(填充了所需的状态);从而减少不必要的对象创建并节省内存消耗。

微软表示, Intern 方法使用 intern 池来搜索与字符串值相等的字符串。如果存在这样的字符串,则返回它在实习生池中的引用;否则,将对该字符串的引用添加到实习生池,然后返回该引用。英寸 NET Core 3.1,我执行下面这段代码的时候,firstStringthirdString都是指同一个字符串。结果,这段代码的最后一行返回 True,而当您比较firstStringsecondString时,情况并非如此,因为它们引用的是不同的对象。

#region test for in-built flyweight pattern
string firstString = "A simple string";
string secondString = new StringBuilder().Append("A").Append(" simple").Append(" string").ToString();
string thirdString = String.Intern(secondString);
// Different references.
Console.WriteLine((Object)secondString == (Object)firstString);
// Same reference.
Console.WriteLine((Object)thirdString == (Object)firstString);
#endregion

所以,你可以说。NET Core 3.1 遵循 Flyweight 模式。

十一、组合模式

本章涵盖了组合模式。

GoF 定义

将对象组成树结构来表示部分-整体层次结构。Composite 允许客户端统一处理单个对象和对象的组合。

概念

考虑一家出售不同种类干果的商店,如腰果、枣和核桃。这些物品都有一定的价格。让我们假设你可以购买这些单独的物品中的任何一种,或者你可以购买由不同的干果物品组成的“礼品包”(或盒装物品)。在这种情况下,数据包的开销是其组成部分的总和。组合模式在类似的情况下很有用,在这种情况下,您以相同的方式处理单个部分和部分的组合,以便可以统一处理它们。

这种模式对于表示对象的部分-整体层次结构很有用。在面向对象编程中,组合对象是由一个或多个相似对象组成的对象,其中每个对象都具有相似的功能。(这也称为对象之间的“有-有”关系。)这种模式在树结构数据中很常见,当您在这样的数据结构中实现这种模式时,您不需要区分树的分支和叶节点。因此,您可以使用该模式实现这两个关键目标。

  • 您可以将对象组合成一个树形结构,以代表部分整体层次结构。

  • 您可以统一访问组合对象(分支)和单个对象(叶节点)。因此,您可以降低代码的复杂性,并使应用不容易出错。

真实世界的例子

除了我们前面的例子,你还可以想到一个由许多部门组成的组织。一般来说,一个组织有很多员工。这些雇员中的一些被分组以形成一个部门,这些部门可以被进一步分组以构建组织的高层结构。

计算机世界的例子

我提到过树数据结构可以遵循这个概念,其中客户端可以以相同的方式处理树叶和非树叶(或树枝)。所以,当你看到一个分层的数据时,你可以得到一个线索,组合模式可能是有用的。XML 文件是这种树结构的常见例子。

Note

当你遍历树时,你经常会用到迭代器设计模式的概念,这将在第十八章中介绍。

履行

在这个例子中,我代表一个大学组织。假设有一个校长和两个系主任(hod),一个是计算机科学与工程(CSE),一个是数学(Math)。假设在数学系,目前有两个讲师(或老师),在计算机科学与工程系,有三个讲师(老师)。该组织的树形结构如图 11-1 所示。

img/463942_2_En_11_Fig1_HTML.jpg

图 11-1

一个大学组织,有一名校长、两名主任和五名讲师/教师

我们还假设在年底,CSE 部门的一名讲师提交了辞呈。以下示例考虑了提到的所有场景。

类图

图 11-2 显示了类图。

img/463942_2_En_11_Fig2_HTML.jpg

图 11-2

类图

解决方案资源管理器视图

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

img/463942_2_En_11_Fig3_HTML.jpg

图 11-3

解决方案资源管理器视图

示范

该演示以树形结构为特色。IEmployee是一个接口,有三个读写属性和一个叫做DisplayDetails()的方法。看起来是这样的。

interface IEmployee
    {
        // To set an employee name
        string Name { get; set; }
        // To set an employee department
        string Dept { get; set; }
        // To set an employee designation
        string Designation { get; set; }
        // To display an employee details
        void DisplayDetails();
    }

从相关的注释中,很容易理解这三个属性设置了雇员的姓名、对应的部门和职务。EmployeeCompositeEmployee具体类实现了这个接口。Employee班级(讲师)充当一个叶节点,另一个是非叶节点。一个或多个员工可以向一个部门主管报告。因此,它被视为非叶(或分支)节点。同样,所有的 hod 都向校长报告。所以,Principal是另一个非叶节点。

数学讲师名叫 m .乔伊和 m .鲁尼。CSE 的老师被命名为 c .萨姆,c .琼斯和 c .马里乌姆。这些讲师不监督任何人,所以他们被视为叶节点。

CompositeEmployee类维护一个列表和另外两个名为AddEmployee(...)RemoveEmployee(...)的方法。这些方法向列表中添加雇员或从列表中删除雇员。

现在浏览完整的实现,并参考支持性的评论。

using System;
/* For List<Employee> using
 * the following namespace.
 */
using System.Collections.Generic;

namespace CompositePattern
{
    interface IEmployee
    {
        // To set an employee name
        string Name { get; set; }
        // To set an employee department
        string Dept { get; set; }
        // To set an employee designation
        string Designation { get; set; }
        // To display an employee details
        void DisplayDetails();
    }
    // Leaf node
    class Employee : IEmployee
    {
        public string Name { get; set; }
        public string Dept { get; set; }
        public string Designation { get; set; }
        // Details of a leaf node
        public void DisplayDetails()
        {
            Console.WriteLine($"\t{Name} works in { Dept} department.Designation:{Designation}");
        }
    }
    // Non-leaf node
    class CompositeEmployee : IEmployee
    {
        public string Name { get; set; }
        public string Dept { get; set; }
        public string Designation { get; set; }

        // The container for child objects
        private List<IEmployee> subordinateList = new List<IEmployee>();

        // To add an employee
        public void AddEmployee(IEmployee e)
        {
            subordinateList.Add(e);
        }

        // To remove an employee
        public void RemoveEmployee(IEmployee e)
        {
            subordinateList.Remove(e);
        }

        // Details of a composite node
        public void DisplayDetails()
        {
            Console.WriteLine($"\n{Name} works in {Dept} department.Designation:{Designation}");
            foreach (IEmployee e in subordinateList)
            {
                e.DisplayDetails();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Composite Pattern Demo. ***");

            #region Mathematics department
            // 2 lecturers work in Mathematics department
            Employee mathTeacher1 = new Employee { Name = "M.Joy", Dept = "Mathematic", Designation = "Lecturer" };
            Employee mathTeacher2 = new Employee { Name = "M.Roony", Dept = "Mathematics", Designation = "Lecturer" };

            // The college has a Head of Department in Mathematics
            CompositeEmployee hodMaths = new CompositeEmployee { Name = "Mrs.S.Das", Dept = "Maths", Designation = "HOD-Maths" };

            // Lecturers of Mathematics directly reports to HOD-Maths
            hodMaths.AddEmployee(mathTeacher1);
            hodMaths.AddEmployee(mathTeacher2);
            #endregion

            #region Computer Science department
            // 3 lecturers work in Computer Sc. department
            Employee cseTeacher1 = new Employee { Name = "C.Sam", Dept = "Computer Science", Designation = "Lecturer" };
            Employee cseTeacher2 = new Employee { Name = "C.Jones", Dept = "Computer Science.", Designation = "Lecturer" };
            Employee cseTeacher3 = new Employee { Name = "C.Marium", Dept = "Computer Science", Designation = "Lecturer" };

            // The college has a Head of Department in Computer science
            CompositeEmployee hodCompSc = new CompositeEmployee { Name = "Mr. V.Sarcar", Dept = "Computer Sc.", Designation = "HOD-Computer Sc." };

            /* Lecturers of Computer Sc. directly reports to HOD-CSE */
            hodCompSc.AddEmployee(cseTeacher1);
            hodCompSc.AddEmployee(cseTeacher2);
            hodCompSc.AddEmployee(cseTeacher3);
            #endregion

            #region Top level management
            // The college also has a Principal
            CompositeEmployee principal = new CompositeEmployee { Name = "Dr.S.Som", Dept = "Planning-Supervising-Managing", Designation = "Principal" };

            /* Head of Departments's of Maths and Computer Science directly reports to Principal.*/
            principal.AddEmployee(hodMaths);
            principal.AddEmployee(hodCompSc);
            #endregion

            /*
             * Printing the leaf-nodes and branches in the same way. i.e. in each case, we are calling DisplayDetails() method.
             */
            Console.WriteLine("\nDetails of a Principal object is as follows:");
            // Prints the complete structure
            principal.DisplayDetails();

            Console.WriteLine("\nDetails of a HOD object is as follows:");
            /* Prints the details of Computer Science department */
            hodCompSc.DisplayDetails();

            // Leaf node
            Console.WriteLine("\nDetails of an individual employee(leaf node) is as follows:");
            mathTeacher1.DisplayDetails();

            /*
             * Suppose, one Computer Science lecturer(C.Jones)
             * is leaving now from the organization.
             */
            hodCompSc.RemoveEmployee(cseTeacher2);
            Console.WriteLine("\nAfter the resignation of C.Jones, the organization has the following members:");
            principal.DisplayDetails();
            // Wait for user
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Composite Pattern Demo. ***

Details of a Principal object is as follows:

Dr. S.Som works in Planning-Supervising-Managing department.Designation:Principal

Mrs. S.Das works in Maths department.Designation:HOD-Maths
        M.Joy works in Mathematic department.Designation:Lecturer
        M.Roony works in Mathematics department.Designation:Lecturer

Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc.
        C.Sam works in Computer Science department.Designation:Lecturer
        C.Jones works in Computer Science. department.Designation:Lecturer
        C.Marium works in Computer Science department.Designation:Lecturer

Details of a HOD object is as follows:

Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc.
        C.Sam works in Computer Science department.Designation:Lecturer
        C.Jones works in Computer Science. department.Designation:Lecturer
        C.Marium works in Computer Science department.Designation:Lecturer

Details of an individual employee(leaf node) is as follows:
        M.Joy works in Mathematic department.Designation:Lecturer

After the resignation of C.Jones, the organization has the following members:

Dr. S.Som works in Planning-Supervising-Managing department.Designation:Principal

Mrs. S.Das works in Maths department.Designation:HOD-Maths
        M.Joy works in Mathematic department.Designation:Lecturer
        M.Roony works in Mathematics department.Designation:Lecturer

Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc.
        C.Sam works in Computer Science department.Designation:Lecturer
        C.Marium works in Computer Science department.Designation:Lecturer

问答环节

11.1 使用组合设计模式的优势是什么?

以下是一些优点。

  • 在树状结构中,您可以统一处理组合对象(分支节点)和单个对象(叶节点)。在这个例子中,我使用了一个名为DisplayDetails的常用方法来打印组合对象结构(校长或部门领导)和单个对象(讲师)。

  • 使用这种设计模式实现部分-整体层次结构是很常见的。

  • 您可以轻松地向架构中添加新的组件,或者从架构中删除现有的组件。

11.2 使用组合设计模式有哪些挑战?

以下是一些缺点。

  • 如果您想要保持子节点的顺序(例如,如果解析树被表示为组件),您可能需要特别小心。

  • 如果您正在处理不可变的对象,您不能删除它们。

  • 您可以轻松地添加新组件,但是经过一段时间后,维护可能会很困难。有时,您可能想要处理具有特殊组件的组合材料。这种约束可能会导致额外的开发成本,因为您可能需要实现一个动态检查机制来支持这个概念。

11.3 在这个例子中,你使用了一个列表 数据结构 。其他数据结构可以用吗?

绝对的。没有放之四海而皆准的规则。您可以自由使用您喜欢的数据结构。GoF 还确认了没有必要使用通用数据结构。

如何将 迭代器设计模式 连接到组合设计模式?

在这个例子中,如果您想要检查一个组合对象架构,您可能需要迭代对象。此外,如果您想对一些分支执行一些特殊的活动,您可能需要迭代它的叶节点和非叶节点。

11.5 在你的实现中,在这个界面中,你只定义了一个方法, DisplayDetails 。但是您使用了附加的方法来添加和移除组合类中的对象( CompositeEmployee )。为什么不把这些方法放在接口中?

不错的观察。甚至 GoF 也讨论过这个问题。让我们看看如果在接口中放入AddEmployee(...)RemoveEmployee(...)方法会发生什么。在这种情况下,叶节点需要实现这些添加和删除操作。但是这种情况下会有意义吗?答案是否定的,在这种情况下,可能会显得你失去了透明性,但我相信你更安全,因为我在叶子节点中屏蔽了无意义的操作。这就是为什么 GoF 提到这种决定涉及到安全性和透明度之间的权衡。

我想用一个抽象类来代替接口。这是允许的吗?

在大多数情况下,简单的答案是肯定的,但是您需要理解抽象类和接口之间的区别。在典型的场景中,您可能会发现其中一个比另一个更有用。在整本书中,我只给出简单易懂的例子,所以你可能看不出它们之间有什么区别。

Note

在第三章的“问答”部分,我讨论了如何在抽象类和接口之间做出选择。

十二、桥接模式

本章涵盖了桥接模式。

GoF 定义

将抽象与其实现解耦,这样两者可以独立变化。

概念

这种模式也称为手柄/主体模式。使用它,您可以通过在实现类和抽象类之间提供一个桥梁来将它们解耦。

这个桥接口使得具体类的功能独立于接口实现者类。您可以在结构上改变不同种类的类,而不会相互影响。这种模式最初可能看起来很复杂,这就是为什么在这一章中,有两种不同的实现并有很多解释。当你浏览这些例子时,这个概念会更清楚。

真实世界的例子

在软件产品开发公司中,开发团队和营销团队都扮演着至关重要的角色。营销团队做市场调查,收集客户需求。开发团队在产品中实现这些需求,以满足客户需求。一个团队中的任何变化(如运营策略)都不应对另一个团队产生直接影响。在这种情况下,营销团队在产品的客户和软件公司的开发团队之间扮演着桥梁的角色。

计算机世界的例子

GUI 框架可以使用桥模式将抽象从平台特定的实现中分离出来。例如,使用这种模式,您可以从 Linux 或 macOS 的窗口实现中分离出一个窗口抽象。

履行

假设你需要为销售不同电子产品的卖家设计一个软件。为简单起见,我们假设卖家目前在销售电视机和 DVD 播放器,他以线上和线下(在不同的展厅)两种模式销售。

在这种情况下,您可以从图 12-1 或图 12-2 所示的设计开始。

img/463942_2_En_12_Fig2_HTML.jpg

图 12-2

方法 2

img/463942_2_En_12_Fig1_HTML.jpg

图 12-1

方法 1

经过进一步分析,您发现方法 1 很混乱,很难维护。

首先,方法 2 看起来更清晰,但是如果您想要包含新的价格(例如,ThirdPartyPriceFestiveTimePrice等)。),或者如果您想要包含新的电子产品(例如,空调、冰箱等)。),您面临着新的挑战,因为在这种设计中,各种元素紧密耦合。但是在真实的场景中,这种增强是经常需要的。

因此,为了将来的增强,您需要从一个松散耦合的系统开始,这样这两个层次(电子产品及其价格)中的任何一个都可以独立增长。桥接模式非常适合这种情况。因此,当您使用桥接模式时,结构可能看起来如图 12-3 所示。

img/463942_2_En_12_Fig3_HTML.jpg

图 12-3

使用桥接模式维护两个独立的层次结构

现在让我们从一个桥模式最常见的类图开始(见图 12-4 )。

img/463942_2_En_12_Fig4_HTML.jpg

图 12-4

经典的桥梁模式

在这个类图中,

  • Abstraction定义抽象接口,维护Implementor引用。在我的例子中,它是一个抽象类,但是非常重要的是要注意,你不应该假设你需要一个抽象类或接口来定义一个抽象。重要的是要知道这里的单词 abstraction 关于去除复杂性的方法说了什么。这些方法只是对客户端代码隐藏了它们工作的内部细节。

  • RefinedAbstraction(一个具体类)扩展了Abstraction定义的接口。这是客户在演示 1 中使用的。

  • 定义实现类的接口。这个接口方法不必与抽象方法完全对应。通常,它包括原语操作,抽象定义了基于这些原语的高级操作。还要注意,抽象类方法和实现者方法之间不需要一对一的映射。您可以在抽象类方法中使用实现者方法的组合。演示 2 说明了这一点,也可以参考 Q & A 12.5。

  • ConcreteImplementor(一个具体的类)实现了Implementor接口。

在即将到来的演示中,我遵循类似的设计。供您参考,我用注释指出了实现中的所有参与者。

类图

图 12-5 显示了类图。

img/463942_2_En_12_Fig5_HTML.jpg

图 12-5

类图

解决方案资源管理器视图

图 12-6 显示了程序的高层结构。

img/463942_2_En_12_Fig6_HTML.jpg

图 12-6

解决方案资源管理器视图

演示 1

在这个例子中,ElectronicGoods是我们的抽象类。它被放置在层级 1 中。该类定义如下。

// Abstraction
    public abstract class ElectronicGoods
    {
        public IPrice Price { get; set; }
        public string ProductType { get; set; }
        public abstract void Details();
    }

IPrice接口是我们的实现者接口。它维护第二个层次结构,定义如下。

// Implementor
    public interface IPrice
    {
        void DisplayDetails(string product);
    }

Television是覆盖Details()方法的具体抽象类,如下所示。

// Refined Abstraction
    public class Television : ElectronicGoods
    {
        /*
         * Implementation specific:
         * Delegating the task
         * to the Implementor object.
         */

        public override void Details()
        {
            Price.DisplayDetails(ProductType);
        }
    }

通过支持注释,您可以看到在Details()方法中,我从另一个层次结构中调用了DisplayDetails()方法,并传递了关于产品类型的信息。

具体的实现者(OnlinePrice, ShowroomPrice)捕获这些信息并在DisplayDetails(...)中使用它们。两个具体的实现是相似的。下面展示了其中的一个,供您参考。

    // This is ConcreteImplementor-1
    // OnlinePrice class
    public class OnlinePrice : IPrice
    {
        public void DisplayDetails(string productType)
        {
            Console.Write($"\n{productType} price at online is : 2000$");
        }
    }

为了简单起见,我没有改变演示 1 中的价格,但是在演示 2 中,您会注意到使用这种模式的灵活性,我也改变了价格。现在进行完整的演示,如下所示。

using System;

namespace BridgePattern
{
    // Abstraction
    public abstract class ElectronicGoods
    {
        public IPrice Price { get; set; }
        public string ProductType { get; set; }
        public abstract void Details();

    }
    // Refined Abstraction
    public class Television : ElectronicGoods
    {
        /*
         * Implementation specific:
         * Delegating the task
         * to the Implementor object.
         */

        public override void Details()
        {
            Price.DisplayDetails(ProductType);
        }
    }

    // Implementor
    public interface IPrice
    {
        void DisplayDetails(string product);
    }
    // This is ConcreteImplementor-1
    // OnlinePrice class
    public class OnlinePrice : IPrice
    {
        public void DisplayDetails(string productType)
        {
            Console.Write($"\n{productType} price at online is : 2000$");
        }
    }
    // This is ConcreteImplementor-2
    // ShowroomPrice class
    public class ShowroomPrice : IPrice
    {
        public void DisplayDetails(string productType)
        {
            Console.Write($"\n{productType} price at showroom is : 3000$");
        }
    }
    // Client code
    class Client
    {
        static void Main(string[] args)

        {
            Console.WriteLine("***Bridge Pattern Demo.***");
            Console.WriteLine("Verifying the market price of a television.");
            ElectronicGoods eItem = new Television();
            eItem.ProductType = "Sony Television";
            // Verifying online  price
            IPrice price = new OnlinePrice();
            eItem.Price = price;
            eItem.Details();
            // Verifying showroom price
            price = new ShowroomPrice();
            eItem.Price = price;
            eItem.Details();
        }
    }
}

输出

这是输出。

***Bridge Pattern Demo.***
Verifying the market price of a television.

Sony Television price at online is : 2000$
Sony Television price at showroom is : 3000$

附加实现

我在这一章中包含了一个额外的实现,以帮助您了解使用这一模式的灵活性。在这个例子中,我使用了构造函数,而不是属性。但是在我向您展示灵活性之前,让我们假设卖方对出售的产品提供折扣。

为了适应这一点,在这个实现中,让我们在抽象类中添加下面的方法(ElectronicGoods)。

// Additional method
public void Discount(int percentage)
{
     price.GetDiscount(percentage);
}

以及实现接口中的以下方法(IPrice)。

void GetDiscount(int percentage);

因为Discount方法是而不是抽象的,所以Television类或者ElectronicGoods的任何派生类继承了这个方法。但是由于在IPrice接口中添加了GetDiscount(int percentage)方法,具体的实现者需要实现这个方法。下面是来自OnlinePrice类实现者的这样一个实现。

public void GetDiscount(int percentage)
{
    Console.Write($"\nAt online, you can get upto {percentage}% discount.");
}

Note

同样,这些修改只是为了提供对折扣方法的支持。你应该感觉不到原来的桥模式受到改变的影响。为了保持演示 1 简短,我没有包括这个方法。

现在是灵活性部分。让我们假设卖家想卖叫做 DVD 的电子产品。卖家有时会对所有产品打折,但在节日期间,只对 DVD 提供额外折扣。

因此,DVD 类现在需要包含另一种方法来提供双重折扣(正常折扣+附加折扣)。你不能在ElectronicGoods抽象类中添加这个方法,因为在那种情况下,Television 类也会有你不想要的这个方法。最重要的是,尽管包含了 DVD 类,但是旧的代码结构不能改变。

桥接模式解决了这个问题。类图给了你一个线索。除此之外,请注意我是如何在 DVD 类中实现以下方法的。

        // Specific method in DVD
        public void DoubleDiscount()
        {
            // Normal discount(10%)
            Discount(10);
            // Festive season additional discount(5%)
            Discount(5);
        }

Note

你可以看到在DoubleDiscount()方法内部,使用了ElectronicGoodsDiscount(...)方法,所以我是按照超类抽象来编码的,它允许抽象和实现独立变化。

因为我使用了构造函数而不是属性,所以让我们先来看看变化。以下是用Details(...)Discount(...)方法进行的抽象。

    // Abstraction
    public abstract class ElectronicGoods
    {
       //public IPrice Price { get; set; }
        private IPrice price;
        public string type;
        public double cost;
        public ElectronicGoods(IPrice price)
        {
            this.price = price;
        }
        public void Details()
        {
            price.DisplayDetails(type, cost);
        }
        // Additional method
        public void Discount(int percentage)
        {
           price.GetDiscount(percentage);
        }
    }

现在,这是第一个精炼的抽象(Television类)。在这个类中,没有定义新的方法,这仅仅意味着Television类准备使用它的父类方法,并且不希望提供任何新的行为。

    // Refined Abstraction-1
    // Television class uses the default discount method.
    public class Television : ElectronicGoods
    {
        public Television(IPrice price):base(price)
        {
            this.type = "Television";
            this.cost = 2000;
        }
        // No additional method exists for Television

    }

下面是我们第二个精炼抽象(DVD类),是新加入的。在这个类中,定义了一个名为DoubleDiscount(...)的新方法,这仅仅意味着客户端可以使用这个特定于 DVD 类的方法。这个方法是在超类抽象中编码的,其他层次结构不会因为这个 DVD 类的添加而受到影响。(我的意思是,由于在层级 1 中添加了 DVD 类(或任何其他类似的类),您不需要更改位于层级 2 中的ShowroomPriceOnlinePrice等。即使您向抽象类添加了一些额外的方法,您也不需要对层次结构 2 进行更改。类似地,如果您在 implementor 中添加一个方法,您不需要在 hierarchy 1 中进行更改。)

Note

简而言之,这里您将“客户端使用的方法”与“这些方法是如何实现的”分开

    // Refined Abstraction-2
    // DVD class can give additional discount.
    public class DVD : ElectronicGoods
    {
        public DVD(IPrice price) : base(price)
        {
            this.type = "DVD";
            this.cost = 3000;
        }

        // Specic method in DVD
        public void DoubleDiscount()
        {
            // Normal discount(10%)
            Discount(10);
            // Festive season additional discount(5%)
            Discount(5);
        }
}

对照图 12-7 所示的类图。然后直接按照完整的演示输出。对于这个修改后的实现,我没有显示 Solution Explorer 视图,因为根据前面的讨论和下面的类图,它很容易理解。

img/463942_2_En_12_Fig7_HTML.jpg

图 12-7

演示 2 的类图

类图

图 12-7 显示了修改后的类图。

演示 2

下面是完整的实现。

using System;

namespace BridgePatternDemo2
{
    // Abstraction
    public abstract class ElectronicGoods
    {
       //public IPrice Price { get; set; }
        private IPrice price;
        public string type;
        public double cost;
        public ElectronicGoods(IPrice price)
        {
            this.price = price;
        }
        public void Details()
        {
            price.DisplayDetails(type,cost);
        }
        // additional method
        public void Discount(int percentage)
        {
           price.GetDiscount(percentage);
        }

    }
    // Refined Abstraction-1
    // Television class uses the default discount method.
    public class Television : ElectronicGoods
    {
        public Television(IPrice price):base(price)
        {
            this.type = "Television";
            this.cost = 2000;
        }
        // No additional method exists for Television

    }
    // Refined Abstraction-2
    // DVD class can give additional discount.
    public class DVD : ElectronicGoods
    {
        public DVD(IPrice price) : base(price)
        {
            this.type = "DVD";
            this.cost = 3000;
        }

        // Specic method in DVD
        public void DoubleDiscount()
        {
            // Normal discount(10%)
            Discount(10);
            // Festive season additional discount
            Discount(5);
        }
    }

    // Implementor
    public interface IPrice
    {
        void DisplayDetails(string product, double price);
        // additional method
        void GetDiscount(int percentage);
    }
    // This is ConcreteImplementor-1
    // OnlinePrice class
    public class OnlinePrice : IPrice
    {
        public void DisplayDetails(string productType, double price)
        {
            Console.Write($"\n{productType} price at online is : {price}$");
        }
        public void GetDiscount(int percentage)
        {
            Console.Write($"\nAt online, you can get upto {percentage}% discount.");
        }
    }
    // This is ConcreteImplementor-2
    // ShowroomPrice class
    public class ShowroomPrice : IPrice
    {
        public virtual void DisplayDetails(string productType, double price)
        {
            // Showroom price is 300$ more
            Console.Write($"\n{productType} price at showroom is : {price + 300}$");
        }
        public void GetDiscount(int percentage)
        {
            Console.Write($"\nAt showroom, additional {percentage}% discount can be approved.");
        }
    }
    // Client code
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Alternative Implementation of Bridge Pattern.***");
            #region Television details
            Console.WriteLine("Verifying the market price of a television.");
             ElectronicGoods eItem = new Television(new OnlinePrice());
            // Verifying online price details
            eItem.Details();
            // Giving 10% discount
            eItem.Discount(10);
            // Verifying showroom price
            eItem = new Television(new ShowroomPrice());
            eItem.Details();
            // Giving 10% discount
            eItem.Discount(10);
            #endregion

            #region DVD details
            Console.WriteLine("\n\nNow checking the DVD details.");
            // Verifying online  price
            eItem = new DVD(new OnlinePrice());
            eItem.Details();
            // Giving 10% discount
            eItem.Discount(10);
            // Verifying showroom price
            eItem = new DVD(new ShowroomPrice());
            eItem.Details();
            Console.WriteLine("\nIn showroom, you want to give double discounts at festive season.");
            Console.WriteLine("For DVD, you can get double discounts using the DoubleDiscount() method.");
            //eItem.Discount();
            Console.WriteLine("For example, in festive season:");
            ((DVD)eItem).DoubleDiscount();
            #endregion
        }
    }
}

输出

***Alternative Implementation of Bridge Pattern.***
Verifying the market price of a television.

Television price at online is : 2000$
At online, you can get upto 10% discount.
Television price at showroom is : 2300$
At showroom, additional 10% discount can be approved.

Now checking the DVD details.

DVD price at online is : 3000$
At online, you can get upto 10% discount.
DVD price at showroom is : 3300$
In showroom, you want to give double discounts at festive season.
For DVD , you can get double discounts using the DoubleDiscount() method.
For example, in festive season:

At showroom, additional 10% discount can be approved.
At showroom, additional 5% discount can be approved.

问答环节

12.1 这种模式如何让我的 编程 生活更轻松?

本章介绍了两个具有以下主要意图的示例。

  • 避免项目及其相应价格之间的紧密耦合

  • 维护两个不同的层次结构,在这两个层次结构中,两者都可以扩展而不会相互影响

  • 处理实现在它们之间共享的多个对象

你可以使用简单的子类化来代替这种设计。这是正确的吗?

不。通过简单的子类化,你的实现不能动态变化。您的实现可能看起来行为不同,但是它们在编译时被绑定到抽象。

12.3 我可以在抽象类中使用构造函数而不是属性吗?

是的。有些开发人员更喜欢构造函数而不是属性(或者 getter-setter 方法)。因此,我在两个演示中向您展示了这两种用法。

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

以下是一些优点。

  • 实现不局限于抽象。

  • 抽象和实现都可以独立发展。

  • 具体类独立于接口实现者类。换句话说,其中一个的变化不会影响另一个。因此,您也可以用不同的方式改变抽象和实现层次。

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

整体结构可能变得复杂。这里你不直接调用一个方法。相反,抽象层将工作委托给实现层。因此,在执行操作时,您可能会注意到轻微的性能影响。

有时,桥接模式与适配器模式相混淆。(请记住,适配器模式的主要目的是只处理不兼容的接口。)

当你使用抽象类方法时,你可以结合使用实现者方法。在演示 2 中,您会看到这一点。”你能详细说明一下吗?

演示 2 中的DoubleDiscount()方法显示了这一点,其中您调用了两次Discount()方法。再举一个例子,假设实现者有下面的GiveThanks()方法。

public interface IPrice
    {
        void DisplayDetails(string product, double price);
        // Additional method
        void GetDiscount(int percentage);
        // Added for Q&A session
        void GiveThanks();
    }

具体实现者实现了该方法。假设OnlinePrice如下实现了这个方法。

public void GiveThanks()
 {
  Console.Write("Thank you, please visit the site again.");
 }

另一个具体的实现者ShowroomPrice如下实现这个方法。

public void GiveThanks()
{
Console.Write("Thank you for coming. please visit the shop again.");
 }

现在,在抽象内部,您可以添加这个方法(如果您愿意)。例如,您更新后的Discount可能如下所示。

// Additional method
public void Discount(int percentage)
{
 price.GetDiscount(percentage);
 // Added for Q&A session
 price.GiveThanks();
}

当您使用这些更改运行程序(演示 2)时,您会看到以下修改后的输出。

***Alternative Implementation of Bridge Pattern.***
Verifying the market price of a television.

Television price at online is : 2000$
At online, you can get upto 10% discount.Thank you, please visit the site again.
Television price at showroom is : 2300$
At showroom, additional 10% discount can be approved. Thank you for coming. Please visit the shop again.

Now checking the DVD details.

DVD price at online is : 3000$
At online, you can get upto 10% discount. Thank you, please visit the site again.
DVD price at showroom is : 3300$
In showroom, you want to give double discounts at festive season.
For DVD , you can get double discounts using the DoubleDiscount() method.
For example, in festive season:

At showroom, additional 10% discount can be approved. Thank you for coming. Please visit the shop again.
At showroom, additional 5% discount can be approved. Thank you for coming. Please visit the shop again.

Note

一个高级抽象方法可以包含多个实现者方法,但是客户可能不知道这一点。

十三、访问者模式

本章介绍访问者模式。

GoF 定义

表示要在对象结构的元素上执行的操作。Visitor 允许您定义一个新的操作,而不改变它所操作的元素的类。

概念

在这个模式中,您将算法从对象结构中分离出来。因此,您可以在对象上添加新的操作,而无需修改它们现有的体系结构。该模式支持打开/关闭原则(即允许扩展,但不允许修改实体,如类、函数等)。

Note

当您将这种设计模式与组合模式相结合时,您可以体验到这种设计模式的真正威力,如本章后面的实现所示。

为了理解这种模式,让我们考虑一个场景,其中有一个名为Number的抽象类,如下所示。

    /// <summary>
    /// Abstract class- Number
    /// </summary>
    abstract class Number
    {
        private int numberValue;
        private string type;
        public Number(string type, int number)
        {
            this.type = type;
            this.numberValue = number;
        }
        // I want to restrict the change in original data
        // So, no setter is present here.
        public int NumberValue
        {
            get
            {
                return numberValue;
            }
        }
        public string TypeInfo
        {
            get
            {
                return type;
            }
        }
        public abstract void SomeMethod();
    }

Number派生出两个具体的类SmallNumberBigNumber,定义如下。

    /// <summary>
    /// Concrete class-SmallNumber
    /// </summary>

    class SmallNumber : Number
    {
        public SmallNumber(string type, int number) : base(type, number)
        { }

        public override void SomeMethod()
        {
            // Some code
        }
    }
    /// <summary>
    /// Concrete class-BigNumber
    /// </summary>
    class BigNumber : Number
    {
        public BigNumber(string type, int number) : base(type, number)
        { }

        public override void SomeMethod
        {
            // Some code
        }
    }

这种继承层次很容易理解。现在让我们来看一段你和顾客之间的假想对话。

客户:我希望您创建一个设计,其中每个具体的类都有一个增加数值的方法。

你:那容易。我将在 Number 类中引入一个公共方法,结果是每个具体的类都可以获得该方法。

顾客:等等。我希望您使用一个递增数字的方法,但是在每次调用SmallNumber类中的方法时,它应该将数字递增 1,对于BigNumber类,它应该将数字递增 10。

你:那不成问题。我可以在Number类中定义一个抽象方法,在每个派生类中,你可以不同地实现它。

顾客:我没问题。

您可以一次性接受这个客户请求,但是如果您的客户经常要求类似的请求,您是否有可能在每个类中引入这样的方法,特别是当整个代码结构非常复杂的时候?还有,在一个树形结构中,如果只是一个分支节点,你能想象这些变化对其他节点的影响吗?

这一次你可能会明白问题所在,可能会想出一些办法来对付你那些善变的顾客。访问者模式可以在这种情况下帮助你。您可以在演示 1 中看到这样的实现。

真实世界的例子

想象一个出租车预订的场景。当出租车到达你家门口,你进入车内,出租车司机控制交通。他可以通过一条你不熟悉的路线带你去目的地,最糟糕的情况下,还可以更改目的地(由于访客模式使用不当而产生)。

计算机世界的例子

当公共 API 需要支持插件操作时,这种模式非常有用。然后,客户端可以在不修改源代码的情况下对一个类(使用访问类)执行它们想要的操作。

履行

让我们继续讨论访问者模式。你可以看到图 13-1 中的类图。它向您提示了我在接下来的演示中是如何实现它的。我引入了一个新的层次结构,其中,在顶层,有一个名为IVisitor的接口,带有两个名为VisitBigNumbers(..)VisitSmallNumbers(..)的方法。看起来是这样的。

    interface IVisitor
    {
        // A visit operation for SmallNumber class
        void VisitSmallNumbers(SmallNumber number);

        // A visit operation for BigNumber class
        void VisitBigNumbers(BigNumber number);
    }

Note

代替使用不同的名称(VisitSmallNumbers(..),VisitBigNumbers(...))对于这些方法,您可以使用相同的方法(例如,VisitNumbers(...))通过使用方法重载。在问答环节,我讨论了在这个例子中使用不同名称的原因。

IncrementNumberVisitor实现这个接口方法,如下所示。

    class IncrementNumberVisitor : IVisitor
    {
        public void VisitSmallNumbers(SmallNumber number)
        {
            Number currentNumber = number as Number;
            /*
             I do not want (infact I can't change because it's readonly now) to modify the original data. So, I'm making a copy of it before I use it.
            */
            int temp = currentNumber.NumberValue;
            // For SmallNumber's incrementing by 1
            Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I use it as:{++temp} for rest of my code.");
            // Remaining code, if any
        }

        public void VisitBigNumbers(BigNumber number)
        {
            Number currentNumber = number as Number;
            /*
              * I do not want (infact I can't change because it's readonly now)
             * to modify the original data.
             * So, I'm making a copy of it before I use it.
             */
            int temp = currentNumber.NumberValue;
            // For BigNumber's incrementing by 10
            Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I convert it as:{temp+10} for rest of my code.");
            // Remaining code, if any
        }
    }

值得注意的一点是,我不想修改原始数据。因此,在Number类中,您只能看到 getter 方法。这是因为我假设一旦你从具体的Number类中获得数据,你可以用不同的方式使用它,但是你不允许改变原始数据。(这是一个更好的做法,但这是可选的)。

在这个例子中,我维护了一个名为numberListList数据结构,它用不同类型的数字初始化一个对象结构。因此,在演示 1 中,您会得到以下代码段。

    class NumberCollection
    {
        List<Number> numberList = new List<Number>();
        // List contains both SmallNumber's and BigNumber's
        public NumberCollection()
        {
            numberList.Add(new SmallNumber("small-1", 10));
            numberList.Add(new SmallNumber("small-2", 20));
            numberList.Add(new SmallNumber("small-3", 30));
            numberList.Add(new BigNumber("big-1", 200));
            numberList.Add(new BigNumber("big-2", 150));
            numberList.Add(new BigNumber("big-3", 70));
        }
            // remaining code

同样,您可以用这种方式初始化列表,或者一旦您初始化了一个空列表,您可以使用AddNumberToList(...)方法在客户端代码中提供列表的元素。类似地,您可以使用RemoveNumberFromList(...)方法从列表中删除一个元素。在演示 1 中,我没有使用这些方法,但是我保留了它们供您参考。所以,注意以下方法。

        public void AddNumberToList(Number number)
        {
            numberList.Add(number);
        }
        public void RemoveNumberFromList(Number number)
        {
            numberList.Remove(number);
        }

现在我们来看最重要的部分。在Number类中,您会看到下面一行。

public abstract void Accept(IVisitor visitor);

来自Number的具体派生类根据需要覆盖它。例如,SmallNumber会按如下方式覆盖它。

      public override void Accept(IVisitor visitor)
      {
        visitor.VisitSmallNumbers(this);
      }

并且BigNumber实现如下。

      public override void Accept(IVisitor visitor)
      {
        visitor.VisitBigNumbers(this);
      }

您可以看到,在Accept方法中,您可以传递一个“特定的访问者对象”,它反过来可以跨类调用适当的方法。SmallNumberBigNumber类都通过这种方法暴露自己(这里封装受到了损害)。现在客户机与访问者进行交互,您可以在访问者层次结构中添加新方法。因此,在客户端代码中,您会注意到如下代码段。

NumberCollection numberCollection = new NumberCollection();
// some other code
// ....
IncrementNumberVisitor incrVisitor = new IncrementNumberVisitor();
// Visitor is visiting the list
Console.WriteLine("IncrementNumberVisitor is about to visit the list:");
numberCollection.Accept(incrVisitor);

类图

图 13-1 为类图。这一次,我希望您在类图中显示完整的方法签名,因此,为了在一个公共位置容纳所有内容,参与者的大小变得比通常要小。

img/463942_2_En_13_Fig1_HTML.jpg

图 13-1

类图

解决方案资源管理器视图

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

img/463942_2_En_13_Fig2_HTML.jpg

图 13-2

解决方案资源管理器视图

演示 1

这是完整的代码。

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    /// <summary>
    /// Abstract class- Number
    /// </summary>
    abstract class Number
    {
        private int numberValue;
        private string type;
        public Number(string type, int number)
        {
            this.type = type;
            this.numberValue = number;
        }
        //I want to restrict the change in original data
        //So, no setter is present here.
        public int NumberValue
        {
            get
            {
                return numberValue;
            }
        }
        public string TypeInfo
        {
            get
            {
                return type;
            }
        }
        public abstract void Accept(IVisitor visitor);
    }
    /// <summary>
    /// Concrete class-SmallNumber
    /// </summary>

    class SmallNumber : Number
    {
        public SmallNumber(string type, int number) : base(type, number)
        { }

        public override void Accept(IVisitor visitor)
        {
            visitor.VisitSmallNumbers(this);
        }
    }
    /// <summary>
    /// Concrete class-BigNumber
    /// </summary>
    class BigNumber : Number
    {
        public BigNumber(string type, int number) : base(type, number)
        { }

        public override void Accept(IVisitor visitor)
        {
            visitor.VisitBigNumbers(this);
        }
    }
    class NumberCollection
    {
        List<Number> numberList = new List<Number>();
        //List contains both SmallNumber's and BigNumber's
        public NumberCollection()
        {
            numberList.Add(new SmallNumber("small-1", 10));
            numberList.Add(new SmallNumber("small-2", 20));
            numberList.Add(new SmallNumber("small-3", 30));
            numberList.Add(new BigNumber("big-1", 200));
            numberList.Add(new BigNumber("big-2", 150));
            numberList.Add(new BigNumber("big-3", 70));
        }
        public void AddNumberToList(Number number)
        {
            numberList.Add(number);
        }
        public void RemoveNumberFromList(Number number)
        {
            numberList.Remove(number);
        }
        public void DisplayList()
        {
            Console.WriteLine("Current list is as follows:");
            foreach (Number number in numberList)
            {
                Console.Write(number.NumberValue+"\t");
            }
            Console.WriteLine();
        }
        public void Accept(IVisitor visitor)
        {
            foreach (Number n in numberList)
            {
                n.Accept(visitor);
            }
        }
    }
    /// <summary>
    /// The Visitor interface.
    /// GoF suggests to make visit opearation for each concrete class of /// ConcreteElement (in our example,SmallNumber and BigNumber) in the /// object structure
    /// </summary>
    interface IVisitor
    {
        //A visit operation for SmallNumber class
        void VisitSmallNumbers(SmallNumber number);

        //A visit operation for BigNumber class
        void VisitBigNumbers(BigNumber number);
    }
    /// <summary>
    /// A concrete visitor-IncrementNumberVisitor
    /// </summary>
    class IncrementNumberVisitor : IVisitor
    {
         public void VisitSmallNumbers(SmallNumber number)
        {
            Number currentNumber = number as Number;
            /*
             I do not want( infact I can't change because it's readonly now) to modify the original data. So, I'm making a copy of it before I use it.
            */
            int temp = currentNumber.NumberValue;
            //For SmallNumber's incrementing by 1
            Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I use it as:{++temp} for rest of my code.");
            //Remaining code, if any
        }

        public void VisitBigNumbers(BigNumber number)
        {
            Number currentNumber = number as Number;
            /*
             I do not want( infact I can't change because it's readonly now) to modify the original data. So, I'm making a copy of it before I use it.
            */
            int temp = currentNumber.NumberValue;
            //For BigNumber's incrementing by 10
            Console.WriteLine($"{currentNumber.TypeInfo} is {currentNumber.NumberValue}; I convert it as:{temp+10} for rest of my code.");
            //Remaining code, if any
        }
    }
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Visitor Pattern Demo***\n");
            NumberCollection numberCollection = new NumberCollection();
            //Showing the current list
            numberCollection.DisplayList();
            IncrementNumberVisitor incrVisitor = new IncrementNumberVisitor();
            //Visitor is visiting the list
            Console.WriteLine("IncrementNumberVisitor is about to visit the list:");
            numberCollection.Accept(incrVisitor);
            //Showing the current list
            numberCollection.DisplayList();

            Console.ReadLine();
        }
    }
}

输出

这是输出。

***Visitor Pattern Demo***

Current list is as follows:
10      20      30      200     150     70
IncrementNumberVisitor is about to visit the list:
small-1 is 10; I use it as:11 for rest of my code.
small-2 is 20; I use it as:21 for rest of my code.
small-3 is 30; I use it as:31 for rest of my code.
big-1 is 200; I convert it as:210 for rest of my code.
big-2 is 150; I convert it as:160 for rest of my code.
big-3 is 70; I convert it as:80 for rest of my code.
Current list is as follows:
10      20      30      200     150     70

问答环节

13.1 什么时候应该考虑实现访问者设计模式?

这里有一些要考虑的用例。

  • 您需要向一组对象添加新的操作,而不改变它们对应的类。这是实现访问者模式的主要目的。当运营经常变化时,这种方法可以成为你的救星。

  • 如果需要更改各种操作的逻辑,只需通过访问者实现即可。

这种模式有什么缺点吗?

这种模式有一些缺点。

  • 我前面提到过,封装不是它的主要关注点。因此,您可以使用访问者来打破封装的力量。

  • 如果您需要频繁地向现有架构添加新的具体类,那么访问者层次结构将变得难以维护。例如,假设您想在原来的层次结构中添加另一个具体的类。在这种情况下,您需要相应地修改 visitor 类的层次结构。

13.3 你为什么说一个 visitor 类会违反 封装

请注意,在Accept方法中,您可以传递一个“特定的访问者对象”,它反过来可以跨类调用适当的方法。SmallNumberBigNumber类都通过这种方法暴露自己,这里封装性受到了损害。

此外,在许多情况下,您可能会看到访问者需要在一个复合结构中四处移动,以从其中收集信息,然后它可以使用这些信息进行修改。(尽管在演示 1 中,我不允许这种修改)。所以,当你提供这种支持时,你违背了封装的核心目标。

13.4 为什么这种模式会损害封装?

在这里,您对一组也可能是异构的对象执行一些操作。但是您的约束是您不能改变它们对应的类。因此,您的访问者需要一种方法来访问这些对象的成员。为了满足这一要求,您需要向访问者公开信息。

13.5 在演示 1 中,我看到在 visitor 接口中,你是 而不是 使用方法重载 的概念。例如,您编写了如下的接口方法。

        // A visit operation for SmallNumber class
        void VisitSmallNumbers(SmallNumber number);

        // A visit operation for BigNumber class
        void VisitBigNumbers(BigNumber number);

在我看来,你可以使用类似下面这样的东西。

        // A visit operation for SmallNumber class
        void VisitNumbers(SmallNumber number);

        // A visit operation for BigNumber class
        void VisitNumbers(BigNumber number);

这是正确的吗?

接得好。是的,你可以这样做,但是我想让你注意到这些方法在做不同的工作(一个是将 int 增加 1,另一个是增加 10)。通过使用不同的名称,当你浏览代码时,我试图在Number类层次结构中区分它们。

在《Java 设计模式(a press,2018)一书中,我使用了你提到的方法。你只需要记住这些接口方法应该只针对特定的类,比如SmallNumber或者BigNumber

在演示 2 中,我将访问者模式与组合模式相结合,使用了重载方法。

13.6 假设在演示 1 中,我增加了 Number 的另一个具体子类叫做 UndefinedNumber 。我应该如何进行?我应该在访问者界面中使用另一个特定的方法吗?

没错。您需要定义一个特定于这个新类的新方法。因此,您的接口可能如下所示(这里使用了方法重载)。

   interface IVisitor
    {
        // A visit operation for SmallNumber class
        void VisitNumbers(SmallNumber number);
        // A visit operation for BigNumber class
        void VisitNumbers(BigNumber number);
        // A visit operation for UndefinedNumber class
        void VisitNumbers(UndefinedNumber number);
    }

然后,您需要在具体的 visitor 类中实现这个新方法。

假设,我需要在现有架构中支持新的操作。我应该如何处理访问者模式?

对于每个新操作,创建一个新的 Visitor 子类,并在其中实现操作。然后,按照我在前面的例子中向您展示的方式访问您现有的结构。例如,如果您想要调查SmallNumber类实例的int值是否大于 10,以及对于BigNumber类,它们是否大于 100 的方法。对于这个需求,您可以添加一个新的具体类,InvestigateNumberVisitor,,它继承自IVisitor,定义如下。

    /// <summary>
    /// Another concrete visitor-InvestigateNumberVisitor
    /// </summary>
    class InvestigateNumberVisitor : IVisitor
    {
        public void VisitSmallNumbers(SmallNumber number)
        {
            Number currentNumber = number as Number;
            int temp = currentNumber.NumberValue;
            // Checking whether the number is greater than 10 or not
            string isTrue = temp > 10 ? "Yes" : "No";
            Console.WriteLine($"Is {currentNumber.TypeInfo} greater than 10 ? {isTrue}");
        }
        public void VisitBigNumbers(BigNumber number)
        {
            Number currentNumber = number as Number;
            int temp = currentNumber.NumberValue;
            // Checking whether the number is greater than 100 or not
            string isTrue = temp > 100 ? "Yes" : "No";
            Console.WriteLine($"Is {currentNumber.TypeInfo} greater than 100 ? {isTrue}");
        }
    }

现在,在客户端代码中,您可以添加下面的代码段来检查它是否正常工作。

// Visitor-2
InvestigateNumberVisitor investigateVisitor = new InvestigateNumberVisitor();
// Visitor is visiting the list
Console.WriteLine("InvestigateNumberVisitor is about to visit the list:");
numberCollection.Accept(investigateVisitor);

一旦你在demonstration 1,中添加了这些段,使用如下的客户端代码。

  class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Visitor Pattern Demo2.***\n");
            NumberCollection numberCollection = new NumberCollection();
            // Showing the current list
            numberCollection.DisplayList();
            // Visitor-1
            IncrementNumberVisitor incrVisitor = new IncrementNumberVisitor();
            // Visitor is visiting the list
            Console.WriteLine("IncrementNumberVisitor is about to visit the list:");
            numberCollection.Accept(incrVisitor);
            // Visitor-2
            InvestigateNumberVisitor investigateVisitor = new InvestigateNumberVisitor();
            // Visitor is visiting the list
            Console.WriteLine("InvestigateNumberVisitor is about to visit the list:");
            numberCollection.Accept(investigateVisitor);

            Console.ReadLine();
        }
    }

运行该程序时,您可以获得以下输出。

***Visitor Pattern Demo2.***

Current list is as follows:
10      20      30      200     150     70
IncrementNumberVisitor is about to visit the list:
Original data:10; I use it as:11
Original data:20; I use it as:21
Original data:30; I use it as:31
Original data:200; I use it as:210
Original data:150; I use it as:160
Original data:70; I use it as:80
InvestigateNumberVisitor is about to visit the list:
Is small-1 greater than 10 ? No
Is small-2 greater than 10 ? Yes
Is small-3 greater than 10 ? Yes
Is big-1 greater than 100 ? Yes
Is big-2 greater than 100 ? Yes
Is big-3 greater than 100 ? No

您可以从 Apress 网站下载这个修改示例的完整代码。我将它合并到名为 VisitorPatternDemo2 的名称空间中。

我看到你正在用 SmallNumber 和 BigNumber 的对象初始化 numberList。创建这样的结构是强制性的吗?

不。我做了一个容器,帮助客户一次就能顺利访问。在另一个不同的版本中,您可以看到,在遍历列表之前,您首先初始化一个空列表,然后在客户端代码中添加(或移除)元素。

要理解前一行,您可以参考演示 2,在演示 2 中,我只在客户端代码中创建了容器类。

一起使用访问者模式和组合模式

在演示 1 中,您看到了访问者设计模式的一个示例,并且在问答会话中,您经历了它的一个扩展版本。现在我将向您展示另一个实现,但是这一次,我将它与组合模式结合起来。

让我们考虑一下第十一章中的复合设计模式的例子。在这个例子中,有一个学院有两个不同的系。每个系都有一名系主任和多名教授/讲师。所有的 hod 都向学院的校长报告。

图 13-3 显示了本例的树形结构。学院的结构和第十一章中描述的一样。数学讲师/教师是 M. Joy 和 M. Roony,CSE 教师是 C. Sam、C. Jones 和 C. Marium。这些讲师不监管任何人,所以在树形图中被当作叶节点。S. Som 博士是校长,职位最高。两个 HOD(s . Das 夫人(HOD-Math)和 V. Sarcar 先生(HOD-Comp。Sc)向负责人报告。hod 和 principal 是非叶节点。

img/463942_2_En_13_Fig3_HTML.jpg

图 13-3

复合设计示例的树结构

现在假设学院的校长想提升一些员工。假设教学经验是晋升的唯一标准,但高级教师(分支节点)和初级教师(叶节点)之间的标准有所不同,具体如下:对于初级教师,晋升的最低标准是 12 年,高级教师是 15 年。

如果您理解示范 1,您会意识到晋升标准在将来可能会改变,并且可能会有来自上级的额外要求。因此,访问者模式非常适合满足当前的需求。这就是为什么在接下来的例子中,你会看到一个新的属性和一个新的方法被添加到了Employee接口;支持性的评论应该很容易理解。

// Newly added for this example
// To set years of Experience
double Experience { get; set; }
// Newly added for this example
void Accept(IVisitor visitor);

按照演示 1 中的设计,让我们用名为VisitEmployee(...)的方法制作一个名为IVisitor的访问者接口,它有两个重载版本。这是访问者的层次结构。

    /// <summary>
    /// Visitor interface
    /// </summary>
    interface IVisitor
    {
        // To visit leaf nodes
        void VisitEmployees(Employee employee);

        // To visit composite nodes
        void VisitEmployees(CompositeEmployee employee);
    }
    /// <summary>
    /// Concrete visitor class-PromotionCheckerVisitor
    /// </summary>
    class PromotionCheckerVisitor : IVisitor
    {
        string eligibleForPromotion = String.Empty;
        public void VisitEmployees(CompositeEmployee employee)
        {
            //We'll promote them if experience is greater than 15 years
            eligibleForPromotion = employee.Experience > 15 ? "Yes" : "No";
            Console.WriteLine($"\t{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");

        }

        public void VisitEmployees(Employee employee)
        {
            //We'll promote them if experience is greater than 12 years
            eligibleForPromotion = employee.Experience > 12 ? "Yes" : "No";
            Console.WriteLine($"\t{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");
        }
}

这一次,我在客户机代码中制作容器(一个列表数据结构,称为参与者)。当访问者从这个学院结构中收集必要的详细信息时,它可以显示符合晋升条件的候选人,这就是包含以下代码段的原因。

Console.WriteLine("\n***Visitor starts visiting our composite structure***\n");
IVisitor visitor = new PromotionCheckerVisitor();
//Visitor is traversing the participant list
foreach ( IEmployee  emp in participants)
   {
      emp.Accept(visitor);
   }

访问者从原始的学院结构中一次一个地收集数据,而不对其进行任何修改。一旦收集过程结束,访问者分析数据以显示预期的结果。为了直观地理解这一点,你可以跟随图 13-4 到 13-8 中的箭头。校长在组织的最高层,所以你可以假设他没有得到提升。

第一步

图 13-4 为步骤 1。

img/463942_2_En_13_Fig4_HTML.jpg

图 13-4

第一步

第二步

图 13-5 为步骤 2。

img/463942_2_En_13_Fig5_HTML.jpg

图 13-5

第二步

第三步

图 13-6 为步骤 3。

img/463942_2_En_13_Fig6_HTML.jpg

图 13-6

第三步

第四步

图 13-7 为步骤 4。

img/463942_2_En_13_Fig7_HTML.jpg

图 13-7

第四步

第五步

图 13-8 为步骤 5。

img/463942_2_En_13_Fig8_HTML.jpg

图 13-8

第五步

等等...

我在演示 1 中遵循了类似的设计,代码示例建立在第十一章中唯一的演示之上。为了简洁起见,我在这个例子中没有包括类图和解决方案浏览器视图。所以,直接通过下面的实现。

演示 2

下面是实现。

using System;
using System.Collections.Generic;

namespace VisitorWithCompositePattern
{
    interface IEmployee
    {
        //To set an employee name
        string Name { get; set; }
        //To set an employee department
        string Dept { get; set; }
        //To set an employee designation
        string Designation { get; set; }

        //To display an employee details
        void DisplayDetails();

        //Newly added for this example
        //To set years of Experience
        double Experience { get; set; }
        //Newly added for this example
        void Accept(IVisitor visitor);
    }
    //Leaf node
    class Employee : IEmployee
    {
        public string Name { get; set; }
        public string Dept { get; set; }
        public string Designation { get; set; }
        public double Experience { get; set; }
        //Details of a leaf node
        public void DisplayDetails()
        {
            Console.WriteLine($"{Name} works in { Dept} department.Designation:{Designation}.Experience : {Experience} years.");
        }
        public void Accept(IVisitor visitor)
        {
            visitor.VisitEmployees(this);
        }

    }
    //Non-leaf node
    class CompositeEmployee : IEmployee
    {
        public string Name { get; set; }
        public string Dept { get; set; }
        public string Designation { get; set; }
        public double Experience { get; set; }

        //The container for child objects
        //private List<IEmployee> subordinateList = new List<IEmployee>();
        //Making it public now
        public List<IEmployee> subordinateList = new List<IEmployee>();

        //To add an employee
        public void AddEmployee(IEmployee e)
        {
            subordinateList.Add(e);
        }

        //To remove an employee
        public void RemoveEmployee(IEmployee e)
        {
            subordinateList.Remove(e);
        }

        //Details of a composite node
        public void DisplayDetails()
        {
            Console.WriteLine($"\n{Name} works in {Dept} department.Designation:{Designation}.Experience : {Experience} years.");
            foreach (IEmployee e in subordinateList)
            {
                e.DisplayDetails();
            }
        }

        public void Accept(IVisitor visitor)
        {
            visitor.VisitEmployees(this);
        }
    }
    /// <summary>
    /// Visitor interface
    /// </summary>
    interface IVisitor
    {
        //To visit leaf nodes
        void VisitEmployees(Employee employee);

        //To visit composite nodes
        void VisitEmployees(CompositeEmployee employee);
    }
    /// <summary>
    /// Concrete visitor class-PromotionCheckerVisitor
    /// </summary>
    class PromotionCheckerVisitor : IVisitor
    {
        string eligibleForPromotion = String.Empty;
        public void VisitEmployees(CompositeEmployee employee)
        {
           /*
            We'll promote them if experience is greater than 15 years.
            */
            eligibleForPromotion = employee.Experience > 15 ? "Yes" : "No";
            Console.WriteLine($"{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");

        }

        public void VisitEmployees(Employee employee)
        {
           /*
            We'll promote them if experience is greater
            than 12 years.
            */
            eligibleForPromotion = employee.Experience > 12 ? "Yes" : "No";
            Console.WriteLine($"{ employee.Name } from {employee.Dept} is eligible for promotion? :{eligibleForPromotion}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Visitor Pattern with Composite Pattern Demo. ***");

            #region Mathematics department
            //2 lecturers work in Mathematics department
            Employee mathTeacher1 = new Employee { Name = "M.Joy", Dept = "Mathematic", Designation = "Lecturer" ,Experience=13.7};
            Employee mathTeacher2 = new Employee { Name = "M.Roony", Dept = "Mathematics", Designation = "Lecturer", Experience = 6.5 };

      //The college has a Head of Department in Mathematics
            CompositeEmployee hodMaths = new CompositeEmployee { Name = "Mrs.S.Das", Dept = "Maths", Designation = "HOD-Maths", Experience = 14 };

      //Lecturers of Mathematics directly reports to HOD-Maths
            hodMaths.AddEmployee(mathTeacher1);
            hodMaths.AddEmployee(mathTeacher2);
            #endregion

            #region Computer Science department
            //3 lecturers work in Computer Sc. department
            Employee cseTeacher1 = new Employee { Name = "C.Sam", Dept = "Computer Science", Designation = "Lecturer", Experience = 10.2 };
            Employee cseTeacher2 = new Employee { Name = "C.Jones", Dept = "Computer Science.", Designation = "Lecturer", Experience = 13.5 };
            Employee cseTeacher3 = new Employee { Name = "C.Marium", Dept = "Computer Science", Designation = "Lecturer", Experience = 7.3 };

    //The college has a Head of Department in Computer science
            CompositeEmployee hodCompSc = new CompositeEmployee { Name = "Mr. V.Sarcar", Dept = "Computer Sc.", Designation = "HOD-Computer Sc.", Experience = 16.5 };

    //Lecturers of Computer Sc. directly reports to HOD-CSE
            hodCompSc.AddEmployee(cseTeacher1);
            hodCompSc.AddEmployee(cseTeacher2);
            hodCompSc.AddEmployee(cseTeacher3);
            #endregion

            #region Top level management
            //The college also has a Principal
            CompositeEmployee principal = new CompositeEmployee { Name = "Dr.S.Som", Dept = "Planning-Supervising-Managing", Designation = "Principal", Experience = 21 };

           /*
            Head of Departments's of Maths and Computer Science directly reports to Principal.
            */
            principal.AddEmployee(hodMaths);
            principal.AddEmployee(hodCompSc);
            #endregion

           /*
           Printing the leaf-nodes and branches in the same way i.e. in each case, we are calling DisplayDetails() method.
           */
            Console.WriteLine("\nDetails of a college structure is as follows:");
            //Prints the complete structure
            principal.DisplayDetails();

            List<IEmployee> participants = new List<IEmployee>();

            //For employees who directly reports to Principal
            foreach (IEmployee e in principal.subordinateList)
            {
                participants.Add(e);
            }
            //For employees who directly reports to HOD-Maths
            foreach (IEmployee e in hodMaths.subordinateList)
            {
                participants.Add(e);
            }
           //For employees who directly reports to HOD-Comp.Sc
            foreach (IEmployee e in hodCompSc.subordinateList)
            {
                participants.Add(e);
            }
            Console.WriteLine("\n***Visitor starts visiting our composite structure***\n");
            IVisitor visitor = new PromotionCheckerVisitor();
           /*
           Principal is already holding the highest position.
           We are not checking whether he is eligible
           for promotion or not.
           */
            //principal.Accept(visitor);
            //Visitor is traversing the participant list
            foreach ( IEmployee  emp in participants)
            {
                emp.Accept(visitor);
            }

            //Wait for user
            Console.ReadKey();
        }
    }
}

输出

这是输出。有些部分以粗体显示,表明访问者能够成功完成其工作。

***Visitor Pattern with Composite Pattern Demo. ***

Details of a college structure is as follows:

Dr.S.Som works in Planning-Supervising-Managing department.Designation:Principal.Experience : 21 years.

Mrs.S.Das works in Maths department.Designation:HOD-Maths.Experience : 14 years.
M.Joy works in Mathematic department.Designation:Lecturer.Experience : 13.7 years.
M.Roony works in Mathematics department.Designation:Lecturer.Experience : 6.5 years.

Mr. V.Sarcar works in Computer Sc. department.Designation:HOD-Computer Sc..Experience : 16.5 years.
C.Sam works in Computer Science department.Designation:Lecturer.Experience : 10.2 years.
C.Jones works in Computer Science. department.Designation:Lecturer.Experience : 13.5 years.
C.Marium works in Computer Science department.Designation:Lecturer.Experience : 7.3 years.

***Visitor starts visiting our composite structure***

Mrs.S.Das from Maths is eligible for promotion? :No
Mr. V.Sarcar from Computer Sc. is eligible for promotion? :Yes
M.Joy from Mathematic is eligible for promotion? :Yes
M.Roony from Mathematics is eligible for promotion? :No
C.Sam from Computer Science is eligible for promotion? :No
C.Jones from Computer Science. is eligible for promotion? :Yes
C.Marium from Computer Science is eligible for promotion? :No

十四、观察者模式

本章涵盖了观察者模式。

GoF 定义

定义对象之间的一对多依赖关系,这样当一个对象改变状态时,它的所有依赖对象都会得到通知并自动更新。

概念

在这个模式中,有许多观察者(对象)在观察一个特定的主体(也是一个对象)。观察者希望在对象内部发生变化时得到通知。所以,他们注册了这个科目。当他们对该主题失去兴趣时,他们就从该主题中注销。有时这种模型被称为发布者-订阅者(发布-订阅)模型。整个想法可以总结如下:使用这个模式,一个对象(subject)可以同时向多个观察者(一组对象)发送通知。观察者可以决定如何响应通知,并且可以根据通知执行特定的操作。

您可以用下面的图表来可视化这些场景。

在步骤 1 中,三个观察者请求从一个对象那里得到通知(见图 14-1 )。

img/463942_2_En_14_Fig1_HTML.jpg

图 14-1

第一步

在步骤 2 中,主体可以同意请求;换句话说,连接建立(见图 14-2 )。

img/463942_2_En_14_Fig2_HTML.jpg

图 14-2

第二步

在步骤 3 中,主题向注册用户发送通知(参见图 14-3 )。

img/463942_2_En_14_Fig3_HTML.jpg

图 14-3

第三步

在步骤 4(可选)中,observer2 不希望获得进一步的通知并请求注销自己(或者主题由于某些特定原因不希望将 observer2 保留在其通知列表中,并且他注销了 observer2)。因此,受试者和观察者 2 之间的连接已经断开(见图 14-4 )。

img/463942_2_En_14_Fig4_HTML.jpg

图 14-4

第四步

在第 5 步中,从现在开始,只有观察器 1 和观察器 3 从对象那里得到通知(见图 14-5 )。

img/463942_2_En_14_Fig5_HTML.jpg

图 14-5

第五步

真实世界的例子

想想一个在社交媒体上有很多粉丝的名人。这些追随者中的每一个都想从他们最喜爱的名人那里获得所有最新的更新。所以,他们追随名人直到兴趣减退。当他们失去兴趣时,他们就不再追随那个名人了。把这些粉丝或追随者想象成观察者,把名人想象成主体。

计算机世界的例子

让我们考虑计算机科学中一个简单的基于 UI 的例子。此用户界面连接到某个数据库。用户可以通过 UI 执行查询,在搜索数据库后,返回结果。使用这种模式,您可以将 UI 与数据库隔离开来。如果数据库发生变化,应该通知 UI,以便它可以相应地更新它的显示。

为了简化这个场景,假设您是组织中负责维护数据库的人。每当数据库发生更改时,您都希望收到通知,以便在必要时采取措施。在这种情况下,您可以注意以下几点。

  • 您可以在任何 eventdriven 软件中看到这种模式的存在。像 C# 这样的现代语言有按照这种模式处理这些事件的内置支持。这些构造让你的生活更轻松。

  • 如果你熟悉。NET 框架,你看到在 C# 中,你有泛型System.IObservable<T>System.IObserver<T>接口,其中泛型类型参数提供通知。

履行

对于这个例子,我创建了四个观察者(Roy, Kevin, BoseJacklin)以及两个主题(Celebrity-1 and Celebrity-2))。一个 subject(在我们的例子中是Celebrity)维护一个所有注册用户的列表。当主题中的标志值发生变化时,观察者会收到通知。

最初,三个观察者(Roy、Kevin 和 Bose)注册自己以获得来自名人 1 的通知。所以,在最初阶段,他们都收到了通知。但后来,凯文对名人 1 失去了兴趣。当名人 1 号意识到这一点时,他将凯文从他的观察名单中移除。此时,只有 Roy 和 Bose 在接收通知(当标志值为 50 时)。但是凯文后来改变了主意,想要再次获得通知,所以名人 1 再次注册了他。这就是为什么当名人-1 将标志值设置为 100 时,三个观察者都收到了他的通知。

后来你看到了一个名人,名字叫名人-2。罗伊和杰克林登记在他的观察名单上。因此,当名人-2 将标志值设置为 500 时,罗伊和杰克林都收到了通知。

让我们看看代码。下面是IObserver接口,它有一个Update(...)方法。

    interface IObserver
    {
        void Update(ICelebrity subject);
    }

两个具体的类——ObserverType1ObserverType2——向您展示了您可以拥有不同类型的观察者。这些类如下实现了IObserver接口。

    // ObserverType1
    class ObserverType1 : IObserver
    {
        string nameOfObserver;
        public ObserverType1(String name)
        {
            this.nameOfObserver = name;
        }
        public void Update(ICelebrity celeb)
        {
            Console.WriteLine($"{nameOfObserver} has received an alert from {celeb.Name}.Updated value is: {celeb.Flag}");
        }
    }

    // ObserverType2
    class ObserverType2 : IObserver
    {
        string nameOfObserver;
        public ObserverType2(String name)
        {
            this.nameOfObserver = name;
        }
        public void Update(ICelebrity celeb)
        {
            Console.WriteLine($"{nameOfObserver} notified.Inside {celeb.Name}, the updated value is: {celeb.Flag}");
        }
    }

主题接口(ICelebrity)包含三个方法,分别叫做Register(...), Unregister(...)NotifyRegisteredUsers(),很容易理解。这些方法分别注册一个观察器、注销一个观察器和通知所有已注册的观察器。下面是ICelebrity界面。

    interface ICelebrity
    {
        // Name of Subject
        string Name { get; }
        int Flag { get; set; }
        // To register
        void Register(IObserver o);
        // To Unregister
        void Unregister(IObserver o);
        // To notify registered users
        void NotifyRegisteredUsers();
    }

Celebrity具体类实现了ICelebrity接口。重要的一点是,这个具体的类维护一个注册用户列表。您可以在这个类中看到下面一行代码。

List<IObserver> observerList = new List<IObserver>();

Note

在这种模式的一些例子中,您可能会看到一个细微的变化,其中使用了一个抽象类来代替接口(ICelebrity),并且列表(observerList)在抽象类中维护。两种变化都可以。您可以实现您喜欢的方法。

我在Celebrity类中使用了一个构造函数。构造函数如下。

        public Celebrity(string name)
        {
            this.name = name;
        }

我对不同的名人使用这个构造函数。因此,在客户端代码中,您会看到以下带有注释的行。

Console.WriteLine("Working with first celebrity now.");
ICelebrity celebrity = new Celebrity("Celebrity-1");
// some other code
// Creating another celebrity

ICelebrity celebrity2 =新名人("名人-2 ");

最后,我在Celebrity类中使用了一个表达式体属性。你可以在这段代码中看到。

        //public string Name
        //{
        //    get
        //    {
        //        return name;
        //    }
        //}
        // Or, simply use expression bodied
        // properties(C# v6.0 onwards)
        public string Name => name;

Note

如果您的 C# 版本早于 6.0,那么您可以使用注释代码块。同样的评论也适用于本书中类似的代码。

剩下的代码很容易理解。如果你想的话,跟随支持的评论。

类图

图 14-6 显示了类图。

img/463942_2_En_14_Fig6_HTML.jpg

图 14-6

类图

解决方案资源管理器视图

图 14-7 显示了程序的高层结构。

img/463942_2_En_14_Fig7_HTML.jpg

图 14-7

解决方案资源管理器视图

示范

这是完整的演示。

using System;
// We have used List<Observer> here
using System.Collections.Generic;
namespace ObserverPattern
{
    interface IObserver
    {
        void Update(ICelebrity subject);
    }
    class ObserverType1 : IObserver
    {
        string nameOfObserver;
        public ObserverType1(String name)
        {
            this.nameOfObserver = name;
        }
        public void Update(ICelebrity celeb)
        {
            Console.WriteLine($"{nameOfObserver} has received an alert from {celeb.Name}. Updated value is: {celeb.Flag}");
        }
    }
    class ObserverType2 : IObserver
    {
        string nameOfObserver;
        public ObserverType2(String name)
        {
            this.nameOfObserver = name;
        }
        public void Update(ICelebrity celeb)
        {
            Console.WriteLine($"{nameOfObserver} notified.Inside {celeb.Name}, the updated value is: {celeb.Flag}");
        }
    }

    interface ICelebrity
    {
        // Name of Subject
        string Name { get; }
        int Flag { get; set; }
        // To register
        void Register(IObserver o);
        // To Unregister
        void Unregister(IObserver o);
        // To notify registered users
        void NotifyRegisteredUsers();

    }
    class Celebrity : ICelebrity
    {
        List<IObserver> observerList = new List<IObserver>();
        private int flag;
        public int Flag
        {
            get
            {
                return flag;
            }
            set
            {
                flag = value;
                // Flag value changed. So notify observer(s).
                NotifyRegisteredUsers();
            }
        }
        private string name;
        public Celebrity(string name)
        {
            this.name = name;
        }
        //public string Name
        //{
        //    get
        //    {
        //        return name;
        //    }
        //}
        // Or, simply use expression bodied
        // properties(C#6.0 onwards)
        public string Name => name;

        // To register an observer.
        public void Register(IObserver anObserver)
        {
            observerList.Add(anObserver);
        }
        // To unregister an observer.
        public void Unregister(IObserver anObserver)
        {
            observerList.Remove(anObserver);
        }
        // Notify all registered observers.
        public void NotifyRegisteredUsers()
        {
            foreach (IObserver observer in observerList)
            {
                observer.Update(this);
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Observer Pattern Demonstration.***\n");
            // We have 4 observers - 2 of them are ObserverType1, 1 is of // ObserverType2
            IObserver myObserver1 = new ObserverType1("Roy");
            IObserver myObserver2 = new ObserverType1("Kevin");
            IObserver myObserver3 = new ObserverType2("Bose");
            IObserver myObserver4 = new ObserverType2("Jacklin");
            Console.WriteLine("Working with first celebrity now.");
            ICelebrity celebrity = new Celebrity("Celebrity-1");
            // Registering the observers - Roy, Kevin, Bose
            celebrity.Register(myObserver1);
            celebrity.Register(myObserver2);
            celebrity.Register(myObserver3);
            Console.WriteLine(" Celebrity-1 is setting Flag = 5.");
            celebrity.Flag = 5;
            /*
            Kevin doesn't want to get further notification.
            So, unregistering the observer(Kevin)).
            */
            Console.WriteLine("\nCelebrity-1 is removing Kevin from the observer list now.");
            celebrity.Unregister(myObserver2);
            // No notification is sent to Kevin this time. He has // unregistered.
            Console.WriteLine("\n Celebrity-1 is setting Flag = 50.");
            celebrity.Flag = 50;
            // Kevin is registering himself again
            celebrity.Register(myObserver2);
            Console.WriteLine("\n Celebrity-1 is setting Flag = 100.");
            celebrity.Flag = 100;

            Console.WriteLine("\n Working with another celebrity now.");
            // Creating another celebrity
            ICelebrity celebrity2 = new Celebrity("Celebrity-2");
            // Registering the observers-Roy and Jacklin
            celebrity2.Register(myObserver1);
            celebrity2.Register(myObserver4);
            Console.WriteLine("\n --Celebrity-2 is setting Flag value as 500.--");
            celebrity2.Flag = 500;

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Observer Pattern Demonstration.***

Working with first celebrity now.
 Celebrity-1 is setting Flag = 5.
Roy has received an alert from Celebrity-1\. Updated value is: 5
Kevin has received an alert from Celebrity-1\. Updated value is: 5
Bose notified.Inside Celebrity-1, the updated value is: 5

Celebrity-1 is removing Kevin from the observer list now.

 Celebrity-1 is setting Flag = 50.
Roy has received an alert from Celebrity-1\. Updated value is: 50
Bose notified.Inside Celebrity-1, the updated value is: 50

 Celebrity-1 is setting Flag = 100.
Roy has received an alert from Celebrity-1\. Updated value is: 100
Bose notified.Inside Celebrity-1, the updated value is: 100
Kevin has received an alert from Celebrity-1\. Updated value is: 100

 Working with another celebrity now.

 --Celebrity-2 is setting Flag value as 500.--
Roy has received an alert from Celebrity-2\. Updated value is: 500
Jacklin notified.Inside Celebrity-2, the updated value is: 500

问答环节

14.1 如果只有一个观察者,那么我不需要设置界面。这是正确的吗?

是的。但是如果你想遵循纯面向对象的编程准则,你可能总是更喜欢接口(或者抽象类)而不是具体的类。除了这一点之外,通常有多个观察者,您在契约之后实现它们。这就是你从这种设计中受益的地方。

14.2 你能有不同类型的观察者吗?

是的。在真实世界的场景中思考这个问题。当任何人对组织的数据库进行重要更改时,来自不同部门的多组人员可能希望了解该更改(例如您的老板和数据库的所有者,他们在不同的级别工作)并相应地采取行动。因此,您可能需要在应用中为不同类型的观察器提供支持。这就是为什么在这一章中,我向你展示了一个例子,涉及多名观察员和多名名人。

14.3 你能在运行时添加或删除观察者吗?

是的。请注意,在程序开始时,为了获得通知,Kevin 注册了自己。后来,他注销,然后重新注册。

在我看来,观察者模式和责任链模式有相似之处(见第 章第 22 )。这是正确的吗?

在观察者模式中,所有注册用户同时收到通知;但是在责任链模式中,链中的对象被一个接一个地通知,直到一个对象完全处理通知(或者,到达链的末端)。图 14-8 和图 14-9 总结了不同之处。

img/463942_2_En_14_Fig9_HTML.jpg

图 14-9

责任链模式

img/463942_2_En_14_Fig8_HTML.jpg

图 14-8

观察者模式

在图 14-9 中,我假设观察者 3 能够完全处理通知。所以,它是链条的末端节点。在这种情况下,您还需要记住,如果通知到达了链的末端,但是没有人正确地处理它,您可能需要采取特殊的操作。

14.5 该模型是否支持 一对多关系

是的,GoF 定义证实了这一点。由于一个主题可以向多个观察者发送通知,这种依赖关系描述了一对多的关系。

14.6 有现成的构造可用(例如, System.IObservable<T> )。你为什么不使用它们,而是自己写代码呢?

你不能改变现成的功能,但我相信当你尝试自己实现这个概念时,你会更好地理解现成的结构。

另一个需要注意的要点是,当你使用系统时。可观测的系统。iob server接口,需要熟悉泛型编程。不仅如此,如果仔细观察这些接口,您会看到以下内容。

public interface IObservable<out T>

public interface IObserver<in T>

这仅仅意味着你也需要熟悉 C# 中的协方差和逆变。起初,这些概念似乎很难。在我的书《高级 C# 入门》(Apress,2020)中,我用代码示例详细讨论了这些概念。

14.7 观察者模式的主要优势是什么?

以下是一些关键优势。

  • 主体(我们例子中的名人)和他们的注册用户(观察者)组成了一个松散耦合的系统。他们不需要明确地相互了解。

  • 在通知列表中添加或删除观察者时,不需要对主题进行更改。

  • 此外,您可以在运行时独立地添加或删除观察器。

14.8 观察者模式的主要挑战是什么?

当您实现(或使用)这个模式时,这里有一些关键的挑战。

  • 毫无疑问,在 C# 中处理事件时,内存泄漏是最大的问题(也称为失效监听器问题)。在这种情况下,自动垃圾收集器可能并不总是对您有所帮助。

  • 通知的顺序不可靠。

十五、策略模式

本章涵盖了策略模式。它也被称为策略模式。

GoF 定义

定义一系列算法,封装每一个算法,并使它们可以互换。策略让算法独立于使用它的客户端而变化。

概念

客户端可以在运行时从一组算法中动态选择一个算法。这种模式还提供了一种使用所选算法的简单方法。

你知道一个对象可以有状态和行为。其中一些行为可能会因类的对象而异。这种模式侧重于在特定时间与对象相关联的变化行为。

在我们的例子中,您会看到一个Vehicle类。您可以使用该类创建一个车辆对象。创建车辆对象后,可以向该对象添加和设置行为。在客户端代码中,您也可以用新行为替换当前行为。最有趣的是,您会看到,由于行为是可以改变的,定义行为的是而不是;它只是将任务委托给车辆引用的对象。整体实现可以让你概念更清晰。

真实世界的例子

在一场足球比赛中,如果 A 队在比赛快结束时以 1 比 0 领先 B 队,A 队不会进攻,而是防守以保持领先。与此同时,B 队全力以赴去扳平比分。

计算机世界的例子

假设您有一个备份内存插槽。如果您的主内存已满,但您需要存储更多数据,您可以使用备份内存插槽。如果您没有这个备份内存插槽,并且您试图将额外的数据存储到您的主内存中,这些数据将被丢弃(当主内存已满时)。在这些情况下,您可能会得到异常,或者您可能会遇到一些特殊的行为(基于程序的架构)。因此,在存储数据之前,运行时检查是必要的。然后你就可以继续了。

履行

在这个实现中,我只关注车辆行为的变化。在实现中,您可以看到,一旦创建了一个车辆对象,它就与一个InitialBehavior,相关联,这个InitialBehavior,简单地声明在这种状态下,车辆不能做任何特殊的事情。但是一旦你设置了一个FlyBehavior,车辆就能飞起来。当你设定FloatBehavior时,它可以浮动。所有变化的行为都在一个单独的层级中维护。

    /// <summary>
    /// Abstract Behavior
    /// </summary>
    public abstract class VehicleBehavior
    {
        public abstract void AboutMe(string vehicle);
    }
    /// <summary>
    /// Floating capability
    /// </summary>
    class FloatBehavior : VehicleBehavior
    {
        public override void AboutMe(string vehicle)
        {
            Console.WriteLine($"My {vehicle} can float now.");
        }
    }
    /// <summary>
    /// Flying capability
    /// </summary>
    class FlyBehavior : VehicleBehavior
    {
        public override void AboutMe(string vehicle)
        {
            Console.WriteLine($"My {vehicle} can fly now.");
        }
    }
    /// <summary>
    /// Initial behavior. Cannot do anything special.
    /// </summary>
    class InitialBehavior : VehicleBehavior
    {
        public override void AboutMe(string vehicle)
        {
            Console.WriteLine($"My {vehicle} is just born.It cannot do anything special.");
        }
    }

在许多例子中,你会看到一个叫做上下文类的术语。Vehicle是本演示中的上下文类。该类定义如下。

    /// <summary>
    /// Context class-Vehicle
    /// </summary>
    public class Vehicle
    {
        VehicleBehavior behavior;
        string vehicleType;
        public Vehicle(string vehicleType)
        {
            this.vehicleType = vehicleType;
            // Setting the initial behavior
            this.behavior = new InitialBehavior();
        }
        /*
         * It's your choice. You may prefer to use a setter
         * method instead of using a constructor.
         * You can call this method whenever we want
         * to change the "vehicle behavior" on the fly.
         */
        public void SetVehicleBehavior(VehicleBehavior behavior)
        {
            this.behavior = behavior;
        }
        /*
        This method will help us to delegate the behavior to
the object referenced by vehicle.You do not know about the object type, but you simply know that this object can tell something about it, i.e. "AboutMe()" method
        */
        public void DisplayAboutMe()
        {
            behavior.AboutMe(vehicleType);
        }
    }

您可以看到,在构造函数内部,我设置了初始行为,稍后可以使用SetVehicleBehavior(...)方法对其进行修改。DisplayAboutMe()将任务委托给特定的对象。

类图

图 15-1 显示了类图的重要部分。

img/463942_2_En_15_Fig1_HTML.jpg

图 15-1

类图

解决方案资源管理器视图

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

img/463942_2_En_15_Fig2_HTML.jpg

图 15-2

解决方案资源管理器视图

示范

下面是实现。

using System;

namespace StrategyPattern
{
    /// <summary>
    /// Abstract Behavior
    /// </summary>
    public abstract class VehicleBehavior
    {
        public abstract void AboutMe(string vehicle);
    }
    /// <summary>
    /// Floating capability
    /// </summary>
    class FloatBehavior : VehicleBehavior
    {
        public override void AboutMe(string vehicle)
        {
            Console.WriteLine($"My {vehicle} can float now.");
        }
    }
    /// <summary>
    /// Flying capability
    /// </summary>
    class FlyBehavior : VehicleBehavior
    {
        public override void AboutMe(string vehicle)
        {
            Console.WriteLine($"My {vehicle} can fly now.");
        }
    }
    /// <summary>
    /// Initial behavior.Cannot do anything special.
    /// </summary>
    class InitialBehavior : VehicleBehavior
    {
        public override void AboutMe(string vehicle)
        {
            Console.WriteLine($"My {vehicle} is just born.It cannot do anything special.");
        }
    }
    /// <summary>
    /// Context class-Vehicle
    /// </summary>
    public class Vehicle
    {
        VehicleBehavior behavior;
        string vehicleType;
        public Vehicle(string vehicleType)
        {
            this.vehicleType = vehicleType;
            //Setting the initial behavior
            this.behavior = new InitialBehavior();
        }
        /*
         * It's your choice. You may prefer to use a setter
         * method instead of using a constructor.
         * You can call this method whenever we want
         * to change the "vehicle behavior" on the fly.
         */
        public void SetVehicleBehavior(VehicleBehavior behavior)
        {
            this.behavior = behavior;
        }
        /*
        This method will help us to delegate the behavior to
the object referenced by vehicle.You do not know about the object type, but you simply know that this object can tell something about it, i.e. "AboutMe()" method
        */
        public void DisplayAboutMe()
        {
            behavior.AboutMe(vehicleType);
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Strategy Pattern Demo.***\n");
            Vehicle context = new Vehicle("Aeroplane");
            context.DisplayAboutMe();
            Console.WriteLine("Setting flying capability to vehicle.");
            context.SetVehicleBehavior(new FlyBehavior());
            context.DisplayAboutMe();

            Console.WriteLine("Changing the vehicle behavior again.");
            context.SetVehicleBehavior(new FloatBehavior());
            context.DisplayAboutMe();

            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Strategy Pattern Demo.***

My Aeroplane is just born.It cannot do anything special.
Setting flying capability to vehicle.
My Aeroplane can fly now.
Changing the vehicle behavior again.
My Aeroplane can float now.

问答环节

在我看来,你专注于改变行为让一切都变得复杂了。此外,我不明白为什么我需要上下文类。您可以简单地使用继承机制并继续。你能解决这些问题吗?

如果一个行为对于所有子类型都是通用的,那么使用继承是没问题的,例如,你可以创建一个抽象类,将通用行为放入其中,这样所有的子类都可以获得通用行为。但是,当行为可以在对象之间变化,并且使用继承来维护它们很困难时,策略的真正力量就显现出来了。

例如,假设你从不同的行为开始,你把它们放在一个抽象类中,如下所示。

    public abstract class Vehicle
    {
        public abstract void AboutMe();
        public abstract void FloatBehavior();
        public abstract void FlyBehavior();

        public virtual void DefaultJob()
        {
            Console.WriteLine("By default, I float.");
        }
    }

现在假设BoatAeroplane是从它继承的两个具体类。您知道一个Boat对象不应该飞行,所以在Boat类中,您可以简单地如下重写FlyBehavior

    public override void FlyBehavior()
    {
        throw new NotImplementedException();
    }

同样的,Aeroplane物体也不应该浮在水中(正常情况下)。所以,在Aeroplane类中,你可以如下重写FloatBehavior

    public override void FloatBehavior()
    {
     throw new NotImplementedException();
    }

现在考虑一下,像这样的对象有很多变化的行为。这种维护可能是开销。

除此之外,让我们考虑一种具有特殊功能的特殊车辆。如果你只是把这些特殊的特性放在抽象类中,所有其他的 vehicle 对象都会继承这些特性并需要实现它们。但这还没有结束。进一步,假设在Boat类上有一个约束,简单地说它不能有任何这样的特殊行为。现在您遇到了一个死锁情况。如果实现这个特殊的方法,就违反了约束。如果您不实现它,系统架构就会崩溃,因为语言构造要求您实现该行为。(或者,您需要用abstract关键字标记该类,但同时,请记住您不能从抽象类创建实例。)

为了克服这一点,我可以创建一个单独的继承层次结构,用一个接口来保存所有的专用特性,如果需要的话,我的类可以实现这个接口。但是,这可能部分解决了问题,因为接口可能包含多个方法,而您的类可能只需要实现其中的一个。最后,在任何一种情况下,整体维护都变得很困难。除此之外,特殊的行为可能会改变,在这种情况下,您需要跟踪实现这些行为的所有类。

在这种情况下,上下文类充当了救世主的角色。比如对于Boat类对象,客户端设置 fly 行为,或者对于Aeroplane类对象,客户端设置 float 行为;他仅仅知道特定车辆的预期行为。所以,如果你愿意,你可以防止客户错误地给车辆设置不正确的行为。

为了简化这一点,context 类为变化的行为保存一个引用变量,并将任务委托给适当的行为类。这就是为什么您会在我们的Vehicle上下文类中看到下面的片段。

    public class Vehicle
    {
        VehicleBehavior behavior;
        //Some other code
        /*
         * It's your choice. You may prefer to use a setter
         * method instead of using a constructor.
         * You can call this method whenever we want
         * to change the "vehicle behavior" on the fly.
         */
        public void SetVehicleBehavior(VehicleBehavior behavior)
        {
            this.behavior = behavior;
        }
       //Some other code
    }

对于这个例子,“has-a”关系比“is-a”关系更合适,这也是大多数设计模式鼓励复合而不是继承的主要原因之一。

15.2 使用策略设计模式的主要优势是什么?

以下是一些关键优势。

  • 这种设计模式使你的类独立于算法。在这里,一个类在运行时动态地将算法委托给策略对象(封装了算法)。因此,算法的选择在编译时不受限制。

  • 维护您的代码库更容易。

  • 它很容易扩展。

这方面可以参考问答 15.1 的回答。

15.3 与策略设计模式相关的主要挑战是什么?

缺点可以总结如下。

  • 添加上下文类会导致应用中存在更多的对象。

  • 应用的用户必须了解不同的策略;否则,输出可能会让他们大吃一惊。

十六、模板方法模式

本章涵盖了模板方法模式。

GoF 定义

在操作中定义算法的框架,将一些步骤推迟到子类。模板方法允许子类在不改变算法结构的情况下重新定义算法的某些步骤。

概念

使用这种模式,您可以从算法的最小或基本结构开始。然后你将一些责任委托给子类。因此,派生类可以在不改变算法流程的情况下重新定义算法的某些步骤。

简单地说,这种设计模式在实现多步算法但允许通过子类定制时非常有用。

真实世界的例子

当你点比萨饼时,餐馆的厨师可以使用基本的机制来准备比萨饼,但他可能允许你选择最终的材料。例如,顾客可以选择不同的配料,如培根、洋葱、额外的奶酪、蘑菇等。因此,就在送披萨之前,厨师可以包括这些选择。

计算机世界的例子

假设你被雇佣来设计一个在线工程学位课程。你知道,一般来说,课程的第一学期对所有课程都是一样的。对于随后的学期,你需要根据学生选择的课程在申请中添加新的论文或科目。

当您希望避免应用中的重复代码,但允许子类更改基类工作流的某些特定细节,以便为应用带来不同的行为时,模板方法模式是有意义的。(但是,您可能不希望完全覆盖基方法来对子类进行彻底的更改。这样,模式不同于简单的多态。)

履行

假设每个工科学生需要在最初几个学期通过数学考试并展示软技能(如沟通技能、人员管理技能等等)才能获得学位。后来,你根据他们选择的道路(计算机科学或电子)在他们的课程中添加特殊的论文。

为此,在抽象类BasicEngineering,中定义了一个模板方法DisplayCourseStructure(),如下所示。

    /// <summary>
    /// Basic skeleton of actions/steps
    /// </summary>
    public abstract class BasicEngineering
    {

        //The following method(step) will NOT vary
        private void Math()
        {
            Console.WriteLine("1.Mathematics");
        }
        //The following method(step) will NOT vary
        private  void SoftSkills()
        {
            Console.WriteLine("2.SoftSkills");
        }
        /*
        The following method will vary.It will be
        overridden by derived classes.
        */

        public abstract void SpecialPaper();

        //The "Template Method"
        public void DisplayCourseStructure()
        {
            //Common Papers:
            Math();
            SoftSkills();
            //Specialized Paper:
            SpecialPaper();
        }
    }

注意,BasicEngineering的子类不能改变DisplayCourseStructure()方法的流程,但是它们可以覆盖SpecialPaper()方法以包含特定于课程的细节,并使最终的课程列表彼此不同。

名为ComputerScienceElectronics的具体类是BasicEngineering,的子类,它们借此机会覆盖了SpecialPaper()方法。下面的代码段展示了来自ComputerScience类的这样一个例子。

//The concrete derived class-ComputerScience
public class ComputerScience : BasicEngineering
{
  public override void SpecialPaper()
  {
        Console.WriteLine("3.Object-Oriented Programming");
  }
}

类图

图 16-1 显示了类图的重要部分。

img/463942_2_En_16_Fig1_HTML.jpg

图 16-1

类图

解决方案资源管理器视图

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

img/463942_2_En_16_Fig2_HTML.jpg

图 16-2

解决方案资源管理器视图

演示 1

下面是实现。

using System;

namespace TemplateMethodPattern
{
    /// <summary>
    /// Basic skeleton of actions/steps
    /// </summary>
    public abstract class BasicEngineering
    {

        //The following method(step) will NOT vary
        private void Math()
        {
            Console.WriteLine("1.Mathematics");
        }
        //The following method(step) will NOT vary
        private  void SoftSkills()
        {
            Console.WriteLine("2.SoftSkills");
        }
        /*
        The following method will vary.It will be
        overridden by derived classes.
        */

        public abstract void SpecialPaper();

        //The "Template Method"
        public void DisplayCourseStructure()
        {
            //Common Papers:
            Math();
            SoftSkills();
            //Specialized Paper:
            SpecialPaper();
        }
    }

    //The concrete derived class-ComputerScience
    public class ComputerScience : BasicEngineering
    {
        public override void SpecialPaper()
        {
            Console.WriteLine("3.Object-Oriented Programming");
        }
    }

    //The concrete derived class-Electronics
    public class Electronics : BasicEngineering
    {
        public override void SpecialPaper()
        {
            Console.WriteLine("3.Digital Logic and Circuit Theory");
        }
    }

    //Client code
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine("***Template Method Pattern Demonstration-1.***\n");
            BasicEngineering bs = new ComputerScience();
            Console.WriteLine("Computer Science course includes the following subjects:");
            bs.DisplayCourseStructure();
            Console.WriteLine();
            bs = new Electronics();
            Console.WriteLine("Electronics course includes the following subjects:");
            bs.DisplayCourseStructure();
            Console.ReadLine();
        }
    }
}

输出

这是输出。

***Template Method Pattern Demonstration-1.***

Computer Science course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Object-Oriented Programming

Electronics course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Digital Logic and Circuit Theory

问答环节

在这种模式中,子类可以根据他们的需要简单地重新定义方法。这是正确的吗?

是的。

16.2 抽象类 BasicEngineering 中,只有一个方法是抽象的,其他两个方法都是具体的方法。这背后的原因是什么?

这是一个只有三个方法的简单例子,您希望子类只覆盖这里的SpecialPaper()方法。其他方法是两个课程共有的,它们不需要被子类覆盖。

16.3 假设你想在 BasicEngineering 类中添加更多的方法,但是当且仅当你的子类需要这些方法时,你才想使用它们;否则,你忽略它们。这种情况在一些博士项目中很常见,在这些项目中有些课程是必修的,但是如果一个学生有一定的资格,他可能不需要参加这些课程的讲座。可以用模板方法模式设计这种情况吗?

是的,你可以。基本上,你想使用一个钩子,这是一个可以帮助你控制算法流程的方法。

为了展示这种设计的一个例子,现在我在BasicEngineering中增加了一个叫做IncludeAdditionalPaper()的方法。我们假设,默认情况下,这门学科包含在课程列表中,但电子专业的学生可以选择退出这门课程。

修改后的BasicEngineering类现在看起来像下面这样(注意指出重要变化的粗线)。

    /// <summary>
    /// Basic skeleton of actions/steps
    /// </summary>
    public abstract class BasicEngineering
    {
        //The following method(step) will NOT vary
        private void Math()
        {
            Console.WriteLine("1.Mathematics");
        }
        //The following method(step) will NOT vary
        private void SoftSkills()
        {
            Console.WriteLine("2.SoftSkills");
        }
        /*
        The following method will vary.It will be
        overridden by derived classes.
        */

        public abstract void SpecialPaper();

        //The "Template Method"
        public void DisplayCourseStructure()
        {
            //Common Papers:
            Math();
            SoftSkills();
            //Specialized Paper:
            SpecialPaper();
            //Include an additional subject if required.
            if (IsAdditionalPaperNeeded())
            {
                IncludeAdditionalPaper();
            }
        }

        private void IncludeAdditionalPaper()
        {
            Console.WriteLine("4.Compiler Design.");
        }
        //A hook method.
        //By default,an additional subject is needed
        public virtual bool IsAdditionalPaperNeeded()
        {
            return true;
        }
    }

由于电子类不需要包含附加方法,因此定义如下:

    //The concrete derived class-Electronics
    public class Electronics : BasicEngineering
    {
        public override void SpecialPaper()
        {
            Console.WriteLine("3.Digital Logic and Circuit Theory");
        }
        //Using the hook method now.
        //Additional paper is not needed for Electronics.
        public override bool IsAdditionalPaperNeeded()
        {
            return false;
        }

    }

现在让我们来看一下程序和输出。

演示 2

下面是修改后的实现。关键变化以粗体显示。

using System;

namespace TemplateMethodPattern
{
    /// <summary>
    /// Basic skeleton of actions/steps
    /// </summary>
    public abstract class BasicEngineering
    {
        //The following method(step) will NOT vary
        private void Math()
        {
            Console.WriteLine("1.Mathematics");
        }
        //The following method(step) will NOT vary
        private void SoftSkills()
        {
            Console.WriteLine("2.SoftSkills");
        }
        /*
        The following method will vary.It will be
        overridden by derived classes.
        */

        public abstract void SpecialPaper();

        //The "Template Method"
        public void DisplayCourseStructure()
        {
            //Common Papers:
            Math();
            SoftSkills();
            //Specialized Paper:
            SpecialPaper();
            //Include an additional subject if required.
            if (IsAdditionalPaperNeeded())
            {
                IncludeAdditionalPaper();
            }
        }

        private void IncludeAdditionalPaper()
        {
            Console.WriteLine("4.Compiler Design.");
        }
        //A hook method.
        //By default,an additional subject is needed.
        public virtual bool IsAdditionalPaperNeeded()
        {
            return true;
        }
    }

    //The concrete derived class-ComputerScience
    public class ComputerScience : BasicEngineering
    {
        public override void SpecialPaper()
        {
            Console.WriteLine("3.Object-Oriented Programming");
        }
        //Not tested the hook method.
        //An additional subject is needed
    }

    //The concrete derived class-Electronics
    public class Electronics : BasicEngineering
    {
        public override void SpecialPaper()
        {
            Console.WriteLine("3.Digital Logic and Circuit Theory");
        }
        //Using the hook method now.
        //Additional paper is not needed for Electronics.
        public override bool IsAdditionalPaperNeeded()
        {
            return false;
        }

    }

    //Client code
    class Program
    {
        static void Main(string[] args)
        {

            Console.WriteLine("***Template Method Pattern Demonstration-2.***\n");
            BasicEngineering bs = new ComputerScience();
            Console.WriteLine("Computer Science course includes the following subjects:");
            bs.DisplayCourseStructure();
            Console.WriteLine();
            bs = new Electronics();
            Console.WriteLine("Electronics course includes the following subjects:");
            bs.DisplayCourseStructure();
            Console.ReadLine();
        }
    }
}

输出

这是修改后的输出。

***Template Method Pattern Demonstration-2.***

Computer Science course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Object-Oriented Programming
4.Compiler Design.

Electronics course includes the following subjects:
1.Mathematics
2.SoftSkills
3.Digital Logic and Circuit Theory

Note

你可能更喜欢另一种方法。例如,您可以在BasicEngineering中直接包含名为IncludeAdditionalPaper()的默认方法。之后,您可以覆盖Electronics类中的方法,并使方法体为空。但是这种方法与前面的方法相比并没有看起来更好。

看起来这个模式类似于 Builder 模式。这是正确的吗?

不。不要忘记核心意图;模板方法模式是一种行为设计模式,而构建器是一种创造设计模式。在构建者模式中,客户/顾客是老板。他们可以控制算法的顺序。在模板方法模式中,您(或开发人员)是老板。您将代码放在一个中心位置(例如,本例中的抽象类BasicEngineering.cs),并且您对执行流程拥有绝对的控制权,这是客户端无法更改的。例如,您可以看到数学和软技能总是出现在顶部,遵循模板方法DisplayCourseStructure()中的执行顺序。客户端需要遵守这个流程。

如果您改变了模板方法中的流程,其他参与者也将遵循新的流程。

使用模板方法设计模式的主要优势是什么?

以下是一些关键优势。

  • 你可以控制算法的流程。客户端无法更改它们。

  • 常见操作集中在一个位置。例如,在一个抽象类中,子类可以只重定义变化的部分,这样就可以避免多余的代码。

16.6 与模板方法设计模式相关的主要挑战是什么?

缺点可以总结如下。

  • 客户端代码不能指导步骤的顺序。如果您想要这种类型的功能,请使用构建器模式。

  • 子类可以覆盖父类中定义的方法

    (换句话说,在父类中隐藏原始定义),这可能违反 Liskov 替换原则,该原则基本上说,如果 S 是 T 的子类型,那么 T 类型的对象可以用 S 类型的对象替换。

  • 子类越多,意味着代码越分散,维护越困难。

16.7 如果一个子类试图覆盖基础工程中的其他父方法,会发生什么?

这种模式建议不要这样做。当您使用这种模式时,您不应该完全覆盖所有的父方法来给子类带来根本性的改变。这样,它不同于简单的多态。

16.8 这种模式与策略模式 有何不同?

你找到了一个好的切入点。是的,策略和模板方法模式有相似之处。在策略中,您可以使用委托来改变整个算法;然而,模板方法模式建议您使用继承来改变算法中的某些步骤,但是算法的整体流程是不变的。

十七、命令模式

本章介绍了命令模式。

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 类的构造函数中,你正在用一个关闭的状态初始化电视。所以,状态和上下文类都可以触发状态转换?

是的。

二十一、中介模式

本章涵盖了中介模式。

GoF 定义

定义一个封装一组对象如何交互的对象。Mediator 通过防止对象显式地相互引用来促进松散耦合,并允许您独立地改变它们的交互。

概念

中介是一组对象通过其进行通信的中介,但它们不能直接相互引用。中介负责控制和协调它们之间的交互。因此,您可以减少不同对象之间的直接互连数量。因此,使用这种模式,您可以减少应用中的耦合。

真实世界的例子

当飞机需要起飞时,会进行一系列的验证。这些类型的验证确认所有组件和单个零件(可能相互依赖)都处于完美状态。

另一个例子是当不同飞机的飞行员(他们正在接近或离开终端区域)与机场塔台通信时。他们不明确地与不同航空公司的其他飞行员交流。他们只是把他们的状态发送给控制塔。这些塔发送信号来确认谁可以起飞(或降落)。你必须注意,这些塔并不控制整个飞行。它们仅在端子区域实施约束。

计算机世界的例子

当客户端处理业务应用时,您可能需要实现一些约束。例如,假设您有一个表单,客户需要提供他们的用户 id 和密码来访问他们的帐户。在同一表单中,您可能需要提供其他必填字段,如电子邮件 ID、通信地址、年龄等。让我们假设您正在应用如下的约束。

首先,检查用户提供的用户 ID 是否有效。如果是有效的 id,则仅启用密码字段。提供这两个字段后,您可能需要检查用户是否提供了电子邮件 ID。让我们进一步假设,在提供了所有这些信息(有效的用户 id、密码、格式正确的电子邮件 ID 等等)之后,您的 Submit 按钮被启用。换句话说,如果用户提供了有效的用户 id、密码、有效的电子邮件 ID 和其他必需的详细信息,则 Submit 按钮被启用。您还可以确保用户 ID 是一个整数,因此如果用户错误地在该字段中提供了任何字符,提交按钮将保持禁用模式。在这种情况下,中介模式变得非常方便。

简而言之,当一个程序由许多类组成,并且逻辑分布在它们之间时,代码变得更难阅读和维护。在这些场景中,如果您想要对系统的行为进行新的更改,这可能会很困难,除非您使用中介模式。

履行

维基百科描述了 Mediator 模式,如图 21-1 (摘自 GoF)。

img/463942_2_En_21_Fig1_HTML.jpg

图 21-1

中介模式示例

参与者描述如下。

  • Mediator:定义了提供Colleague对象间通信的接口。

  • ConcreteMediator:它知道并维护Colleague对象的列表。它实现了Mediator接口,并协调了Colleague对象之间的通信。

  • Colleague:定义与其他同事沟通的接口。

  • ConcreteColleague(s):一个具体的同事必须实现Colleague接口。这些对象通过中介相互通信。

在演示 1 中,我用 AbstractFriendFriend 代替了ColleagueConcreteColleague(s) (是的,你可以假设是友好的环境。)在这个例子中,有三个参与者,分别是 Amit、Sohel 和 Joseph,他们通过聊天服务器相互通信。在这种情况下,聊天服务器扮演中介者的角色。

在下面的例子中,IMediator是接口,用易于理解的注释定义。

interface IMediator
{
    // To register a friend
    void Register(AbstractFriend friend);
    // To send a message from one friend to another friend
    void Send(AbstractFriend fromFriend, AbstractFriend toFriend,string msg);
    // To display currently registered objects/friends.
    void DisplayDetails();
}

ConcreteMediator类实现了这个接口,这个类维护注册参与者的列表。所以,在这个类中,你还会看到下面几行代码。

// List of friends
List<AbstractFriend> participants = new List<AbstractFriend>();

除此之外,中介只允许注册用户相互通信并成功发布消息。因此,ConcreteMediator class中的Send()方法检查发送者和接收者是否都是注册用户。该方法定义如下。

public void Send(AbstractFriend fromFriend, AbstractFriend toFriend,string msg)
{
    // Verifying whether the sender is a registered user or not.
    if (participants.Contains(fromFriend))
    {
            // Verifying whether the receiver is a registered user or not.
                if (participants.Contains(toFriend))
                {
                    Console.WriteLine($"\n[{fromFriend.Name}] posts: {msg}Last message posted {DateTime.Now}");
                    System.Threading.Thread.Sleep(1000);
                    // Target receiver will receive this message.
                    toFriend.ReceiveMessage(fromFriend, msg);
                }
                // Target receiver is NOT a registered user
                else
                {
                    Console.WriteLine($"\n{fromFriend.Name}, you cannot send message to {toFriend.Name} because he is NOT a registered user.");
                }
            }
            // Message sender is NOT a registered user
            else
            {
                Console.WriteLine($"\nAn outsider named {fromFriend.Name} of [{fromFriend.GetType()}] is trying to send a message to {toFriend.Name}.");
            }
        }

在这个例子中,有另一个继承层次,其中我使用了AbstractFriend作为一个抽象类,这样你就不能直接实例化它。相反,你可以从继承自AbstractFriend的具体类FriendStranger,中实例化对象。这个继承层次结构如下。

/// <summary>
/// AbstractFriend class
/// Making it an abstract class, so that you cannot instantiate it directly.
/// </summary>
    abstract class AbstractFriend
    {
        IMediator mediator;

        // Using auto property
        public string Name { get; set; }

        // Constructor
        public AbstractFriend(IMediator mediator)
        {
            this.mediator = mediator;
        }
        public void SendMessage(AbstractFriend toFriend,string msg)
        {
            mediator.Send(this,toFriend, msg);
        }
        public void ReceiveMessage(AbstractFriend fromFriend, string msg)
        {
            Console.WriteLine($"{this.Name} has received a message from {fromFriend.Name} saying: {msg} ");
        }
    }
    /// <summary>
    /// Friend class
    /// </summary>

    class Friend : AbstractFriend
    {
        // Constructor
        public Friend(IMediator mediator)
            : base(mediator)
        {

        }
    }
    /// <summary>
    /// Another class called Stranger
    /// </summary>
    class Stranger : AbstractFriend
    {
        // Constructor
        public Stranger(IMediator mediator)
            : base(mediator)
        {

        }
    }

Note

遵循基本中介模式的核心架构,我使用了两个不同的具体类来演示这样一个事实,即您应该而不是假设通信对象应该只来自同一个类。

在客户端代码中,您会看到以下参与者:两个来自Friend类,一个来自Stranger类。

// 3 persons-Amit,Sohel,Joseph
// Amit and Sohel from Friend class
Friend friend1 = new Friend(mediator);
friend1.Name = "Amit";
Friend friend2 = new Friend(mediator);
friend2.Name = "Sohel";
// Joseph is from Stranger class
Stranger stranger1 = new Stranger(mediator);
stranger1.Name = "Joseph";

这些人可以通过聊天服务器进行交流。因此,在传递消息之前,他们首先向聊天服务器注册,如下所示。

// Registering the participants
mediator.Register(friend1);
mediator.Register(friend2);
mediator.Register(stranger1);

在节目的最后,我介绍了两个人:托德和杰克。托德是一个Friend类对象,杰克是一个Stranger类对象。但是它们都没有向中介对象注册;所以中介不允许他们向期望的对象发送消息。

如果 Jack 在发送消息之前向中介注册,就可以正确地发送消息,如下所示。

mediator.Register(stranger1); // Disabled in Demonstration1
stranger1.SendMessage(friend3,"Hello friend...");

同样的评论也适用于Todd

类图

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

img/463942_2_En_21_Fig2_HTML.jpg

图 21-2

类图

解决方案资源管理器视图

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

img/463942_2_En_21_Fig3_HTML.jpg

图 21-3

解决方案资源管理器视图

演示 1

这是完整的演示。

using System;
using System.Collections.Generic;

namespace MediatorPattern
{
    interface IMediator
    {
        // To register a friend
        void Register(AbstractFriend friend);
        // To send a message from one friend to another friend
        void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg);
        // To display currently registered objects/friends.
        void DisplayDetails();
    }
    // ConcreteMediator
    class ConcreteMediator : IMediator
    {
        // List of friends
        List<AbstractFriend> participants = new List<AbstractFriend>();
        public void Register(AbstractFriend friend)
        {
            participants.Add(friend);
        }
        public void DisplayDetails()
        {
            Console.WriteLine("Current list of registered participants is as follows:");
            foreach (AbstractFriend friend in participants)
            {

                Console.WriteLine($"{friend.Name}");
            }
        }
        /*
         The mediator allows only registered users
         to communicate each other and post messages
         successfully. So, the following method
         checks whether both the sender and receiver
         are registered users or not.
         */
        public void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg)
        {
            // Verifying whether the sender is a registered user or not
            if (participants.Contains(fromFriend))

            {
                /* Verifying whether the receiver is a registered user or not */
                if (participants.Contains(toFriend))
                {
                    Console.WriteLine($"\n[{fromFriend.Name}] posts: {msg}Last message posted {DateTime.Now}");
                    System.Threading.Thread.Sleep(1000);
                    /* Target receiver will receive this message.*/
                    toFriend.ReceiveMessage(fromFriend, msg);
                }
                else
                {
                    Console.WriteLine($"\n{fromFriend.Name}, you cannot send message to {toFriend.Name} because he is NOT a registered user.");
                }
            }
            // Message sender is NOT a registered user.
            else
            {
                Console.WriteLine($"\nAn outsider named {fromFriend.Name} of [{fromFriend.GetType()}] is trying to send a message to {toFriend.Name}.");
            }
        }
    }
    /// <summary>
    /// AbstractFriend class
    /// Making it an abstract class, so that you cannot instantiate it directly.
    /// </summary>
    abstract class AbstractFriend
    {
        IMediator mediator;

        // Using auto property
        public string Name { get; set; }

        // Constructor
        public AbstractFriend(IMediator mediator)
        {
            this.mediator = mediator;
        }
        public void SendMessage(AbstractFriend toFriend, string msg)
        {
            mediator.Send(this, toFriend, msg);
        }
        public void ReceiveMessage(AbstractFriend fromFriend, string msg)
        {
            Console.WriteLine($"{this.Name} has received a message from {fromFriend.Name} saying: {msg} ");
        }
    }
    /// <summary>
    /// Friend class
    /// </summary>

    class Friend : AbstractFriend
    {
        // Constructor
        public Friend(IMediator mediator)
            : base(mediator)
        {

        }
    }
    /// <summary>
    /// Another class called Stranger
    /// </summary>
    class Stranger : AbstractFriend
    {
        // Constructor
        public Stranger(IMediator mediator)
            : base(mediator)
        {

        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Mediator Pattern Demonstration.***\n");

            IMediator mediator = new ConcreteMediator();
            //AbstractFriend afriend = new AbstractFriend(mediator);//error

            // 3 persons-Amit, Sohel, Joseph
            // Amit and Sohel from Friend class
            Friend friend1 = new Friend(mediator);
            friend1.Name = "Amit";
            Friend friend2 = new Friend(mediator);
            friend2.Name = "Sohel";
            // Joseph is from Stranger class
            Stranger stranger1 = new Stranger(mediator);
            stranger1.Name = "Joseph";

            // Registering the participants
            mediator.Register(friend1);
            mediator.Register(friend2);
            mediator.Register(stranger1);

            // Displaying the participant's list
            mediator.DisplayDetails();

            Console.WriteLine("Communication starts among participants...");
            friend1.SendMessage(friend2, "Hi Sohel, can we discuss the mediator pattern?");
            friend2.SendMessage(friend1, "Hi Amit, Yup, we can discuss now.");
            stranger1.SendMessage(friend1, " How are you?");

            // Another friend who does not register to the mediator
            Friend friend4 = new Friend(mediator);
            friend4.Name = "Todd";
            /*
            Todd is NOT a registered user.
            So,he cannot send this message to Joseph.
            */
            friend4.SendMessage(stranger1, "Hello Joseph...");
            /*
            Todd is NOT a registered user.
            So,he cannot receive this message from Amit.
            */
            friend1.SendMessage(friend4, "Hello Todd...");

            // An outsider person tries to participate
            Stranger stranger2 = new Stranger(mediator);
            stranger2.Name = "Jack";
            //mediator.Register(stranger1);
            // This message cannot reach Joseph, because Jack
            // is not the registered user.
            stranger2.SendMessage(stranger1, "Hello friend...");

            // Wait for user
            Console.Read();
        }
    }
}

输出

这是输出。

***Mediator Pattern Demonstration.***

Current list of registered participants is as follows:
Amit
Sohel
Joseph
Communication starts among participants...

[Amit] posts: Hi Sohel, can we discuss the mediator pattern?Last message posted 15-05-2020 11:13:08
Sohel has received a message from Amit saying: Hi Sohel, can we discuss the mediator pattern?

[Sohel] posts: Hi Amit, Yup, we can discuss now. Last message posted 15-05-2020 11:13:09
Amit has received a message from Sohel saying: Hi Amit, Yup, we can discuss now.

[Joseph] posts:  How are you? Last message posted 15-05-2020 11:13:10
Amit has received a message from Joseph saying:  How are you?

An outsider named Todd of [MediatorPattern.Friend] is trying to send a message to Joseph.

Amit, you cannot send message to Todd because he is NOT a registered user.

An outsider named Jack of [MediatorPattern.Stranger] is trying to send a message to Joseph.

分析

请注意,只有注册用户才能相互通信并成功发布消息。调解人不允许任何外人进入系统。(注意输出的最后几行)。

Point to Remember

你不应该假设总是应该有一对一的沟通。这是因为 GoF 声明中介用一对多交互代替了多对多交互。但在这一章中,我假设所有的消息都是私有的,不应该广播给所有人;因此,我举了一个例子,其中中介只将消息发送给预期的接收者。只有当外人试图在聊天服务器中发布消息时,中介才会广播消息以警告其他人。

问答环节

你为什么要把事情复杂化?在前面的例子中,每个参与者都可以彼此直接对话,而您可以绕过中介。这是正确的吗?

在这个例子中,您只有三个注册的参与者,中介只允许他们互相通信。因此,似乎只有三个参与者,他们可以直接相互交流。但是考虑一个更复杂的场景,让我们给这个应用添加另一个约束,它声明当且仅当目标参与者仅处于在线模式(这是聊天服务器的常见场景)时,参与者才可以向目标参与者发送消息。如果不使用中介者模式,仅仅检查参与者是否是有效用户是不够的;除此之外,您还需要在发布消息之前检查目标收件人的在线状态。而如果参与人数不断增长,你能想象系统的复杂程度吗?因此,中介可以将您从这种场景中解救出来,因为您可以将所有验证标准放在中介中。图 21-4 和 21-5 更好地描绘了这一场景。

img/463942_2_En_21_Fig5_HTML.jpg

图 21-5

案例 2:有调解人

img/463942_2_En_21_Fig4_HTML.jpg

图 21-4

情况 1:不使用调解器

修改的实现

在修改后的示例中,如果一个参与者和另一个参与者都是注册用户,并且接收者仅在线,则他们可以向另一个参与者发送消息。中介负责将消息发送到正确的目的地,但是在它发送消息之前,参与者的在线状态是已知的。

图 21-5 暗示在类似的场景中,中介可以检查所有对象的状态并维护发送消息的逻辑。所以,我们来修改一下程序。请注意,我为每个参与者添加了一个州。因此,您可以在AbstractFriend类中看到这个新的代码段。

// New property for Demonstration 2
public string Status { get; set; }

演示 2

下面是修改后的实现。

using System;
using System.Collections.Generic;

namespace MediatorPatternModifiedDemo
{
    interface IMediator
    {
        // To register a friend
        void Register(AbstractFriend friend);
        // To send a message from one friend to another friend
        void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg);
        // To display currently registered objects/friends.
        void DisplayDetails();
    }
    // ConcreteMediator
    class ConcreteMediator : IMediator
    {
        // List of friends
        List<AbstractFriend> participants = new List<AbstractFriend>();
        public void Register(AbstractFriend friend)
        {
            participants.Add(friend);
        }
        public void DisplayDetails()
        {
            Console.WriteLine("Current list of registered participants is as follows:");
            foreach (AbstractFriend friend in participants)
            {

                Console.WriteLine($"{friend.Name}");
            }
        }
        /*
         The mediator allows only registered users
         to communicate with each other and post messages
         successfully. So, the following method
         checks whether both the sender and receiver
         are registered users or not.
         */
        public void Send(AbstractFriend fromFriend, AbstractFriend toFriend, string msg)
        {
            // Verifying whether the sender is a registered user or not.
            if (participants.Contains(fromFriend))

            {
                /* Verifying whether the receiver is a registered user and he is online.*/
                if (participants.Contains(toFriend) && toFriend.Status=="On")
                {
                    Console.WriteLine($"\n[{fromFriend.Name}] posts: {msg}Last message posted {DateTime.Now}");
                    System.Threading.Thread.Sleep(1000);
                    //Target receiver will receive this message.
                    toFriend.ReceiveMessage(fromFriend, msg);
                }
                else
                {
                    Console.WriteLine($"\n{fromFriend.Name},at this moment, you cannot send message to {toFriend.Name} because he is either not a registered user or he is currently offline.");
                }
            }
            //Message sender is NOT a registered user.
            else
            {
                Console.WriteLine($"\nAn outsider named {fromFriend.Name} of [{fromFriend.GetType()}] is trying to send a message to {toFriend.Name}.");
            }
        }
    }
    /// <summary>
    /// AbstractFriend class
    /// Making it an abstract class, so that you cannot instantiate it /// directly.
    /// </summary>
    abstract class AbstractFriend
    {
        IMediator mediator;

        // Using auto property
        public string Name { get; set; }
        // New property for Demonstration 2
        public string Status { get; set; }

        // Constructor
        public AbstractFriend(IMediator mediator)
        {
            this.mediator = mediator;
        }
        public void SendMessage(AbstractFriend toFriend, string msg)
        {
            mediator.Send(this, toFriend, msg);
        }
        public void ReceiveMessage(AbstractFriend fromFriend, string msg)
        {
            Console.WriteLine($"{this.Name} has received a message from {fromFriend.Name} saying: {msg} ");
        }
    }
    /// <summary>
    /// Friend class
    /// </summary>
    class Friend : AbstractFriend
    {
  // Constructor
        public Friend(IMediator mediator)
            : base(mediator)
        {

        }
    }
    /// <summary>
    /// Another class called Stranger
    /// </summary>
    class Stranger : AbstractFriend
    {
        // Constructor
        public Stranger(IMediator mediator)
            : base(mediator)
        {

        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Mediator Pattern Modified Demonstration.***\n");

            IMediator mediator = new ConcreteMediator();
            //AbstractFriend afriend = new AbstractFriend(mediator);//error

            // 3 persons-Amit, Sohel, Joseph
            // Amit and Sohel from Friend class
            Friend friend1 = new Friend(mediator);
            friend1.Name = "Amit";
            friend1.Status = "On";
            Friend friend2 = new Friend(mediator);
            friend2.Name = "Sohel";
            friend2.Status = "On";
            // Joseph is from Stranger class
            Stranger stranger1 = new Stranger(mediator);
            stranger1.Name = "Joseph";
            stranger1.Status = "On";

            // Registering the participants
            mediator.Register(friend1);
            mediator.Register(friend2);
            mediator.Register(stranger1);

            // Displaying the participant's list
            mediator.DisplayDetails();

            Console.WriteLine("Communication starts among participants...");
            friend1.SendMessage(friend2, "Hi Sohel,can we discuss the mediator pattern?");
            friend2.SendMessage(friend1, "Hi Amit,Yup, we can discuss now.");
            stranger1.SendMessage(friend1, " How are you?");

            // Another friend who does not register to the mediator
            Friend friend4 = new Friend(mediator);
            friend4.Name = "Todd";
            // This message cannot reach Joseph, because Todd
            // is not the registered user.
            friend4.SendMessage(stranger1, "Hello Joseph...");

            // This message will NOT reach Todd because he
            // is not a registered user.
            friend1.SendMessage(friend4, "Hello Todd...");

            // An outsider tries to participate
            Stranger stranger2 = new Stranger(mediator);
            stranger2.Name = "Jack";
            //mediator.Register(stranger1);
            // This message cannot reach Joseph, because Jack
            // is not the registered user.
            stranger2.SendMessage(stranger1, "Hello friend...");

            Console.WriteLine("Sohel is going to offline now.");
            friend2.Status = "Off";
            /*
             Since Sohel is offline, he will NOT receive
             this message.
             */
            friend1.SendMessage(friend2, "Hi Sohel, I have a gift for you.");
            Console.WriteLine("Sohel is online again.");
            friend2.Status = "On";
            stranger1.SendMessage(friend2, "Hi Sohel, Amit was looking for you.");

            // Wait for user
            Console.Read();
        }
    }
}

输出

这是修改后的输出。

***Mediator Pattern Modified Demonstration.***

Current list of registered participants is as follows:
Amit
Sohel
Joseph
Communication starts among participants...

[Amit] posts: Hi Sohel,can we discuss the mediator pattern?Last message posted 15-05-2020 11:30:50
Sohel has received a message from Amit saying: Hi Sohel,can we discuss the mediator pattern?

[Sohel] posts: Hi Amit,Yup, we can discuss now.Last message posted 15-05-2020 11:30:51
Amit has received a message from Sohel saying: Hi Amit,Yup, we can discuss now.

[Joseph] posts:  How are you?Last message posted 15-05-2020 11:30:52
Amit has received a message from Joseph saying:  How are you?

An outsider named Todd of [MediatorPatternModifiedDemo.Friend] is trying to send a message to Joseph.

Amit,at this moment, you cannot send message to Todd because he is either not a registered user or he is currently offline.

An outsider named Jack of [MediatorPatternModifiedDemo.Stranger] is trying to send a message to Joseph.
Sohel is going to offline now.

Amit,at this moment, you cannot send message to Sohel because he is either not a registered user or he is currently offline.
Sohel is online again.

[Joseph] posts: Hi Sohel, Amit was looking for you.Last message posted 15-05-2020 11:30:53
Sohel has received a message from Joseph saying: Hi Sohel, Amit was looking for you.

Note

前面输出中的一些行被加粗,以展示修改后的程序的影响(演示 2)。

现在您可以看到,当且仅当一个参与者在线时,他才可以向另一个参与者发送消息。中介负责将消息发送到正确的目的地,在发送消息之前,它确保两个参与者都是注册用户。

21.2 使用中介模式有什么好处?

以下是一些优点。

  • 您可以降低系统中对象通信的复杂性。

  • 该模式促进了松散耦合。因此,对象可以重用。

  • 该模式减少了系统中子类的数量。

  • 您用一对多关系替换了多对多关系,因此代码更容易阅读和理解。这样做的一个明显效果是,维护变得更加容易。

  • 您可以使用这种模式提供集中控制。

  • 简而言之,从代码中去除紧密耦合总是一个好的目标,在这种情况下,中介模式得分很高。

21.3 使用中介模式的缺点是什么?

以下几点应对这些挑战。

  • 在某些情况下,实现适当的封装变得棘手,中介对象的架构变得复杂。

  • 有时维护一个复杂的中介会成为一个大问题。

21.4 如果您需要添加一个新的规则或逻辑,您可以直接将其添加到中介器中。这是正确的吗?

是的。

我在门面模式 和中介模式之间找到了一些相似之处。这是正确的吗?

是的。Steve Holzner 在他的书中提到了相似性,他将中介模式描述为一个复用的门面模式。在 Mediator 模式中,不是使用单个对象的接口,而是在多个对象之间创建一个多路复用的接口来实现平滑过渡。

在这个模式中,你减少了不同对象之间的相互联系。由于这一缩减,您获得了哪些主要好处?

对象之间更多的互连会创建一个难以改变的整体系统(因为行为分布在许多对象中)。另一个副作用是,您可能需要创建许多子类来将这些更改引入系统。

21.7 在这两个实现中,你都在使用 Thread.Sleep(1000) 。这是什么原因呢?

你可以忽略它。我用这个来模拟现实生活中的场景。我假设参与者在正确阅读消息后发布消息,此活动至少需要 1 秒钟。

二十二、责任链模式

本章涵盖了责任链模式。

GoF 定义

通过给多个对象一个处理请求的机会,避免将请求的发送方耦合到接收方。链接接收对象,并沿着链传递请求,直到有对象处理它。

概念

在这种模式中,您形成了一个对象链,在这个对象链中,您将任务的责任从一个对象传递到另一个对象,直到一个对象接受完成任务的责任。链中的每个对象都可以处理特定类型的请求。如果一个对象不能完全处理请求,它会将请求传递给链中的下一个对象。这个过程可以持续到链的末端。这种请求处理机制为您提供了在链中添加新处理对象(处理程序)的灵活性。图 22-1 显示了这样一个有 N 个处理器的链。

img/463942_2_En_22_Fig1_HTML.jpg

图 22-1

责任链模式

真实世界的例子

大多数软件组织都有一些客户服务代表,他们接受客户的反馈,并将任何问题转发给组织中适当的部门。但是,这两个部门不能同时解决这个问题。看似负责的部门会首先查看问题,如果这些员工认为问题应该转发给另一个部门,他们就会转发。

当病人去医院看病时,你可能会看到类似的场景。如果认为有必要,一个部门的医生可以将病人转到另一个部门(以作进一步诊断)。

你也可以考虑移动公司组织。例如,在印度,沃达丰移动公司有一个客户服务部。如果你有投诉,你首先向客户服务部提出问题。如果他们不能解决你的问题,你可以把它升级到一个节点官员。如果您对节点官员给出的解决方案不满意,您可以进一步将问题升级到上诉官员。

计算机世界的例子

考虑一个可以发送电子邮件和传真的软件应用(例如打印机)。因此,任何客户都可以报告传真问题或电子邮件问题,所以您需要两种不同类型的错误处理程序:EmailErrorHandlerFaxErrorHandler。你可以放心地假设EmailErrorHandler只处理电子邮件错误,它不对传真错误负责。同样,FaxErrorHandler处理传真错误,不关心电子邮件错误。

您可以像这样形成一个链:每当您的应用发现一个错误,它就抛出一张票并转发错误,希望其中一个处理程序会处理它。让我们假设请求首先到达FaxErrorhandler。如果这个处理程序同意这是一个传真问题,它就处理它;否则,它将问题转发给EmailErrorHandler

注意,这里的链以EmailErrorHandler结束。但是如果您需要处理另一种类型的问题,比如身份验证问题,由于安全漏洞,您可以创建一个AuthenticationErrorHandler并将其放在EmailErrorHandler之后。现在,如果一个EmailErrorHandler也不能完全解决问题,它会将问题转发给AuthenticationErrorHandler,这个链就此结束。

Points to Remember

这只是一个例子;您可以按照自己喜欢的任何顺序随意放置这些处理程序。底线是处理链可能会在以下两种情况下结束:

  • 处理程序可以完全处理请求。

  • 你已经到了链条的末端。

当您在 C# 应用中使用多个 catch 块实现异常处理机制时,您会看到类似的机制。如果 try 块中出现异常,第一个 catch 块会尝试处理它。如果它不能处理这种类型的异常,下一个 catch 块将尝试处理它,并遵循相同的机制,直到该异常被一些处理程序(catch 块)正确处理。如果应用中的最后一个 catch 块也无法处理它,则会在此链之外引发异常。

履行

让我们假设在下面的例子中,你为我刚刚讨论的计算机世界例子编写程序。在这个例子中,我假设我们需要处理来自电子邮件或传真的不同种类的消息。客户还可以将这些消息标记为普通优先级或高优先级。所以,在程序的开始,你会看到下面的代码段。

/// <summary>
/// Message priorities
/// </summary>
public enum MessagePriority
{
  Normal,
  High
}
/// <summary>
/// Message class
/// </summary>
public class Message
{
 public string Text { get; set; }
 public MessagePriority Priority;
 public Message(string msg, MessagePriority priority)
  {
    this.Text = msg;
    this.Priority = priority;
  }
}

这次我选择了一个抽象的Receiver类,因为我想在它的派生类之间共享一些公共功能。

Points to Note

或者,您可以选择一个接口并使用默认接口方法的概念,这在 C# 8 中是受支持的。因为遗留版本不支持这一点,所以我为这个例子选择了抽象类。

Receiver类如下所示。

abstract class Receiver
{
  protected Receiver nextReceiver;
  //To set the next handler in the chain.  public void NextReceiver(Receiver nextReceiver)
  {
   this.nextReceiver = nextReceiver;
  }
 public abstract void HandleMessage(Message message);
}

FaxErrorHandlerEmailErrorHandler类继承自Receiver,它们在这个程序中充当具体的处理程序。为了演示一个非常简单的用例,我可以在FaxErrorHandler .中使用下面的代码段

if (message.Text.Contains("fax"))
{
    Console.WriteLine($"FaxErrorHandler processed { message.Priority } priority issue: { message.Text }");
}
else if (nextReceiver != null)
{
    nextReceiver.HandleMessage(message);
}

Points to Remember

在前面的代码段中,您可以看到,如果一条消息包含单词 fax ,那么 FaxErrorHandler 会处理它;否则,它会将问题传递给下一个处理程序。同样,在接下来的例子中,如果一条消息包含单词 email ,那么 EmailErrorHandler 将处理这条消息,以此类推。所以,你可能会问,如果一条消息中同时包含了电子邮件传真,会发生什么?我在接下来的例子中处理了这个问题,但是为了简单起见,您可以忽略使用这段代码的情况。在现实世界的问题中,一个错误会导致另一个错误;因此,当传真代码库中出现错误时,相同的错误会传播到电子邮件代码库(如果它们共享一个公共代码库)。一个通用的补丁可以解决这两个问题。在接下来的例子中,我将向您展示何时应该传递问题,以及如何将问题传递给下一个处理者。因此,首先,您可能会忽略单个支柱的复杂性。

实际上,一个组织可能更喜欢实现一个基于人工智能的机制来首先分析一个问题,然后根据症状,他们可以将问题转发给一个特定的部门,但在核心部分,您可能会看到这种模式。

为了演示一条消息同时包含单词 email 和单词 fax 的情况,我对FaxErrorHandler ,使用了一个相对复杂的结构,如下所示(相关的注释可以作为您的指南)。

class FaxErrorHandler : Receiver
{
    bool messagePassedToNextHandler = false;
    public override void HandleMessage(Message message)
    {
        // Start processing if the error message contains "fax"
        if (message.Text.Contains("fax"))
        {
            Console.WriteLine("FaxErrorHandler processed {0} priority issue: {1}", message.Priority, message.Text);
                /*
                Do not leave now, if the error message contains 'email' too.
                */
                if (nextReceiver != null && message.Text.Contains("email"))
                {
                    Console.WriteLine("I've fixed fax side defect.Now email team needs to work on top of this fix.");
                    nextReceiver.HandleMessage(message);
                    // We'll not pass the message repeatedly to next handler
                    messagePassedToNextHandler = true;
                }
            }
            if (nextReceiver != null && messagePassedToNextHandler != true)
            {
                nextReceiver.HandleMessage(message);
            }
        }
}

EmailErrorHandler与此类似。现在,如果你有一条包含电子邮件传真的消息,像"Neither the fax nor email is working,"一样,这个相对复杂的结构可以帮助你得到下面的输出,你可以看到两个团队都在处理缺陷:

FaxErrorHandler processed High priority issue: Neither fax nor email are working.
I've fixed fax side defect. Now email team needs to work on top of this fix.
EmailErrorHandler processed High priority issue: Neither fax nor email are working.
Email side defect is fixed. Now fax team needs to cross verify this fix.

在我的链的末端,有一个UnknownErrorHandler声明这个问题既不是来自Email也不是来自Fax;所以你需要咨询专业的开发者来解决这个问题。

class UnknownErrorHandler : Receiver
    {
        public override void HandleMessage(Message message)
        {
            if (!(message.Text.Contains("fax")|| message.Text.Contains("email")))
            {
                Console.WriteLine("Unknown error occurs.Consult experts immediately.");
            }
            else if (nextReceiver != null)
            {
                nextReceiver.HandleMessage(message);
            }
        }
}

最后,错误处理程序对象的形成非常简单明了,如下所示。

// Different handlers
Receiver emailHandler = new EmailErrorHandler();
Receiver faxHandler = new FaxErrorHandler();
Receiver unknownHandler = new UnknownErrorHandler();

从下面的代码段,你可以很容易地理解如何形成一个处理程序链。

/*
Making the chain :
FaxErrorhandler->EmailErrorHandler->UnknownErrorHandler.
*/
faxHandler.NextReceiver(emailHandler);
emailHandler.NextReceiver(unknownHandler);

类图

图 22-2 为类图。

img/463942_2_En_22_Fig2_HTML.jpg

图 22-2

类图

解决方案资源管理器视图

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

img/463942_2_En_22_Fig3_HTML.jpg

图 22-3

解决方案资源管理器视图

示范

这是完整的程序。

using System;

namespace ChainOfResponsibilityPattern
{
    /// <summary>
    /// Message priorities
    /// </summary>
    public enum MessagePriority
    {
        Normal,
        High
    }
    /// <summary>
    /// Message class
    /// </summary>
    public class Message
    {
        public string Text { get; set; }
        public MessagePriority Priority;
        public Message(string msg, MessagePriority priority)
        {
            this.Text = msg;
            this.Priority = priority;
        }
    }
    /// <summary>
    /// Abstract class -Receiver
    /// The abstract class is chosen to share
    /// the common codes across derived classes.
    /// </summary>
    abstract class Receiver
    {
        protected Receiver nextReceiver;
        //To set the next handler in the chain.
        public void NextReceiver(Receiver nextReceiver)
        {
            this.nextReceiver = nextReceiver;
        }
        public abstract void HandleMessage(Message message);
    }
    /// <summary>
    /// FaxErrorHandler class
    /// </summary>
    class FaxErrorHandler : Receiver
    {
        bool messagePassedToNextHandler = false;
        public override void HandleMessage(Message message)
        {
            //Start processing if the error message contains "fax"
            if (message.Text.Contains("fax"))
            {
                Console.WriteLine($"FaxErrorHandler processed {message.Priority} priority issue: {message.Text}");
                //Do not leave now, if the error message contains email too.
                if (nextReceiver != null && message.Text.Contains("email"))
                {
                    Console.WriteLine("I've fixed fax side defect.Now email team needs to work on top of this fix.");
                    nextReceiver.HandleMessage(message);
                    //We'll not pass the message repeatedly to next handler.
                    messagePassedToNextHandler = true;
                }
            }
            if (nextReceiver != null && messagePassedToNextHandler != true)
            {
                nextReceiver.HandleMessage(message);
            }
        }
    }
    /// <summary>
    /// EmailErrorHandler class
    /// </summary>
    class EmailErrorHandler : Receiver
    {
        bool messagePassedToNextHandler = false;
        public override void HandleMessage(Message message)
        {
            //Start processing if the error message contains "email"
            if (message.Text.Contains("email"))
            {
                Console.WriteLine($"EmailErrorHandler processed {message.Priority} priority issue: {message.Text}");
                //Do not leave now, if the error message contains "fax" too.
                if (nextReceiver != null && message.Text.Contains("fax"))
                {
                    Console.WriteLine("Email side defect is fixed.Now fax team needs to cross verify this fix.");
                    //Keeping the following code here.
                    //It can be useful if you place this handler before fax //error handler
                     nextReceiver.HandleMessage(message);
                    //We'll not pass the message repeatedly to the next //handler.
                    messagePassedToNextHandler = true;
                }
            }
            if (nextReceiver != null && messagePassedToNextHandler != true)
            {
                nextReceiver.HandleMessage(message);
            }
        }
    }
    /// <summary>
    /// UnknownErrorHandler class
    /// </summary>
    class UnknownErrorHandler : Receiver
    {
        public override void HandleMessage(Message message)
        {
            if (!(message.Text.Contains("fax") || message.Text.Contains("email")))
            {
                Console.WriteLine("Unknown error occurs.Consult experts immediately.");
            }
            else if (nextReceiver != null)
            {
                nextReceiver.HandleMessage(message);
            }
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Chain of Responsibility Pattern Demo***\n");

            //Different handlers
            Receiver emailHandler = new EmailErrorHandler();
            Receiver faxHandler = new FaxErrorHandler();
            Receiver unknownHandler = new UnknownErrorHandler();
            /*
            Making the chain :
            FaxErrorhandler->EmailErrorHandler->UnknownErrorHandler.
            */
            faxHandler.NextReceiver(emailHandler);
            emailHandler.NextReceiver(unknownHandler);

            Message msg = new Message("The fax is reaching late to the destination.", MessagePriority.Normal);
            faxHandler.HandleMessage(msg);
            msg = new Message("The emails are not reaching to the destinations.", MessagePriority.High);
            faxHandler.HandleMessage(msg);
            msg = new Message("In email, CC field is disabled always.", MessagePriority.Normal);
            faxHandler.HandleMessage(msg);
            msg = new Message("The fax is not reaching to the destination.", MessagePriority.High);
            faxHandler.HandleMessage(msg);
            msg = new Message("Cannot login  into the system.", MessagePriority.High);
            faxHandler.HandleMessage(msg);
            msg = new Message("Neither fax nor email are working.", MessagePriority.High);
            faxHandler.HandleMessage(msg);
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Chain of Responsibility Pattern Demo***

FaxErrorHandler processed Normal priority issue: The fax is reaching late to the destination.
EmailErrorHandler processed High priority issue: The emails are not reaching to the destinations.
EmailErrorHandler processed Normal priority issue: In email, CC field is disabled always.
FaxErrorHandler processed High priority issue: The fax is not reaching to the destination.
Unknown error occurs.Consult experts immediately.
FaxErrorHandler processed High priority issue: Neither fax nor email are working.
I've fixed fax side defect.Now email team needs to work on top of this fix.
EmailErrorHandler processed High priority issue: Neither fax nor email are working.
Email side defect is fixed.Now fax team needs to cross verify this fix.

问答环节

22.1 在上例中,为什么需要消息优先级?

接得好。实际上,您可以忽略消息优先级,因为为了简单起见,您只是在处理程序中搜索文本电子邮件传真。我添加这些优先级是为了美化代码。不要为电子邮件传真使用单独的处理程序,你可以创建一个不同类型的链来处理基于优先级的消息。但是在我们的演示中,我没有形成基于优先级的链,因为我假设从事传真支柱工作的开发人员不太了解电子邮件支柱,反之亦然。

22.2 使用责任链设计模式有什么好处?

一些显著的优点如下。

  • 您有多个对象来处理一个请求。(如果一个处理程序不能处理整个请求,它可以将责任转发给链中的下一个处理程序。)

  • 链的节点可以动态添加或删除。此外,你可以打乱他们的顺序。例如,在前面的应用中,如果您看到大多数缺陷来自电子邮件,那么您可能会将EmailErrorHandler放置为第一个处理程序,以节省应用的平均处理时间。

  • 处理程序不需要知道链中的下一个处理程序如何处理请求。它可以专注于它的处理机制。

  • 在这个模式中,您将(请求的)发送者与接收者分离。

22.3 使用责任链设计模式有哪些挑战?

以下几点描述了一些挑战。

  • 不能保证请求得到处理,因为您可能到达了链的末端,但是没有找到任何显式的接收者来处理请求。

  • 对于这种设计,调试变得很棘手。

22.4 如果到达了链的末端,但没有处理程序处理请求,你如何处理这种情况?

一个简单的解决方案是通过 try/catch(或 try/finally 或 try/catch/finally)块。您可以将所有的处理程序放在try块中,如果没有一个处理请求,您可以使用适当的消息引发一个异常,并在catch块中捕获该异常以引起您的注意(或者以某种不同的方式处理它)。

GoF 在类似的背景下谈到了 Smalltalk 的自动转发机制(doesNotUnderstand)。如果一个消息找不到合适的处理程序,它就会在doesNotUnderstand实现中被捕获,该实现可以被覆盖以在对象的后继中转发消息,将其记录在一个文件中,并将其存储在一个队列中供以后处理,或者您可以简单地执行任何其他操作。但是您必须注意,默认情况下,该方法会引发一个需要正确处理的异常。

我可以说一个处理程序要么完全处理这个请求,要么把它传递给下一个处理程序。这是正确的吗?

是的。

在我看来,观察者模式 和责任链模式有相似之处。这是正确的吗?

在观察者模式中,所有注册用户并行获得通知,但是在责任链模式中,责任链中的对象依次被逐个通知,并且这个过程一直持续到一个对象完全处理通知(或者您到达责任链的末端)。比较结果在观察者模式的“问答环节”部分用图表显示(参见第十四章中的问答 14.4)。

二十三、解释器模式

本章涵盖了解释器模式。

GoF 定义

给定一种语言,为它的语法定义一个表示,以及一个使用该表示来解释该语言中的句子的解释器。

概念

这种模式扮演着翻译者的角色,它经常被用来评估一种语言中的句子。所以,你首先需要定义一个语法来表示这种语言。然后解释器处理语法。当语法简单时,这种模式是最好的。

Points to Note

为了更好地理解这种模式,熟悉自动机中的单词(或句子)、语法、语言等等是很有帮助的,这是一个很大的话题。对它的详细讨论超出了本书的范围。现在,你知道在正式语言中,字母表可能包含无限数量的元素,一个单词可以是有限的字母序列(简单地说是字符串),由语法生成的所有字符串的集合称为语言生成的语法(G)。通常,语法由元组(V,T,S,P)表示,其中 V 是一组非终结符,T 是一组终结符,S 是开始符,P 是产生式规则。例如,如果你有一个语法 G = (V,T,S,P)其中

V={S},
T={a,b},
P={S->aSbS,S->bSaS,S->ε },
S={S};

ε表示空字符串。该语法可以生成相同数量的 a 和 b,如 ab、ba、abab、baab 等等。例如,以下步骤显示了获取 abba 的推导过程。

S
aSbS [since S->aSbS]
abS [since S->ε]
abbSaS [since S->bSaS]
abbaS [since S->ε]
abba [sinceS->ε]

同样的方法,可以生成 baab 。下面是推导步骤,作为快速参考。

S
bSaS [since S->bSaS]
baS [sinceS->ε]
baaSbS [since S->aSbS]
baabS [sinceS->ε]
baab [sinceS->ε]

这个模式中的每个类可能代表语言中的一个规则,它应该有一个解释表达式的方法。因此,为了处理更多的规则,您需要创建更多的类,这就是为什么解释器模式很少用于处理非常复杂的语法。

让我们考虑计算器程序中不同的算术表达式。虽然这些表达式是不同的,但它们都是使用一些基本规则构造的,并且这些规则是在语言的语法中定义的(这些算术表达式)。因此,如果您能够解释这些规则的一般组合,而不是将每个不同的规则组合视为单独的情况,这将是一个更好的想法。在这样的场景中可以使用解释器模式,当您看到演示 2 的细节时,就会明白这一点。但在此之前,我们先来看看演示 1 中一个相对简单的例子。

这种模式的典型结构通常用类似于图 23-1 的图表来描述。

img/463942_2_En_23_Fig1_HTML.jpg

图 23-1

典型解释器模式的结构

术语描述如下。

  • 抽象表达式通常是一个带有解释器方法的接口。您需要向该方法传递一个上下文对象。

  • 终端表达式用于终端表达式。终结表达式是不需要其他表达式来解释的表达式。它们是数据结构中的叶节点(即,它们没有子节点)。

  • 非终结符用于非终结符表达式。它也称为交替表达式、重复表达式和顺序表达式。这就像可以包含终结和非终结表达式的组合。当您对此调用Interpret()方法时,您会对它的所有子对象调用Interpret()。在演示 2 中,您将看到它们的运行。

  • 上下文保存解释器需要的全局信息。

  • 客户端调用Interpret()方法。可选地,它可以基于语言的规则构建语法树。

Points to Remember

  • 解释器用简单的语法规则处理语言。理想情况下,开发人员不想创建他们自己的语言,这就是他们很少使用这种模式的原因。

  • 本章中有两个演示,它们互不相关。第一个相对简单,但是第二个比较复杂,涉及更多的代码。

  • 在第一个演示中,您将一个三位数的数字转换成它的对等单词形式。这个程序是从这本书的前一版微调而来的。

  • 第二个程序使用解释器模式作为规则验证器,并解释细节。我的书 Java 设计模式 (Apress,2018)用多个例子讨论了同一个概念。

真实世界的例子

现实世界的例子包括翻译外语的翻译。音乐家扮演着音符解释者的角色,也就是“语法”

计算机世界的例子

Java 编译器将 Java 源代码解释成 Java 虚拟机能够理解的字节码。在 C# 中,源代码被转换为由公共语言运行库(CLR)解释的 MSIL 中间代码。在执行时,这个 MSIL 被实时(JIT)编译器转换成本机代码(二进制可执行代码)。

履行

一般来说,你用一个类来表示这些语法规则。让我们定义一个简单的规则,如下所示。

  • E::= E1E2E3

  • E1:=零百(s) |一百(s) |两百(s) |…|九百(s)

  • E2:=零十(s) |一个十(s) | "两个十(s) | …|九十

  • E3:=和零|和一|和二|和三|…|和九

为了简单和更好的可读性,我用四个类来表示这个语法:InputExpression表示 E(一个抽象类)HundredExpression表示 E1TensExpression表示 E2UnitExpression表示 E 3 。所以,在接下来的节目(演示 1)中,789被解释为Seven hundred(s) Eight ten(s) and Nine.

在演示 1 中,Context类非常容易理解。它有一个公共构造函数,接受一个名为input,的字符串参数,这个参数稍后会以 word 形式解释。该类还包含一个只读属性Input和一个名为Output的读写属性,定义如下。

    public class Context
    {
        private string input;
        public string Input {
            get
            {
                return input;
            }
        }
        public string Output { get; set; }

        // The constructor
        public Context(string input)
        {
            this.input = input;
        }

    }

抽象类InputExpression拥有抽象方法Interpret(...),它被它的具体子类HundredExpressionTensExpressionUnitExpression覆盖。这个类还包含一个具体的方法GetWord(string str),它在所有具体的子类中使用。我将这个方法放在这个抽象类中,这样我就可以简单地避免在具体的子类中重复这些代码。这个类如下。

    // The abstract class-will hold the common code.
    abstract class InputExpression
    {
        public abstract void Interpret(Context context);
        public string GetWord(string str)
        {
            switch (str)
            {
                case "1":
                    return "One";
                case "2":
                    return "Two";
                case "3":
                    return "Three";
                case "4":
                    return "Four";
                case "5":
                    return "Five";
                case "6":
                    return "Six";
                case "7":
                    return "Seven";
                case "8":
                    return "Eight";
                case "9":
                    return "Nine";
                case "0":
                    return "Zero";
                default:
                    return "*";
            }
        }
    }

在具体的子类中,您可以看到内置的Substring方法从输入中选择想要的数字。下面一行显示了这一点。

string hundreds = context.Input.Substring(0, 1);

最后,在客户端代码中,在给定的上下文中解释input之前,我使用了一个名为EvaluateInputWithContext的独立方法来构建解析树。所以,你会看到下面几行。

// Building the parse tree
List<InputExpression> expTree = new List<InputExpression>();
expTree.Add(new HundredExpression());
expTree.Add(new TensExpression());
expTree.Add(new UnitExpression());
// Interpret the input
foreach (InputExpression inputExp in expTree)
{
    inputExp.Interpret(context);
}
// some other code..

剩下的代码很容易理解,所以让我们继续。

类图

图 23-2 为类图。

img/463942_2_En_23_Fig2_HTML.jpg

图 23-2

类图

解决方案资源管理器视图

图 23-3 显示了程序各部分的高层结构。

img/463942_2_En_23_Fig3_HTML.jpg

图 23-3

解决方案资源管理器视图

演示 1

这是完整的演示。

using System;
using System.Collections.Generic;

namespace InterpreterPattern
{
    public class Context
    {
        private string input;
        public string Input {
            get
            {
                return input;
            }
        }
        public string Output { get; set; }

        // The constructor
        public Context(string input)
        {
            this.input = input;
        }

    }
    // The abstract class. It will hold the common code
    abstract class InputExpression
    {
        public abstract void Interpret(Context context);
        public string GetWord(string str)
        {

            switch (str)
            {
                case "1":
                    return "One";
                case "2":
                    return "Two";
                case "3":
                    return "Three";
                case "4":
                    return "Four";
                case "5":
                    return "Five";
                case "6":
                    return "Six";
                case "7":
                    return "Seven";
                case "8":
                    return "Eight";
                case "9":
                    return "Nine";
                case "0":
                    return "Zero";
                default:
                    return "*";
            }
        }
    }

    class HundredExpression : InputExpression
    {
        public override void Interpret(Context context)
        {
         string hundreds = context.Input.Substring(0,1);
         context.Output += GetWord(hundreds) + " hundred(s) ";
        }
    }
    class TensExpression : InputExpression
    {
        public override void Interpret(Context context)
        {
            string tens = context.Input.Substring(1,1);
            context.Output += GetWord(tens) + " ten(s) ";
        }
    }
    class UnitExpression : InputExpression
    {
        public override void Interpret(Context context)
        {
            string units = context.Input.Substring(2, 1);
            context.Output += "and "+GetWord(units);
        }
    }

    // Client Class
    class Client
    {
        public static void Main(String[] args)
        {
            Console.WriteLine("***Interpreter Pattern Demonstation-1.***\n");
            Console.WriteLine(" It will validate first three digit of a valid number.");
            string inputString="789";
            EvaluateInputWithContext(inputString);
            inputString = "456";
            EvaluateInputWithContext(inputString);
            inputString = "123";
            EvaluateInputWithContext(inputString);
            inputString = "075";
            EvaluateInputWithContext(inputString);
            inputString = "Ku79";//invalid input
            EvaluateInputWithContext(inputString);

            Console.ReadLine();
        }
        public static void EvaluateInputWithContext(string inputString)
        {
            Context context = new Context(inputString);
            //Building the parse tree
            List<InputExpression> expTree = new List<InputExpression>();
            expTree.Add(new HundredExpression());
            expTree.Add(new TensExpression());
            expTree.Add(new UnitExpression());
            // Interpret the input
            foreach (InputExpression inputExp in expTree)
            {
                inputExp.Interpret(context);
            }
            if (!context.Output.Contains("*"))
                Console.WriteLine($" {context.Input} is interpreted as {context.Output}");
            else
            {
                Console.WriteLine($" {context.Input} is not a valid input.");
            }
        }
    }
}

输出

这是输出。

***Interpreter Pattern Demonstation-1.***

It will validate first three digit of a valid number.
789 is interpreted as Seven hundred(s) Eight ten(s) and Nine
456 is interpreted as Four hundred(s) Five ten(s) and Six
123 is interpreted as One hundred(s) Two ten(s) and Three
075 is interpreted as Zero hundred(s) Seven ten(s) and Five
Ku79 is not a valid input.

另一个实现

让我们看看这种模式的另一种用法。当您考虑实现该模式时,有一些重要的步骤(在本例中遵循这些步骤)。这些如下。

  • 第一步定义你想为之构建解释器的语言的规则。

  • 第二步定义一个抽象类或者接口来表示一个表达式。它应该包含一个解释表达式的方法。

    • 步 2A 识别终结符和非终结符表达式。例如,在接下来的例子中,IndividualEmployee类是一个终端表达式类。

    • 步 2B 创建非终结符表达式类。他们每个人都在他们的子节点上调用解释方法。例如,在接下来的例子中,OrExpressionAndExpression类是非终结表达式类。

  • 步骤 3 使用这些类构建抽象语法树。您可以在客户端代码中完成这项工作,或者您可以创建一个单独的类来完成任务

  • 客户现在使用这个树来解释一个句子。

  • 步骤 5 将上下文传递给解释器。它通常有需要解释的句子。解释器也可以使用这个上下文执行一些额外的任务。

Points to Note

在接下来的程序中,我使用解释器模式作为规则验证器。

在这里,我用不同的员工的“经验年数”和当前的等级来举例说明。为了简单起见,有四个不同级别的员工:G1、G2、G3 和 G4。所以,你会看到下面几行。

   Employee emp1 = new IndividualEmployee(5, "G1");
   Employee emp2 = new IndividualEmployee(10, "G2");
   Employee emp3 = new IndividualEmployee(15, "G3");
   Employee emp4 = new IndividualEmployee(20, "G4");

我想在上下文中验证一个规则,它告诉你要被提升,一个员工应该至少有 10 年的经验,并且他应该来自 G2 级或 G3 级。一旦这些表达式被解释,你会看到布尔值的输出。您可以在Main()方法中看到下面几行代码。

// Minimum Criteria for promoton is:
// The year of experience is minimum 10 yrs. and
// Employee grade should be either G2 or G3
List<string> allowedGrades = new List<string> { "G2", "G3" };
Context context = new Context(10, allowedGrades);

可以看到,允许的成绩存储在一个列表中,并传递给了Context类构造函数。因此,Context类中的以下代码片段对您来说可能有意义。

private int experienceReqdForPromotion;
private List<string> allowedGrades;
public Context(int experience, List<string> allowedGrades)
{
    this.experienceReqdForPromotion = experience;
    this.allowedGrades = new List<string>();
    foreach (string grade in allowedGrades)
    {
        this.allowedGrades.Add(grade);
    }
}

Employee是与Interpret(...)方法的接口,如下所示。

interface Employee
{
    bool Interpret(Context context);
}

正如我之前告诉你的,在这个例子中,IndividualEmployee类充当叶节点。这个类如下实现了Employee接口方法。

public bool Interpret(Context context)
{
    if (this.yearOfExperience >= context.GetYearofExperience()
    && context.GetPermissibleGrades().Contains(this.currentGrade))
    {
        return true;
    }
    return false;
}

现在让我们来处理这个例子中一些复杂的规则或表达式。在客户端代码中,您可以看到第一个复杂的规则,如下所示。

Console.WriteLine("Is emp1 and any of emp2, emp3, emp4 is eligible for promotion?" + builder.BuildTreeBasedOnRule1(emp1, emp2, emp3, emp4).Interpret(context));
Console.WriteLine("Is emp2 and any of emp1, emp3, emp4 is eligible for promotion?"+ builder.BuildTreeBasedOnRule1(emp2, emp1, emp3, emp4).Interpret(context));
// and so on..

第二个复杂规则的形式如下。

Console.WriteLine("Is emp1 or (emp2 but not emp3) is eligible for promotion?"+ builder.BuildTreeBasedOnRule2(emp1, emp2, emp3).Interpret(context));
Console.WriteLine("Is emp2 or (emp3 but not emp4) is eligible for promotion?"+ builder.BuildTreeBasedOnRule2(emp2, emp3, emp4).Interpret(context));

所以,你可能会问这些规则是如何运作的?答案如下:另一个类EmployeeBuilder,有评估这些规则的方法。您将很快看到详细的实现,但是现在,让我们看一下形成第一个规则的一步一步的过程,如下所示,带有支持注释。

// Building the tree
//Complex Rule-1: emp1 and (emp2 or (emp3 or emp4))
public Employee BuildTreeBasedOnRule1(Employee emp1, Employee emp2, Employee emp3, Employee emp4)
{
    // emp3 or emp4
   Employee firstPhase = new OrExpression(emp3, emp4);
   // emp2 or (emp3 or emp4)
   Employee secondPhase = new OrExpression(emp2, firstPhase);
   // emp1 and (emp2 or (emp3 or emp4))
   Employee finalPhase = new AndExpression(emp1, secondPhase);
   return finalPhase;
}

AndExpressionOrExpression,NotExpression是实现接口Employee,的三个具体类,因此它们都有自己的Interpret(...)方法。例如,AndExpression实现Interpret(...)方法如下。

public bool Interpret(Context context)
{
    return emp1.Interpret(context) && emp2.Interpret(context);
}

同样,OrExpression实现Interpret(...)方法如下。

public bool Interpret(Context context)
{
    return emp1.Interpret(context) || emp2.Interpret(context);
}

并且NotExpression实现了如下相同的方法。

public bool Interpret(Context context)
{
    return !emp.Interpret(context);
}

您可以看到每个复合表达式都在调用其所有子表达式的Interpret()方法。剩下的代码很容易理解,让我们继续。

Note

这种设计模式不会指导您如何构建语法树或如何解析句子。它给你自由,让你决定如何前进。

类图

图 23-4 为类图。

img/463942_2_En_23_Fig4_HTML.jpg

图 23-4

类图

解决方案资源管理器视图

图 23-5 显示了程序各部分的高层结构。

img/463942_2_En_23_Fig5_HTML.jpg

图 23-5

解决方案资源管理器视图

演示 2

下面是完整的实现。

using System;
using System.Collections.Generic;

namespace InterpreterPatternDemo2
{
    interface Employee
    {
        bool Interpret(Context context);
    }
    /// <summary>
    /// IndividualEmployee class
    /// </summary>
    class IndividualEmployee : Employee
    {
        private int yearOfExperience;
        private string currentGrade;
        public IndividualEmployee(int experience, string grade)
        {
            this.yearOfExperience = experience;
            this.currentGrade = grade;
        }
        public bool Interpret(Context context)
        {
            if (this.yearOfExperience >= context.GetYearofExperience()
                 && context.GetPermissibleGrades().Contains(this.currentGrade))
               {
                   return true;
               }
            return false;
        }
    }
    /// <summary>
    /// OrExpression class
    /// </summary>
    class OrExpression : Employee
    {
        private Employee emp1;
        private Employee emp2;
        public OrExpression(Employee emp1, Employee emp2)
        {
            this.emp1 = emp1;
            this.emp2 = emp2;
        }
        public bool Interpret(Context context)
        {
            return emp1.Interpret(context) || emp2.Interpret(context);
        }
    }
    /// <summary>
    /// AndExpression class
    /// </summary>
    class AndExpression : Employee
    {
        private Employee emp1;
        private Employee emp2;
        public AndExpression(Employee emp1, Employee emp2)
        {
            this.emp1 = emp1;
            this.emp2 = emp2;
        }
        public bool Interpret(Context context)
        {
            return emp1.Interpret(context) && emp2.Interpret(context);
        }
    }
    /// <summary>
    /// NotExpression class
    /// </summary>
    class NotExpression : Employee
    {
        private Employee emp;
        public NotExpression(Employee expr)
        {
            this.emp = expr;
        }
        public bool Interpret(Context context)
        {
            return !emp.Interpret(context);
        }
    }
    /// <summary>
    /// Context class
    /// </summary>
    class Context
    {
        private int experienceReqdForPromotion;
        private List<string> allowedGrades;
        public Context(int experience, List<string> allowedGrades)
        {
            this.experienceReqdForPromotion = experience;
            this.allowedGrades = new List<string>();
            foreach (string grade in allowedGrades)
            {
                this.allowedGrades.Add(grade);
            }
        }
        public int GetYearofExperience()
        {
            return experienceReqdForPromotion;
        }
        public List<string> GetPermissibleGrades()
        {
            return allowedGrades;
        }
    }
    /// <summary>
    /// EmployeeBuilder class
    /// </summary>
    class EmployeeBuilder
    {
        // Building the tree
        // Complex Rule-1: emp1 and (emp2 or (emp3 or emp4))
        public Employee BuildTreeBasedOnRule1(Employee emp1, Employee emp2, Employee emp3, Employee emp4)
        {
            // emp3 or emp4
            Employee firstPhase = new OrExpression(emp3, emp4);
            // emp2 or (emp3 or emp4)
            Employee secondPhase = new OrExpression(emp2, firstPhase);
            // emp1 and (emp2 or (emp3 or emp4))
            Employee finalPhase = new AndExpression(emp1, secondPhase);
            return finalPhase;
        }
        // Complex Rule-2: emp1 or (emp2 and (not emp3 ))
        public Employee BuildTreeBasedOnRule2(Employee emp1, Employee emp2, Employee emp3)
        {
            // Not emp3
            Employee firstPhase = new NotExpression(emp3);
            // emp2 or (not emp3)
            Employee secondPhase = new AndExpression(emp2, firstPhase);
            // emp1 and (emp2 or (not emp3 ))
            Employee finalPhase = new OrExpression(emp1, secondPhase);
            return finalPhase;
        }
    }
    public class Client
    {

        static void Main(string[] args)
        {
            Console.WriteLine("***Interpreter Pattern Demonstration-2***\n");

            // Minimum Criteria for promoton is:
            // The year of experience is minimum 10 yrs. and
            // Employee grade should be either G2 or G3
            List<string> allowedGrades = new List<string> { "G2", "G3" };
            Context context = new Context(10, allowedGrades);
            Employee emp1 = new IndividualEmployee(5, "G1");
            Employee emp2 = new IndividualEmployee(10, "G2");
            Employee emp3 = new IndividualEmployee(15, "G3");
            Employee emp4 = new IndividualEmployee(20, "G4");

            EmployeeBuilder builder = new EmployeeBuilder();

            // Validating the 1st complex rule
            Console.WriteLine("----- Validating the first complex rule.-----");
            Console.WriteLine("Is emp1 and any of emp2, emp3, emp4 is eligible for promotion?"
                + builder.BuildTreeBasedOnRule1(emp1, emp2, emp3, emp4).Interpret(context));
            Console.WriteLine("Is emp2 and any of emp1, emp3, emp4 is eligible for promotion?"
                + builder.BuildTreeBasedOnRule1(emp2, emp1, emp3, emp4).Interpret(context));
            Console.WriteLine("Is emp3 and any of emp1, emp2, emp3 is eligible for promotion?"
                + builder.BuildTreeBasedOnRule1(emp3, emp1, emp2, emp4).Interpret(context));
            Console.WriteLine("Is emp4 and any of emp1, emp2, emp3 is eligible for promotion?"
                + builder.BuildTreeBasedOnRule1(emp4, emp1, emp2, emp3).Interpret(context));

            Console.WriteLine("-----Validating the second complex rule now.-----");
            //Validating the 2nd complex rule
            Console.WriteLine("Is emp1 or (emp2 but not emp3) is eligible for promotion?"
                + builder.BuildTreeBasedOnRule2(emp1, emp2, emp3).Interpret(context));
            Console.WriteLine("Is emp2 or (emp3 but not emp4) is eligible for promotion?"
                + builder.BuildTreeBasedOnRule2(emp2, emp3, emp4).Interpret(context));
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Interpreter Pattern Demonstration-2***

----- Validating the first complex rule.-----
Is emp1 and any of emp2, emp3, emp4 is eligible for promotion?False
Is emp2 and any of emp1, emp3, emp4 is eligible for promotion?True
Is emp3 and any of emp1, emp2, emp3 is eligible for promotion?True
Is emp4 and any of emp1, emp2, emp3 is eligible for promotion?False
-----Validating the second complex rule now.-----
Is emp1 or (emp2 but not emp3) is eligible for promotion?False
Is emp2 or (emp3 but not emp4) is eligible for promotion?True

问答环节

23.1 什么时候应该使用这种模式?

说实话,日常编程中并不太需要。然而,在一些罕见的情况下,您可能需要使用您自己的编程语言,这可能会派上用场。但在你继续之前,你必须问自己,投资回报率(ROI)是多少?

23.2 使用解释器设计模式有什么好处?

以下是一些优点。

  • 你参与了为一种语言定义语法以及如何表达和解释句子的过程。你也可以改变和扩展语法。

  • 你有充分的自由去解释这些表达。

23.3 与使用解释器设计模式相关的 挑战 有哪些?

我相信工作量是最大的问题。此外,维护复杂的语法变得棘手,因为您可能需要创建(和维护)单独的类来处理不同的规则。

这是本书第一部分的结尾。我希望您喜欢所有 GoF 模式的所有详细实现。现在您可以转到本书的下一部分,这一部分涵盖了其他一些有趣的模式。

二十四、简单工厂模式

本章介绍简单工厂模式。

定义

简单工厂模式创建一个对象,而不向客户机公开实例化逻辑。

概念

在面向对象编程(OOP)中,工厂就是这样一种可以创建其他对象的对象。可以通过多种方式调用工厂,但最常见的是,它使用一种可以返回具有不同原型的对象的方法。任何帮助创建这些新对象的子程序都被认为是一个工厂。最重要的是,它帮助您从应用的消费者那里抽象出对象创建的过程。

真实世界的例子

在南印度餐馆,当你点你最喜欢的印度炒菜时,服务员可能会问你是否喜欢你的印度炒菜多加点香料,或者是否应该少加点香料。根据你的选择,厨师在主料中加入香料,为你做出合适的菜肴。

计算机世界的例子

简单工厂模式在软件应用中很常见,但是在继续之前,请注意以下几点。

  • 在 GoF 的著名著作中,简单工厂模式没有被视为标准设计模式,但是这种方法对于您编写的任何应用来说都是常见的,在这些应用中,您希望将变化很大的代码与没有变化的代码部分分开。假设您在编写的所有应用中都遵循这种方法。

  • 简单工厂模式被认为是工厂方法模式(和抽象工厂模式)的最简单形式。因此,您可以假设任何遵循工厂方法模式或抽象工厂模式的应用也遵循简单工厂模式的设计目标的概念。

在下面的实现中,我用一个常见的用例来讨论这个模式。让我们来看一下实现。

履行

这些是以下实现的重要特征。

  • 在这个例子中,你正在处理两种不同类型的动物:狗和老虎。具体有两个类:Dog.csTiger.cs。每个类都有一个共同的父类,IAnimal.cs。您会看到以下代码:

  • 我将创建对象的代码放在不同的地方(特别是在工厂类中)。使用这种方法,当您创建一只狗或一只老虎时,您不需要在客户端代码中直接使用new操作符。因此,在客户端代码中,您会看到下面一行:

// IAnimal.cs
namespace SimpleFactory
{
    public interface IAnimal
    {
        void AboutMe();
    }
}
// Dog.cs

using System;
namespace SimpleFactory
{
    public class Dog : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The dogs says: Bow-Wow.I prefer barking.");
        }
    }
}
//Tiger.cs
using System;

namespace SimpleFactory
{
    public class Tiger : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The tiger says: Halum.I prefer hunting.");
        }
    }
}

  • 在接下来的示例中,创建对象的过程取决于用户输入。我将可能变化的代码与最不可能变化的代码分开。这种机制可以帮助您消除系统中的紧密耦合。因此,在Main()中,您会看到下面的代码和支持性的注释:

    IAnimal preferredType = null;
    SimpleFactory simpleFactory = new SimpleFactory();
    #region The code region that can vary based on users preference
    /*
    * Since this part may vary, we're moving the
    * part to CreateAnimal() of SimpleFactory class.
    */
    preferredType = simpleFactory.CreateAnimal();
    #endregion
    #region The codes that do not change frequently.
    preferredType.AboutMe();
    #endregion
    
    
preferredType = simpleFactory.CreateAnimal();

Note

在某些地方,您可能会看到这种模式的变体,其中对象是通过参数化的构造函数(如preferredType=simpleFactory.CreateAnimal("Tiger"))创建的。

在接下来的例子中,我根据用户的输入选择动物,不需要参数化的构造函数。在本书的早期版本中,我使用了两种方法:Speak()Action()。但是为了使这个例子简短,我选择了一个叫做AboutMe()的方法。我把前面的两个方法合并成一个方法。

类图

图 24-1 为类图。

img/463942_2_En_24_Fig1_HTML.jpg

图 24-1

类图

解决方案资源管理器视图

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

img/463942_2_En_24_Fig2_HTML.jpg

图 24-2

解决方案资源管理器视图

示范

下面是完整的实现。程序的所有部分都被分开并放在名称空间 SimpleFactory 中。因此,对于下面的代码段,您可能会多次看到命名空间声明。

//IAnimal.cs
namespace SimpleFactory
{
    public interface IAnimal
    {
        void AboutMe();
    }
}
//Dog.cs

using System;
namespace SimpleFactory
{
    public class Dog : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The dog says: Bow-Wow.I prefer barking.");
        }
    }
}

//Tiger.cs
using System;

namespace SimpleFactory
{
    public class Tiger : IAnimal
    {
        public void AboutMe()
        {
            Console.WriteLine("The tiger says: Halum.I prefer hunting.");
        }
    }
}

//SimpleFactory.cs
using System;
namespace SimpleFactory
{
    public class SimpleFactory
    {
        public IAnimal CreateAnimal()
        {
            IAnimal intendedAnimal = null;
            Console.WriteLine("Enter your choice(0 for Dog, 1 for Tiger)");
            string b1 = Console.ReadLine();
            int input;
            if (int.TryParse(b1, out input))
            {
                Console.WriteLine("You have entered {0}", input);
                switch (input)
                {
                    case 0:
                        intendedAnimal = new Dog();
                        break;
                    case 1:
                        intendedAnimal = new Tiger();
                        break;
                    default:
                        Console.WriteLine("You must enter either 0 or 1");
                        //We'll throw a runtime exception for any other //choices.
                        throw new ApplicationException(String.Format
                        (" Unknown Animal cannot be instantiated."));
                }
            }
            return intendedAnimal;
        }
    }
}

//Program.cs(Client)
 using System;
namespace SimpleFactory
{
    /*
     * A client is interested to get an animal
     * who can tell something about it.
     */
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("*** Simple Factory Pattern Demo.***\n");
            IAnimal preferredType = null;
            SimpleFactory simpleFactory = new SimpleFactory();
            #region The code region that can vary based on users preference
            /*
             * Since this part may vary,we're moving the
             * part to CreateAnimal() in SimpleFactory class.
             */
            preferredType = simpleFactory.CreateAnimal();
            #endregion

            #region The codes that do not change frequently.
            preferredType.AboutMe();
            #endregion

            Console.ReadKey();
        }
    }
}

输出

以下是情况 1,用户输入为 0。

*** Simple Factory Pattern Demo.***

Enter your choice(0 for Dog, 1 for Tiger)
0
You have entered 0
The dog says: Bow-Wow.I prefer barking.

下面是情况 2,用户输入 1。

*** Simple Factory Pattern Demo.***

Enter your choice(0 for Dog, 1 for Tiger)
1
You have entered 1
The tiger says: Halum.I prefer hunting.

下面是情况 3,用户输入 3。

*** Simple Factory Pattern Demo.***

Enter your choice(0 for Dog, 1 for Tiger)
3
You have entered 3
You must enter either 0 or 1

在这种情况下,您会得到以下异常:“未知动物无法实例化”(见图 24-3 )。

img/463942_2_En_24_Fig3_HTML.jpg

图 24-3

由于输入无效,出现异常

问答环节

24.1 在这个例子中,我看到客户通过简单的工厂模式委托 对象的创建 。但是他们可以用 new 操作符直接创建对象。这是正确的吗?

不。这些是之前设计背后的主要原因。

  • 面向对象设计的关键原则之一是将代码中最有可能发生变化的部分与其余部分分开。

  • 在这种情况下,只有对象的创建过程会发生变化。您可以假设有代码片段来描述关于动物的一些事情,并且该部分代码不需要在客户端代码中变化。所以,在将来,如果在创建过程中需要任何更改,您只需要更改SimpleFactory类的CreateAnimal()方法。客户端代码不会因为这些更改而受到影响。

  • 您不希望在客户端主体中放置大量的if-else块(或switch语句)。这使得你的代码笨拙。

  • 客户端代码看不到您是如何创建对象的。这种抽象提高了安全性。

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

如果要添加新的动物或者删除已有的动物,需要修改CreateAnimal()方法。这个过程违反了 SOLID 原则的开放/封闭原则(即代码模块应该对扩展开放,但对修改关闭)。

Note

罗伯特·c·马丁提出了坚实的原则。有许多在线资源可用。如果你对快速介绍感兴趣,去 https://en.wikipedia.org/wiki/SOLID

24.3 你能让工厂类成为静态的吗?

可以,但是必须记住与静态类相关的限制。例如,您不能继承它们,等等。当您处理一些没有实现类或单独接口的值对象时,这是有意义的。当您使用不可变的类时,它也很有用,并且您的工厂类不需要在每次使用它时都返回一个全新的对象。

简而言之,值对象是其相等性基于值而不是身份的对象。值对象最重要的特征是,没有身份,它是不可变的。

一个简单的现实生活中的例子可以用印度的五卢比纸币和五卢比硬币来给出。它们的货币价值是相同的,但它们是不同的实例。

一般来说,静态工厂类可以提升全局状态,这对于面向对象编程来说并不理想。

二十五、空对象模式

本章介绍了空对象模式。

定义

空对象模式不是 GoF 设计模式。我从维基百科上得到这个定义,它是这样说的。

在面向对象的计算机编程中,空对象是指没有引用值或具有已定义的中立(“空”)行为的对象。空对象设计模式描述了此类对象的用途及其行为(或缺乏行为)。它最初发表在程序设计的模式语言系列丛书中。

概念

该模式可以实现“什么都不做”的关系,或者当应用遇到空对象而不是真实对象时,它可以提供默认行为。使用这种模式,我们的核心目标是通过if块避免“空对象检查”或“空协作检查”,并通过提供不做任何事情的默认行为来封装对象的缺失,从而制定一个更好的解决方案。该模式的基本结构如图 25-1 所示。

img/463942_2_En_25_Fig1_HTML.jpg

图 25-1

空对象模式的基本结构

本章从一个看似没问题的程序开始,但它有一个严重的潜在 bug。当您使用潜在的解决方案分析 bug 时,您会理解对空对象模式的需求。那么,让我们跳到下一节。

错误的程序

让我们假设您有两种不同类型的交通工具:BusTrain,,并且一个客户端可以传递不同的输入(例如,ab)来创建一个Bus对象或一个Train对象。下面的程序演示了这一点。当输入有效时,这个程序可以顺利运行,但是当您提供一个无效的输入时,一个潜在的错误就暴露出来了。这是有问题的程序。

using System;

namespace ProgramWithOnePotentialBug
{
    interface IVehicle
    {
        void Travel();
    }
    class Bus : IVehicle
    {
        public static int busCount = 0;
        public Bus()
        {
            busCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Bus");
        }
    }
    class Train : IVehicle
    {
        public static int trainCount = 0;
        public Train()
        {
            trainCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Train");
        }
    }

    class Program
    {
        static void Main(string[] args)

        {
            Console.WriteLine("***This program demonstrates the need of null object pattern.***\n");
            string input = String.Empty;
            int totalObjects = 0;

            while (input != "exit")
            {
                Console.WriteLine("Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.");
                input = Console.ReadLine();
                IVehicle vehicle = null;
                switch (input)
                {
                    case "a":
                        vehicle = new Bus();
                        break;
                    case "b":
                        vehicle = new Train();
                        break;
                    case "exit":
                        Console.WriteLine("Creating one more bus and closing the application");
                        vehicle = new Bus();
                        break;
                }
                totalObjects = Bus.busCount + Train.trainCount;
                vehicle.Travel();
                Console.WriteLine($"Total objects created in the system ={totalObjects}");
            }
        }
    }
}

具有有效输入的输出

你可能有一个眼前的问题;当你输入exit时,你创建了一个不必要的对象。这是真的。我们以后再处理。现在,让我们关注另一个对我们来说更危险的 bug。下面是一些有效输入的输出。

***This program demonstrates the need of null object pattern.***

Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
a
Let us travel with Bus
Total objects created in the system =1
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
b
Let us travel with Train
Total objects created in the system =2
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
a
Let us travel with Bus
Total objects created in the system =3
Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.

不需要输入的分析

让我们假设用户错误地提供了一个不同的字符,比如 e ,如下所示。

Enter your choice(Type 'a' for Bus, 'b' for Train.Type 'exit' to quit application.
e

这一次,你得到了一个名为System.NullReferenceException的运行时异常,如图 25-2 所示。

img/463942_2_En_25_Fig2_HTML.jpg

图 25-2

当用户提供无效输入时,会发生运行时异常

潜在的解决办法

您可能想到的直接补救方法是在调用操作之前进行空检查,如下所示。

if (vehicle != null)
{
  vehicle.Travel();
}

分析

先前的解决方案在这种情况下有效。但是请考虑一个企业应用。当您对每个场景进行空检查时,如果您在每个场景中都像这样放置if条件,那么您的代码就会变脏。同时,你可能会注意到维护困难的副作用。空对象模式的概念在类似的情况下很有用。

Point to Remember

在前面的例子中,当用户键入 exit 时,我可以避免创建不必要的对象,如果我使用如下的空条件操作符,也可以避免空检查:

vehicle?.Travel();

该运算符仅在 C# 6 和更高版本中可用。不过,研究一下空对象模式的实现细节对您还是有好处的。例如,当您使用空对象模式时,您可以为这些空对象提供默认行为(最适合您的应用),而不是什么都不做。

真实世界的例子

当有水供应而没有任何内部泄漏时,洗衣机就能正常工作。但是假设有一次,你忘记在开始洗衣服之前供水,但是你按下了开始洗衣服的按钮。在这种情况下,洗衣机不应损坏自身;所以,它可以发出警报声来引起你的注意,并指示此刻没有供水。

计算机世界的例子

假设在客户端-服务器架构中,服务器基于客户端输入进行计算。服务器需要足够智能,不会启动任何不必要的计算。在处理输入之前,它可能希望进行交叉验证,以确保是否需要开始计算,或者应该忽略无效的输入。在这种情况下,您可能会注意到带有空对象模式的命令模式。

基本上,在企业应用中,使用这种设计模式可以避免大量的 null 检查和 if/else 阻塞。下面的实现给出了这种模式的概述。

履行

让我们修改之前讨论过的有问题的程序。这次您通过一个NullVehicle对象处理无效输入。因此,如果用户错误地提供了任何无效数据(换句话说,除了本例中的 ab 之外的任何输入),应用什么都不做;也就是说,它可以通过一个NullVehicle对象忽略那些无效输入,这个对象什么也不做。该类定义如下。

/// <summary>
/// NullVehicle class
/// </summary>
class NullVehicle : IVehicle
{
 private static readonly NullVehicle instance = new NullVehicle();
 private NullVehicle()
 {
  nullVehicleCount++;
  }
 public static int nullVehicleCount;
 public static NullVehicle Instance
 {
  get
  {
    return instance;
  }
 }
 public void Travel()
{
   // Do Nothing
}
}

您可以看到,当我创建一个NullVehicle对象时,我应用了单体设计模式的概念。因为可能有无限多的无效输入,所以在下面的例子中,我不想重复创建NullVehicle对象。一旦有了一个NullVehicle对象,我想重用那个对象。

Note

对于空对象方法,您需要返回任何看起来合理的默认值。在我们的例子中,你不能乘坐一辆不存在的车辆。因此,对于NullVehicle类来说,Travel()方法什么也不做是有道理的。

类图

图 25-3 为类图。

img/463942_2_En_25_Fig3_HTML.jpg

图 25-3

类图

解决方案资源管理器视图

图 25-4 显示了程序的高层结构。

img/463942_2_En_25_Fig4_HTML.jpg

图 25-4

解决方案资源管理器视图

示范

下面是完整的实现。

using System;
namespace NullObjectPattern
{
    interface IVehicle
    {
        void Travel();
    }
    /// <summary>
    /// Bus class
    /// </summary>
    class Bus : IVehicle
    {
        public static int busCount = 0;
        public Bus()
        {
            busCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Bus.");
        }
    }
    /// <summary>
    /// Train class
    /// </summary>
    class Train : IVehicle
    {
        public static int trainCount = 0;
        public Train()
        {
            trainCount++;
        }
        public void Travel()
        {
            Console.WriteLine("Let us travel with Train.");
        }
    }
    /// <summary>
    /// NullVehicle class
    /// </summary>
    class NullVehicle : IVehicle
    {
        private static readonly NullVehicle instance = new NullVehicle();
        private NullVehicle()
        {
            nullVehicleCount++;
        }

        public static int nullVehicleCount;
        public static NullVehicle Instance
        {
            get
            {
                return instance;
            }
        }
        public void Travel()
        {
            // Do Nothing
        }
    }
    /// <summary>
    /// Client code
    /// </summary>
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Null Object Pattern Demonstration.***\n");
            string input = String.Empty;
            int totalObjects = 0;

            while (input != "exit")
            {
                Console.WriteLine("Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit) ");
                input = Console.ReadLine();
                IVehicle vehicle = null;
                switch (input)
                {
                    case "a":
                        vehicle = new Bus();
                        break;
                    case "b":
                        vehicle = new Train();
                        break;
                    case "exit":
                        Console.WriteLine("Closing the application.");
                        vehicle = NullVehicle.Instance;
                        break;
                    default:
                        Console.WriteLine("Please supply the correct input(a/b/exit)");
                        vehicle = NullVehicle.Instance;
                        break;
                }
                totalObjects = Bus.busCount + Train.trainCount + NullVehicle.nullVehicleCount;
                // No need to do null check now.
                //if (vehicle != null)
                vehicle.Travel();
                //}
                Console.WriteLine("Total objects created in the system ={0}",
                totalObjects);

            }
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***Null Object Pattern Demonstration.***

Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
a
Let us travel with Bus.
Total objects created in the system =2
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
b
Let us travel with Train.
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
c
Please supply the correct input(a/b/exit)
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
d
Please supply the correct input(a/b/exit)
Total objects created in the system =3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
b
Let us travel with Train.
Total objects created in the system =4
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to quit)
exit
Closing the application.
Total objects created in the system =4

分析

我提请你注意以下几点。

  • 无效输入及其影响以粗体显示。

  • 由于空车辆对象/无效输入,对象计数没有增加。

  • 您没有执行任何空值检查。尽管如此,程序执行不会因为无效的用户输入而中断。

问答环节

25.1 在实现的开始,我看到创建了一个额外的对象。这是故意的吗?

为了节省一些计算机内存/存储,我在构造NullVehicle类时遵循了支持早期初始化的单例设计模式。您不希望为每个无效输入创建一个NullVehicle对象,因为您的应用可能会收到大量无效输入。如果您不防范这种情况,大量的NullVehicle对象可能会驻留在系统中(这是无用的),它们会占用大量的计算机内存,这反过来会导致一些不必要的副作用。(例如,系统可能会变慢,应用响应时间可能会增加,等等。)

25.2 什么时候应该使用这种模式?

这种模式在下列情况下很有用。

  • 您不希望遇到NullReferenceException(例如,如果您错误地试图调用一个空对象的方法)。

  • 您喜欢忽略代码中的大量空检查。

  • 你想让你的代码更干净,更容易维护。

Note

在这一章的最后,你会学到这种模式的另一种用法。

25.3 与空对象模式相关的 挑战 有哪些?

您需要注意以下情况。

  • 大多数情况下,您可能希望找到并修复失败的根本原因。所以,如果你扔一个NullReferenceException,那对你来说会更好。您总是可以在try / catch块或try / catch / finally块中处理这些异常,并相应地更新日志信息。

  • 当您无意中想要处理一个根本不存在的对象时,空对象模式可以帮助您实现一个默认行为。但是试图提供这样的默认行为可能并不总是合适的。

  • 空对象模式的不正确实现会抑制程序执行中可能正常出现的真正错误。

25.4。看起来好像空对象像代理一样工作。这是正确的吗?

不会。一般来说,代理在某个时间点作用于真实对象,它们也可能提供一些行为。但是空对象不应该做这样的事情。

25.5。空对象模式总是与 NullReferenceException 相关联。这是正确的吗?

概念是相同的,但是异常名可以不同或特定于语言。例如,在 Java 中,您可以使用此模式来防范 java.lang.NullPointerException,但在 C# 这样的语言中,您使用它来防范 System.NullReferenceException。

最后,我想提请大家注意另一个有趣的点。空对象模式在另一个上下文中很有用。例如,考虑下面的代码段。

            //A case study in another context.
            List<IVehicle> vehicleList = new List<IVehicle>();
            vehicleList.Add(new Bus());
            vehicleList.Add(new Train());
            vehicleList.Add(null);
            foreach (IVehicle vehicle in vehicleList)
            {
                vehicle.Travel();
            }

当你使用前面的代码段时,你再次得到System.NullReferenceException。但是如果你用vehicleList.Add(NullVehicle.Instance);代替vehicleList.Add(null);,就没有运行时异常。因此,您可以轻松地循环,这是该模式的另一个重要用途。

二十六、MVC 模式

本章涵盖了 MVC 模式。

定义

MVC(模型-视图-控制器)是一种架构模式。这种模式通常用于 web 应用和开发强大的用户界面。Trygve Reenskaug 于 1979 年在一篇题为“Smalltalk-80TM 中的应用编程:如何使用模型-视图-控制器”的论文中首次描述了 MVC,这篇论文是在万维网存在之前写的。所以,那时候还没有 web 应用的概念。但是现代的应用是最初概念的改编。一些开发人员宁愿称之为“MVC 架构”,而不是真正的设计模式

维基百科是这样定义的。

模型-视图-控制器(Model-view-controller,MVC)是一种通常用于开发用户界面的架构模式,它将应用分成三个相互连接的部分。这样做是为了将信息的内部表示与信息呈现给用户并被用户接受的方式分开。MVC 设计模式将这些主要组件解耦,允许高效的代码重用和并行开发。 ( https://en.wikipedia.org/wiki/Model-view-controller )

我最喜欢的关于 MVC 的描述来自 Connelly Barnes,他说,

理解 MVC 的一个简单方法:模型是数据,视图是屏幕上的窗口,控制器是两者之间的粘合剂。 ( http://wiki.c2.com/?ModelViewController )

概念

使用这种模式,您可以将用户界面逻辑与业务逻辑分离开来,并以一种可以有效重用的方式分离主要组件。这种方法促进了并行开发。

从定义中可以明显看出,模式由这些主要组件组成:模型、视图和控制器。控制器放置在视图和模型之间,使得它们只能通过控制器相互通信。该模型将数据显示机制与数据操作机制分开。图 26-1 显示了 MVC 模式。

img/463942_2_En_26_Fig1_HTML.jpg

图 26-1

典型的 MVC 架构

需要记住的要点

这些是对该模式中关键组件的简要描述。

  • 视图表示最终输出。它还可以接受用户输入。它是表示层,你可以把它想象成一个图形用户界面(GUI)。你可以用各种技术来设计它。例如,在. NET 应用中,您可以使用 HTML、CSS、WPF 等等,而对于 Java 应用,您可以使用 AWT、Swing、JSF、JavaFX 等等。

  • 模型管理数据和业务逻辑,它充当应用的实际大脑。它管理数据和业务逻辑。它知道如何存储、管理或操作数据,并处理来自控制器的请求。但是这个组件与视图组件是分离的。一个典型的例子是数据库、文件系统或类似的存储。它可以用 Oracle、SQL Server、DB2、Hadoop、MySQL 等等来设计。

  • 控制器是中介。它接受来自视图组件的用户输入,并将请求传递给模型。当它从模型得到响应时,它将数据传递给视图。可以用 C# 设计。NET、ASP.NET、VB.NET、核心 Java、JSP、Servlets、PHP、Ruby、Python 等等。

您可能会注意到不同应用中的不同实现。这里有一些例子。

  • 您可以有多个视图。

  • 视图可以将运行时值(例如,使用 JavaScript)传递给控制器。

  • 您的控制器可以验证用户的输入。

  • 您的控制器可以通过多种方式接收输入。例如,它可以通过 URL 从 web 请求中获取输入,或者通过单击表单上的 Submit 按钮传递输入。

  • 在某些应用中,您可能会注意到模型可以更新视图组件。

简而言之,您需要使用这个模式来支持您自己的需求。图 26-2 、 26-3 和 26-4 显示了 MVC 架构的已知变体。

变体 1

图 26-2 为变型 1。

img/463942_2_En_26_Fig2_HTML.jpg

图 26-2

典型的 MVC 框架

变体 2

图 26-3 为变型 2。

img/463942_2_En_26_Fig3_HTML.jpg

图 26-3

一个多视图的 MVC 框架

变体 3

图 26-4 为变型 3。

img/463942_2_En_26_Fig4_HTML.jpg

图 26-4

用观察者模式/基于事件的机制实现的 MVC 模式

对 MVC 最好的描述之一来自于 wiki。c2。com ( http://wiki.c2.com/?ModelViewController ),上面写着,“我们需要智能模型、轻薄控制器、视图。”

真实世界的例子

考虑我们的模板方法模式的真实例子。但这一次,让我们换个角度来解读。我说在餐厅里,根据顾客的输入,一个厨师调整口味,做出最后一道菜。但是你知道顾客不会直接向厨师下订单。顾客看到菜单卡(视图)后,可能会咨询服务员,然后下订单。服务员将订单交给厨师,厨师从餐厅的厨房(类似于仓库或计算机数据库)收集所需的材料。准备好后,服务员把盘子端到顾客的桌子上。所以,你可以考虑一个服务员作为控制者,厨房里的厨师作为模型,食物准备材料作为数据。

计算机世界的例子

许多 web 编程框架使用 MVC 框架的概念。典型的例子包括 Django、Ruby on Rails、ASP.NET 等等。一个典型的 ASP.NET MVC 项目可以有如下图所示的视图 26-5 。

img/463942_2_En_26_Fig5_HTML.jpg

图 26-5

一个典型的 ASP.NET MVC 项目的解决方案浏览器视图

Points to Note

不同的技术遵循不同的结构,所以你不需要如图 26-5 所示的严格命名约定的文件夹结构。

履行

为了简单和符合我们的理论,我还将即将到来的实现分成三个主要部分:模型、视图和控制器。一旦注意到解决方案资源管理器视图,您就可以确定为完成此任务而创建的独立文件夹。以下是一些要点。

  • IModel, IView,IController是三个接口,分别由具体的类EmployeeModel, ConsoleView,EmployeeController,实现。看到这些名称,您可以假设它们是我们 MVC 架构的模型、视图和控制器层的代表。

  • 在这个应用中,要求非常简单。一些员工需要在申请表上注册。最初,该应用有三个不同的注册员工:Amit、Jon 和 Sam。这些员工的 ID 是 E1、E2 和 E3。所以,你看到下面这个构造函数:

    public EmployeeModel()
    {
        // Adding 3 employees at the beginning.
        enrolledEmployees = new List<Employee>();
        enrolledEmployees.Add(new Employee("Amit", "E1"));
        enrolledEmployees.Add(new Employee("John", "E2"));
        enrolledEmployees.Add(new Employee("Sam", "E3"));
    }
    
    
  • 在任何时间点,您都应该能够在系统中看到注册的员工。在客户端代码中,您调用控制器对象上的DisplayEnrolledEmployees(),如下所示:

controller.DisplayEnrolledEmployees();

然后,控制器将调用传递给视图层,如下所示:

view.ShowEnrolledEmployees(enrolledEmployees);

您会看到视图接口的具体实现者(ConsoleView.cs)对该方法的描述如下:

  • 您可以在注册员工列表中添加新员工或删除员工。为此使用了AddEmployeeToModel(Employee employee)RemoveEmployeeFromModel(string employeeIdToRemove)方法。让我们看看RemoveEmployeeFromModel(...)的方法签名。要删除一个雇员,您需要提供雇员 ID(它只不过是一个字符串)。但是如果没有找到雇员 ID,应用将忽略这个删除请求。

  • 在 Employee 类中添加了一个简单的检查,以确保不会在应用中重复添加具有相同 ID 的雇员。

public void ShowEnrolledEmployees (List<Employee> enrolledEmployees)
{
        Console.WriteLine("\n ***This is a console view of currently enrolled employees.*** ");
        foreach (Employee emp in enrolledEmployees)
        {
                Console.WriteLine(emp);
        }
        Console.WriteLine("---------------------");
}

现在来看一下实现。是的,它很大,但是当你在前面的要点和支持图的帮助下一部分一部分地分析它时,你应该不会在理解代码上遇到任何困难。也可以考虑一下评论,供自己即时参考。

Points to Note

通常,您希望将 MVC 与提供内置支持并执行大量基础工作的技术结合使用。例如,当你使用 ASP.NET(或类似的技术)来实现 MVC 模式时,因为你有很多内置的支持。在这些情况下,你需要学习新的术语。

在本书中,我使用控制台应用来实现设计模式。让我们在即将到来的实现中继续使用同样的方法,因为我们的重点只放在 MVC 架构上。

类图

图 26-6 为类图。

img/463942_2_En_26_Fig6_HTML.jpg

图 26-6

类图

解决方案资源管理器视图

图 26-7 显示了程序的高层结构。

img/463942_2_En_26_Fig7_HTML.jpg

图 26-7

解决方案资源管理器视图

演示 1

这是完整的演示。

模型文件夹中的内容

// Employee.cs

namespace MVCPattern.Model
{
    // The key "data" in this application
    public class Employee
    {
        private string empName;
        private string empId;
        public string GetEmpName()
        {
            return empName;
        }
        public string GetEmpId()
        {
            return empId;
        }
        public Employee(string empName, string empId)
        {
            this.empName = empName;
            this.empId = empId;
        }

        public override string ToString()
        {
            return  $"{empName} is enrolled with id : {empId}.";
        }
    }
}

// Model.cs
using System.Collections.Generic;

namespace MVCPattern.Model
{
    public interface IModel
    {

        List<Employee> GetEnrolledEmployeeDetailsFromModel();
        void AddEmployeeToModel(Employee employeee);
        void RemoveEmployeeFromModel(string employeeId);
    }
}

// EmployeeModel.cs
using System;
using System.Collections.Generic;

namespace MVCPattern.Model
{
    public class EmployeeModel : IModel
    {
        List<Employee> enrolledEmployees;

        public EmployeeModel()
        {
            // Adding 3 employees at the beginning.
            enrolledEmployees = new List<Employee>();
            enrolledEmployees.Add(new Employee("Amit", "E1"));
            enrolledEmployees.Add(new Employee("John", "E2"));
            enrolledEmployees.Add(new Employee("Sam", "E3"));
        }

        public List<Employee> GetEnrolledEmployeeDetailsFromModel()
        {
            return enrolledEmployees;
        }

        // Adding an employee to the model(registered employee list)
        public void AddEmployeeToModel(Employee employee)
        {
            Console.WriteLine($"\nTrying to add an employee to the registered list.The employee name is {employee.GetEmpName()} and id is {employee.GetEmpId()}.");

            if (!enrolledEmployees.Contains(employee))
            {
                enrolledEmployees.Add(employee);
                Console.WriteLine(employee + " [added recently.]");
            }
            else
            {
                Console.WriteLine("This employee is already added in the registered list.So, ignoring the request of addition.");
            }
        }
        // Removing an employee from model(registered employee list)

        public void RemoveEmployeeFromModel(string employeeIdToRemove)
        {
            Console.WriteLine($"\nTrying to remove an employee from the registered list.The employee id is {employeeIdToRemove}.");
            Employee emp = FindEmployeeWithId(employeeIdToRemove);
            if (emp != null)
            {
                Console.WriteLine("Removing this employee.");
                enrolledEmployees.Remove(emp);
            }
            else
            {
                Console.WriteLine($"At present, there is no employee with id {employeeIdToRemove}.Ignoring this request.");
            }
        }
        Employee FindEmployeeWithId(string employeeIdToRemove)
        {
            Employee removeEmp = null;
            foreach (Employee emp in enrolledEmployees)
            {
                if (emp.GetEmpId().Equals(employeeIdToRemove))
                {

                    Console.WriteLine($" Employee Found.{emp.GetEmpName()} has id: { employeeIdToRemove}.");
                    removeEmp = emp;
                }
            }
            return removeEmp;
        }
    }
}

视图文件夹中的内容

// View.cs
using MVCPattern.Model;
using System.Collections.Generic;

namespace MVCPattern.View
{
    public interface IView
    {
        void ShowEnrolledEmployees(List<Employee> enrolledEmployees);
    }
}

// ConsoleView.cs
using System;
using System.Collections.Generic;
using MVCPattern.Model;

namespace MVCPattern.View
{
    public class ConsoleView : IView
    {
        public void ShowEnrolledEmployees(List<Employee> enrolledEmployees)
        {
            Console.WriteLine("\n ***This is a console view of currently enrolled employees.*** ");
            foreach (Employee emp in enrolledEmployees)
            {
                Console.WriteLine(emp);
            }
            Console.WriteLine("---------------------");
        }
    }
}

控制器文件夹中的内容

// Controller.cs
using MVCPattern.Model;

namespace MVCPattern.Controller
{
    interface IController
    {
        void DisplayEnrolledEmployees();
        void AddEmployee(Employee employee);
        void RemoveEmployee(string employeeId);
    }

}

// EmployeeController.cs
using System.Collections.Generic;
using MVCPattern.Model;
using MVCPattern.View;

namespace MVCPattern.Controller
{
    public class EmployeeController : IController
    {
        IModel model;
        IView view;

        public EmployeeController(IModel model, IView view)
        {
            this.model = model;
            this.view = view;
        }

        public void DisplayEnrolledEmployees()
        {
            // Get data from Model
            List<Employee> enrolledEmployees = model.GetEnrolledEmployeeDetailsFromModel();
            // Connect to View
            view.ShowEnrolledEmployees(enrolledEmployees);
        }

        // Sending a request to model to add an employee to the list.
        public void AddEmployee(Employee employee)
        {
            model.AddEmployeeToModel(employee);
        }
        // Sending a request to model to remove an employee from the list.
        public void RemoveEmployee(string employeeId)
        {
            model.RemoveEmployeeFromModel(employeeId);

        }
    }
}

客户代码

// Program.cs
using MVCPattern.Controller;
using MVCPattern.Model;
using MVCPattern.View;
using System;

namespace MVCPattern
{
    class Client
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***MVC architecture Demo***\n");
            // Model
            IModel model = new EmployeeModel();

            // View
            IView view = new ConsoleView();

            // Controller
            IController controller = new EmployeeController(model, view);
            controller.DisplayEnrolledEmployees();

            // Add an employee
            Employee empToAdd = new Employee("Kevin", "E4");
            controller.AddEmployee(empToAdd);
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            // Remove an existing employee using the employee id.
            controller.RemoveEmployee("E2");
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            /* Cannot remove an  employee who does not belong to the list.*/
            controller.RemoveEmployee("E5");
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            // Avoiding a duplicate entry
            controller.AddEmployee(empToAdd);
            // Printing the current details
            controller.DisplayEnrolledEmployees();

            /* This segment is added to discuss a question in "Q&A Session" and initially commented out. */
           // view = new MobileDeviceView();
           // controller = new EmployeeController(model, view);
           // controller.DisplayEnrolledEmployees();
            Console.ReadKey();
        }
    }
}

输出

这是输出。

***MVC architecture Demo***

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
Kevin is enrolled with id : E4\. [added recently.]

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E2.
 Employee Found.John has id: E2.
Removing this employee.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E5.
At present, there is no employee with id E5.Ignoring this request.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
This employee is already added in the registered list.So, ignoring the request of addition.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

问答环节

假设你有一名程序员、一名数据库管理员和一名图形设计师。你能预测他们在 MVC 架构中的角色吗?

图形设计师设计视图层,DBA 创建模型,程序员制作智能控制器。

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

一些重要的优点如下。

  • 高内聚和低耦合是 MVC 的好处。您可能已经注意到,在这种模式中,模型和视图之间的紧密耦合很容易消除。因此,应用可以很容易地扩展和重用。

  • 该模式支持并行开发。

  • 您还可以容纳多个运行时视图。

26.3 与 MVC 模式相关的挑战是什么?

这里有一些挑战。

  • 它需要高度熟练的人员。

  • 对于微小的应用来说,可能不太适合。

  • 开发人员可能需要熟悉多种语言、平台和技术。

  • 多工件一致性是一个大问题,因为您将整个项目分成三个主要部分。

26.4 你能在这个实现中提供多个视图吗?

当然可以。让我们在应用中添加一个名为 MobileDeviceView 的新的更短的视图。让我们将这个类添加到视图文件夹中,如下所示。

using System;
using System.Collections.Generic;
using MVCPattern.Model;
namespace MVCPattern.View
{
    public class MobileDeviceView:IView
    {

        public void ShowEnrolledEmployees(List<Employee> enrolledEmployees)
        {
            Console.WriteLine("\n +++This is a mobile device view of currently enrolled employees.+++ ");
            foreach (Employee emp in enrolledEmployees)
            {
                Console.WriteLine(emp.GetEmpId() + "\t" + emp.GetEmpName());
            }
            Console.WriteLine("+++++++++++++++++++++");
        }
    }
}

一旦添加了这个类,修改后的解决方案资源管理器视图应该类似于图 26-8 。

img/463942_2_En_26_Fig8_HTML.jpg

图 26-8

修改的解决方案资源管理器视图

现在,在客户端代码的末尾添加以下代码段(请参考注释以供参考)。

/* This segment is added to discuss a question in "Q&A Session and was
   initially commented out.Now I’m uncommenting the following three lines of code."
*/
view = new MobileDeviceView();
controller = new EmployeeController(model, view);
controller.DisplayEnrolledEmployees();

现在,如果您运行应用,您会看到修改后的输出。

修改输出

下面是修改后的输出。输出的最后一部分显示了新变化的效果。更改以粗体显示。

***MVC architecture Demo***

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
Kevin is enrolled with id : E4\. [added recently.]

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
John is enrolled with id : E2.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E2.
 Employee Found.John has id: E2.
Removing this employee.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to remove an employee from the registered list.The employee id is E5.
At present, there is no employee with id E5.Ignoring this request.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------

Trying to add an employee to the registered list.The employee name is Kevin and id is E4.
This employee is already added in the registered list.So, ignoring the request of addition.

 ***This is a console view of currently enrolled employees.***
Amit is enrolled with id : E1.
Sam is enrolled with id : E3.
Kevin is enrolled with id : E4.
---------------------
 +++This is a mobile device view of currently enrolled employees

.+++
E1      Amit
E3      Sam
E4      Kevin
+++++++++++++++++++++

二十七、异步编程中的模式

你会在异步编程中看到许多有趣的模式,这很艰难,很有挑战性,但也很有趣。它通常被称为异步。整体概念不是一天进化出来的,这需要时间,而在 C# 5.0 中,你得到了asyncawait关键词让它变得更简单。在此之前,程序员用各种技术实现了这个概念。每种技术都有其优点和缺点。本章的目标是向你介绍不同的异步编程模式。

概观

首先,让我们讨论异步编程。简单地说,你在你的应用中取一个代码段,并在一个单独的线程上运行它。关键优势是什么?简单的答案是,您可以释放原始线程,让它继续执行剩余的任务,而在一个单独的线程中,您可以执行不同的任务。这种机制帮助您开发现代应用;例如,当您实现一个高度响应的用户界面时,这些概念非常有用。

Points to Remember

大体上,您会注意到异步编程中的三种不同模式,如下所示:

  • IAsyncResult 模式 : 或者,它被称为异步编程模型(APM)。在这个模式中,在核心处,您可以看到支持异步行为的IAsyncResult接口。在同步模型中,如果您有一个名为 XXX()的同步方法,在异步版本中,您会看到对应同步方法的BeginXXX()EndXXX()方法。比如在同步版本中,如果你有Read()方法支持读操作;在异步编程中,通常有BeginRead()EndRead()方法来异步支持相应的读操作。使用这个概念,从演示 5 到演示 7,您会看到BeginInvokeEndInvoke方法。但是对于即将到来的和新的开发,不推荐使用这种模式。

  • 😗*基于事件的异步模式。NET 框架 2.0。它基于事件机制。这里您可以看到带有Async后缀的方法名,一个或多个事件,以及EventArg派生类型。这种模式仍在使用,但不推荐用于新的开发。

**** 基于任务的异步模式(TAP) : 它最早出现在 in.NET 框架 4 中;这是异步编程的推荐做法。在 C# 中,你经常会看到这种模式中的asyncawait关键字。***

***为了使这一章简短,我可以省略关于 APM 和 EAP 的讨论,但是我在这一章中讨论它们,以便你理解遗留代码。同时,您发现了异步编程持续发展的途径。

为了更好地理解异步编程,让我们从它的对应物开始讨论:同步编程。同步方法很简单,代码路径也很容易理解,但是在这种编程中,您需要等待从特定的代码段获得结果,直到您不能做任何有成效的事情。例如,当一段代码试图打开一个需要时间加载的网页时,或者当一段代码正在运行一个长时间运行的算法时,等等。在这些情况下,如果您遵循同步方法,您需要无所事事。因此,即使你的计算机速度超快,计算能力更强,你也没有充分发挥它的潜力,这不是一个好主意。因此,为了支持现代需求和构建高响应性的应用,对异步编程的需求与日俱增。因此,当您了解这一类别中不同的实现模式时,您会受益匪浅。

使用同步方法

在演示 1 中,我执行了一个简单的程序,从同步方法开始。这里有两个简单的方法叫做ExecuteMethodOne()ExecuteMethodTwo()。在Main()方法内部,我同步调用这些方法(即,我先调用ExecuteMethodOne(),然后调用ExecuteMethodTwo())。为了专注于关键的讨论,我把这些方法做得非常简单。我将简单的 sleep 语句放入其中,以确保这些方法执行的工作需要一定的时间来完成。一旦您运行应用并注意到输出,您会看到只有在ExecuteMethodOne()完成执行后,ExecuteMethodTwo()才能开始执行。在这种情况下,Main()方法不能完成,直到方法完成它们的执行。

Note

在本章中,你会看到这些方法略有不同。我试图维护类似的方法,以便您可以轻松地比较异步编程的不同技术。出于简单演示的目的,在这些例子中,我假设ExecuteMethodOne()需要更多的时间来完成,因为它将执行一些冗长的操作。所以,我强迫自己在里面睡了一个相对长的时间。相反,我假设ExecuteMethodTwo()执行一个小任务,所以我在里面放置了一个相对较短的睡眠。

演示 1

这是完整的演示。

using System;
using System.Threading;

namespace SynchronousProgrammingExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***A Synchronous Program Demonstration.***");
            Console.WriteLine("ExecuteMethodTwo() needs to wait for ExecuteMethodOne() to finish first.");
            ExecuteMethodOne();
            ExecuteMethodTwo();
            Console.WriteLine("End Main().");
            Console.ReadKey();
        }
        // First Method
        private static void ExecuteMethodOne()
        {
            Console.WriteLine("MethodOne has started.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("MethodOne has finished.");
        }
        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }
    }
}

输出

这是输出。

***A Synchronous Program Demonstration.***
ExecuteMethodTwo() needs to wait for ExecuteMethodOne() to finish first.
MethodOne has started.
MethodOne has finished.
MethodTwo has started.
MethodTwo has finished.
End Main().

使用线程类

如果您仔细观察演示 1 中的方法,您会发现这些方法并不相互依赖。如果您可以并行执行它们,您的应用的响应时间将会得到改善,并且您可以减少总的执行时间。所以,让我们找到一些更好的方法。

在这种情况下,您可以实现多线程的概念。演示 2 是一个使用线程的简单解决方案。它展示了在一个新线程中替换ExecuteMethodOne()方法。

演示 2

using System;
using System.Threading;

namespace UsingThreadClass
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Asynchronous Programming using Thread class.***");
            //ExecuteMethodOne();
            //Old approach.Creating a separate thread for the following //task(i.e. ExecuteMethodOne.)
            Thread newThread = new Thread(() =>
            {
                Console.WriteLine("MethodOne has started on a separate thread.");
                // Some big task
                Thread.Sleep(1000);
                Console.WriteLine("MethodOne has finished.");
            }
            );
            newThread.Start();
            /*
               Taking a small sleep to increase the probability of executing ExecuteMethodOne() before ExecuteMethodTwo().
             */
            Thread.Sleep(20);
            ExecuteMethodTwo();
            Console.WriteLine("End Main().");
            Console.ReadKey();
        }

        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }
    }
}

输出

下面是一个可能的输出。

***Asynchronous Programming using Thread class.***
MethodOne has started on a separate thread.
MethodTwo has started.
MethodTwo has finished.
End Main().
MethodOne has finished.

分析

注意,尽管ExecuteMethodOne()开始得很早,但是ExecuteMethodTwo()并没有等待ExecuteMethodOne()完成它的执行。此外,由于ExecuteMethodTwo()做得很少(睡眠时间为 100 毫秒),它能够在ExecuteMethodOne()完成执行之前完成。不仅如此,由于主线程没有被阻塞,它能够继续执行。

问答环节

27.1 为什么在 Main 里面的 Method2() 执行之前放一个睡眠语句?

接得好。这不是必须的,但是在某些情况下,您可能会注意到,即使您试图在当前线程中的ExecuteMethodTwo()之前启动ExecuteMethodOne()在一个单独的线程上执行,也不会发生这种情况。因此,您可能会注意到以下输出。

***Asynchronous Programming using Thread class.***
MethodTwo has started.
MethodOne has started in a separate thread.
MethodTwo has finished.
End Main().
MethodOne has finished.

在这个例子中,这个简单的 sleep 语句帮助你增加在ExecuteMethodTwo()之前开始ExecuteMethodOne()的概率。

使用线程池类

通常不鼓励在现实世界的应用中直接创建线程。这背后的一些关键原因如下。

  • 维护太多的线程会导致困难和高成本的操作。

  • 由于上下文切换,浪费了大量时间,而不是做真正的工作。

为了避免直接创建线程,C# 为您提供了使用内置ThreadPool类的便利。有了这个类,您可以使用现有的线程,这些线程可以重用以满足您的需要。ThreadPool类在维护应用中的最佳线程数量方面非常有效。因此,如果需要,您可以使用这个工具异步执行一些任务。

ThreadPool是包含一些static方法的静态类;他们中的一些人也有超载的版本。为了您的快速参考,图 27-1 是来自 Visual Studio IDE 的部分截图,显示了ThreadPool类中的方法。

img/463942_2_En_27_Fig1_HTML.jpg

图 27-1

Visual Studio 2019 IDE 中 ThreadPool 类的截图

在本节中,我们的重点是QueueUserWorkItem方法。图 27-1 显示该方法有两个重载版本。现在要了解这个方法的细节,让我们展开 Visual Studio 中的方法描述。例如,一旦展开此方法的第一个重载版本,您会注意到以下情况。

//
// Summary:
//     Queues a method for execution. The method executes when a thread //     pool thread becomes available.
//
// Parameters:
//   callBack:
//     A System.Threading.WaitCallback that represents the method to be //     executed.
//
// Returns:
//     true if the method is successfully queued; System.NotSupportedException
//     is thrown if the work item could not be queued.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     callBack is null.
//
//   T:System.NotSupportedException:
//     The common language runtime (CLR) is hosted, and the host does not //     support this action.
[SecuritySafeCritical]
public static bool QueueUserWorkItem(WaitCallback callBack);

如果您进一步研究方法参数,您会发现WaitCallBack是一个具有以下描述的委托。

//
// Summary:
//     Represents a callback method to be executed by a thread pool thread.
//
// Parameters:
//   state:
//     An object containing information to be used by the callback method.
[ComVisible(true)]
public delegate void WaitCallback(object state);

第二个重载版本的QueueUserWorkItem可以接受一个名为state的额外的object参数。内容如下。

public static bool QueueUserWorkItem(WaitCallback callBack, object state);

它告诉我们,使用这个重载版本,您可以通过这个参数向您的方法传递一些有价值的数据。在接下来的演示中,我使用了两个重载版本,这就是为什么在接下来的例子中,除了ExecuteMethodOne()ExecuteMethodTwo()(您在前面的演示中已经看到了)之外,我还引入了另一个名为ExecuteMethodThree()的方法,在这个方法中我传递了一个对象参数。

人们经常互换使用变量和形参这两个词。但是一个专业的程序员通常对此很挑剔。方法定义中使用的变量称为方法的参数。例如,如果您在一个类中看到如下所示的方法定义:

public void Sum(int firstNumber,int secondNumber)

你说 firstNumber 和 secondNumber 是方法 Sum 的参数。现在假设你有一个类的对象,比如 ob。因此,当您使用以下代码行调用该方法时:

ob.Sum(1,2)

你说 1 和 2 是传递给 Sum 方法的参数。

简而言之,你可以说我们将参数传递给一个方法,这些值被赋给方法参数。根据这些定义,我应该在我的注释中说,我已经将 10 作为参数传递给了ExecuteMethodThree。但是为了简单起见,程序员通常不会过多强调这些术语,而且你可能会看到这些术语可以互换使用。

演示 3

为了有效地使用QueueUserWorkItem方法,您需要使用一个匹配WaitCallBack委托签名的方法。在下面的演示中,我将两个方法放入一个ThreadPool中。在演示 1 和演示 2 中,ExecuteMethodTwo()没有接受任何参数。所以,如果你想按原样使用这个方法并把它传递给QueueUserWorkItem,你会得到下面的编译错误。

No overload for 'ExecuteMethodTwo' matches delegate 'WaitCallback'

因此,让我们用一个虚拟的object参数来修改ExecuteMethodTwo()方法,如下所示。(我保留了评论,供大家参考。)

/*
The following method's signature should match
the delegate WaitCallback.It is as follows:
public delegate void WaitCallback(object state)
*/
private static void ExecuteMethodTwo(object state)
{
  Console.WriteLine("--MethodTwo has started.");
  // Some small task
  Thread.Sleep(100);
  Console.WriteLine("--MethodTwo has finished.");
}

现在让我们介绍另一个名为ExecuteMethodThree(...)的方法,它真正使用了参数。该方法描述如下。

private static void ExecuteMethodThree(object number)
{
 Console.WriteLine("---MethodThree has started.");
 int upperLimit = (int)number;
 for (int i = 0; i < upperLimit; i++)
 {
  Console.WriteLine("---MethodThree prints 3.0{0}", i);
 }
 Thread.Sleep(100);
 Console.WriteLine("---MethodThree has finished.");
}

现在通过下面的演示和相应的输出。

using System;
using System.Threading;

namespace UsingThreadPool
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Asynchronous Programming using ThreadPool class.***");

            // Using Threadpool
            // Not passing any argument to ExecuteMethodTwo
            ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteMethodTwo));
            /*
             Passing 10 as the argument to
             ExecuteMethodThree.
            */
            ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteMethodThree), 10);
            ExecuteMethodOne();

            Console.WriteLine("End Main().");
            Console.ReadKey();
        }

        private static void ExecuteMethodOne()
        {
            Console.WriteLine("-MethodOne has started.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("-MethodOne has finished.");
        }

        /*
        The following method's signature should match
        the delegate WaitCallback.It is as follows:
        public delegate void WaitCallback(object state)
        */
        private static void ExecuteMethodTwo(object state)
        {
            Console.WriteLine("--MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("--MethodTwo has finished.");
        }
        /*
        The following method has a parameter.
        This method's signature also matches the WaitCallBack
        delegate signature.
        */
        private static void ExecuteMethodThree(object number)
        {
            Console.WriteLine("---MethodThree has started.");
            int upperLimit = (int)number;
            for (int i = 0; i < upperLimit; i++)
            {
                Console.WriteLine($"---MethodThree prints 3.0{i}");
            }
            Thread.Sleep(100);
            Console.WriteLine("---MethodThree has finished.");
        }
    }
}

输出

下面是一个可能的输出。

***Asynchronous Programming using ThreadPool class.***
-MethodOne has started.
--MethodTwo has started.
---MethodThree has started.
---MethodThree prints 3.00
---MethodThree prints 3.01
---MethodThree prints 3.02
---MethodThree prints 3.03
---MethodThree prints 3.04
---MethodThree prints 3.05
---MethodThree prints 3.06
---MethodThree prints 3.07
---MethodThree prints 3.08
---MethodThree prints 3.09
--MethodTwo has finished.
---MethodThree has finished.
-MethodOne has finished.
End Main().

问答环节

27.2 使用简单的委托实例化技术,如果我使用下面的第一行而不是第二行,应用会编译并运行吗?

ThreadPool.QueueUserWorkItem(ExecuteMethodTwo);

thread pool . queue user work item(new waiting callback(executemethod two));

是的,但是既然你现在正在学习使用WaitCallback委托,我使用了实例化的详细方法来引起你对它的特别注意。

将 Lambda 表达式与 ThreadPool 类一起使用

如果您喜欢 lambda 表达式,您可以在类似的上下文中使用它。例如,在前面的演示中,您可以使用 lambda 表达式替换ExecuteMethodThree(...),如下所示。

// Using lambda Expression
// Here the method needs a parameter(input).
// Passing 10 as an argument to ExecuteMethodThree
ThreadPool.QueueUserWorkItem((number) =>
{
  Console.WriteLine("--MethodThree has started.");
  int upperLimit = (int)number;
  for (int i = 0; i < upperLimit; i++)
  {
   Console.WriteLine("---MethodThree prints 3.0{0}", i);
  }
  Thread.Sleep(100);
  Console.WriteLine("--MethodThree has finished.");
  }, 10

);

因此,在前面的演示中,您可以注释掉下面的行,并用前面介绍的 lambda 表达式替换ExecuteMethodThree(...)

ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteMethodThree), 10);

如果您再次执行该程序,您会得到类似的输出。为了便于参考,我在演示 4 中展示了完整的实现。

演示 4

using System;
using System.Threading;

namespace UsingThreadPoolWithLambdaExpression
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Asynchronous Programming Demonstration.***");
            Console.WriteLine("***Using ThreadPool with Lambda Expression.***");

            // Using Threadpool
            // Not passing any parameter for ExecuteMethodTwo
            ThreadPool.QueueUserWorkItem(ExecuteMethodTwo);
            // Using lambda Expression
            // Here the method needs a parameter(input).
            // Passing 10 as an argument to ExecuteMethodThree
            ThreadPool.QueueUserWorkItem((number) =>
            {
                Console.WriteLine("--MethodThree has started.");
                int upperLimit = (int)number;
                for (int i = 0; i < upperLimit; i++)
                {
                    Console.WriteLine("---MethodThree prints 3.0{0}", i);
                }
                Thread.Sleep(100);
                Console.WriteLine("--MethodThree has finished.");
            }, 10

          );

            ExecuteMethodOne();
            Console.WriteLine("End Main().");
            Console.ReadKey();
        }
        /// <summary>
        /// ExecuteMethodOne()
        /// </summary>
        private static void ExecuteMethodOne()
        {
            Console.WriteLine("-MethodOne has started.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("-MethodOne has finished.");
        }

        /*
        The following method's signature should match
        the delegate WaitCallback.It is as follows:
        public delegate void WaitCallback(object state)
        */

        private static void ExecuteMethodTwo(Object state)
        {
            Console.WriteLine("--MethodTwo has started.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("--MethodTwo has finished.");
        }
    }
}

输出

下面是一个可能的输出。

***Asynchronous Programming Demonstration.***
***Using ThreadPool with Lambda Expression.***
--MethodTwo has started.
-MethodOne has started.
--MethodThree has started.
---MethodThree prints 3.00
---MethodThree prints 3.01
---MethodThree prints 3.02
---MethodThree prints 3.03
---MethodThree prints 3.04
---MethodThree prints 3.05
---MethodThree prints 3.06
---MethodThree prints 3.07
---MethodThree prints 3.08
---MethodThree prints 3.09
--MethodTwo has finished.
--MethodThree has finished.
-MethodOne has finished.
End Main().

Note

这次,您看到了带有ThreadPool类的 lambda 表达式。在演示 2 中,您看到了带有Thread类的 lambda 表达式。

使用 IAsyncResult 模式

我提到过IAsyncResult接口帮助你实现异步行为。我还告诉你,在同步模型中,如果你有一个名为XXX的同步方法,在异步版本中,你会看到对应同步方法的BeginXXXEndXXX方法。现在你可以看到这些细节了。

使用异步委托进行轮询

在演示 3 和演示 4 中,您看到了一个内置的WaitCallBack委托。通常,委托有许多不同的用途。在本节中,您将看到另一个重要的用法。让我们考虑一下轮询,这是一种重复检查条件的机制。在我们接下来的例子中,让我们检查一个委托实例是否完成了它的任务。

演示 5

这一次,我稍微修改了一下ExecuteMethodOne(...)ExecuteMethodTwo()方法。这些方法可以打印线程 id。这次,我允许ExecuteMethodOne(...)接受一个提供睡眠时间的int参数,而不是盲目地睡眠 1000 毫秒。

和以前的情况一样,ExecuteMethodTwo()只休眠了 100 毫秒,但是与ExecuteMethodTwo()相比,ExecuteMethodOne(...)需要更多的时间来完成它的任务。为了实现这一点,在本例中,我在ExecuteMethodOne(...)中传递了 3000 毫秒作为方法参数。

让我们看看代码的重要部分。现在我的ExecuteMethodOne如下:

// First Method
private static void ExecuteMethodOne(int sleepTimeInMilliSec)
{
  Console.WriteLine("MethodOne has started.");
  Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
  // Some big task
  Thread.Sleep(sleepTimeInMilliSec);
  Console.WriteLine("\nMethodOne has finished.");
}

为了匹配签名,我如下声明委托Method1Delegate

public delegate void Method1Delegate(int sleepTimeinMilliSec);

稍后我将它实例化如下。

Method1Delegate method1Del = ExecuteMethodOne;

到目前为止,一切都很简单。现在来看代码中最重要的一行,如下所示。

IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);

你还记得在委托的上下文中,你可以使用Invoke()方法吗?但是那一次你的代码遵循同步路径。现在你正在探索异步编程,所以你看到了BeginInvokeEndInvoke方法。当 C# 编译器看到 delegate 关键字时,它为动态生成的类提供这些方法。

BeginInvoke方法的返回类型是IAsyncResult。如果您将鼠标悬停在BeginInvoke上或者注意它的结构,您会看到虽然ExecuteMethodOne只接受一个参数,但是BeginInvoke方法总是接受两个额外的参数:一个类型为AsyncCallback和一个类型为object。你很快就会看到对它们的讨论。在这个例子中,我只使用了第一个参数,并将 3000 毫秒作为ExecuteMethodOne的参数。但是对于BeginInvoke的后两个参数,我传了null值。

BeginInvoke返回的结果很重要,我将结果保存在一个IAsyncResult对象中。IAsyncResult具有以下只读属性。

public interface IAsyncResult
{
 bool IsCompleted { get; }
 WaitHandle AsyncWaitHandle { get; }
 object AsyncState { get; }
 bool CompletedSynchronously { get; }
}

目前,我关注的是isCompleted属性。如果您进一步扩展这些定义,您会看到isCompleted的定义如下。

//
// Summary:
//     Gets a value that indicates whether the asynchronous  operation has //     completed.
//
// Returns:
//     true if the operation is complete; otherwise, false.
bool IsCompleted { get; }

因此,很明显,您可以使用这个属性来验证代理是否已经完成了它的工作。

在下面的例子中,我检查其他线程中的委托是否完成了它的工作。如果工作没有完成,我会在控制台窗口中打印星号(*),并强制主线程短暂休眠,这就是为什么您会在本演示中看到下面这段代码。

while (!asyncResult.IsCompleted)
{
    // Keep working in main thread
    Console.Write("*");
    Thread.Sleep(5);
}

最后,EndInvoke方法接受一个类型为IAsyncResult的参数。所以,我通过asyncResult作为这个方法中的一个参数。现在进行完整的演示。

using System;
using System.Threading;

namespace PollingDemoInDotNetFramework
{
    //WILL NOT WORK ON .NET CORE.
    //RUN THIS PROGRAM ON .NET FRAMEWORK.
    class Program
    {
        public delegate void Method1Delegate(int sleepTimeinMilliSec);
        static void Main(string[] args)
        {
            Console.WriteLine("***Polling Demo.Run it in .NET Framework.***");
            Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            // Synchronous call
            //ExecuteMethodOne(3000);

            Method1Delegate method1Del = ExecuteMethodOne;
            IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
            ExecuteMethodTwo();
            while (!asyncResult.IsCompleted)
            {
                // Keep working in main thread
                Console.Write("*");
                Thread.Sleep(5);
            }

            method1Del.EndInvoke(asyncResult);
            Console.ReadKey();
        }
        // First Method
        private static void ExecuteMethodOne(int sleepTimeInMilliSec)
        {
            Console.WriteLine("MethodOne has started.");
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(sleepTimeInMilliSec);
            Console.WriteLine("\nMethodOne has finished.");
        }
        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }

    }
}

输出

下面是一个可能的输出。

***Polling Demo.Run it in .NET Framework.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.

问答环节

27.3 上一个案例中, ExecuteMethodOne(...) 只带一个参数, BeginInvoke 带三个参数。那么,我是不是可以简单的说,如果 ExecuteMethodOne(...) 接受 n 数量的参数,那么 BeginInvoke 就有 n+2 参数?

是的,初始的参数集是基于您的方法的,但是对于最后两个参数,一个是类型AsyncCallback,的,最后一个是类型object.

Points to Remember

  • 这种类型的例子在。NET 框架 4.7.2。如果你在。NET Core 3.0,你会得到这个异常:System。PlatformNotSupportedException:“此平台上不支持操作。其中一个主要原因是异步委托实现依赖于中不存在的远程处理功能。NET 核心。关于这一点的详细论述可以在 https://github.com/dotnet/runtime/issues/16312 中找到。

  • 如果您不想在控制台窗口中检查和打印星号(*),您可以在主线程完成执行后简单地调用委托类型的EndInvoke()方法。EndInvoke()本身等待直到代理完成它的工作。

  • 如果你没有明确地检查代理是否完成了它的执行,或者你只是忘记调用EndInvoke(),代理的线程在主线程死亡后停止。例如,如果您注释掉前面示例中的以下代码段。

    //while (!asyncResult.IsCompleted)
    //{
    //    Keep working in main thread
    //    Console.Write("*");
    //    Thread.Sleep(5);
    //}
    //method1Del.EndInvoke(asyncResult);
    //Console.ReadKey();
    
    
  • BeginInvoke通过使用EndInvoke.帮助调用线程稍后获得异步方法调用的结果

And run the application again, you may NOT see the statement "MethodOne has finished."

使用 IAsyncResult 的 AsyncWaitHandle

你注意到了吗WaitHandle``AsyncWaitHandle``IAsyncResult?里面的{ get; }很重要,这一次,我向你展示了使用这个属性的另一种方法。如果你仔细观察,你会发现AsyncWaitHandle返回一个WaitHandle,,它有如下描述。

//
// Summary:
//     Gets a System.Threading.WaitHandle that is used to wait for an //     asynchronous operation to complete.
//
// Returns:
//     A System.Threading.WaitHandle that is used to wait for an //     asynchronous operation to complete.
WaitHandle AsyncWaitHandle { get; }

Visual Studio IDE 确认WaitHandle是一个等待对共享资源进行独占访问的抽象类。在WaitHandle中,你会看到有五个不同重载版本的WaitOne()方法,如下所示。

public virtual bool WaitOne(int millisecondsTimeout);
public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);
public virtual bool WaitOne(TimeSpan timeout);
public virtual bool WaitOne(TimeSpan timeout, bool exitContext);
public virtual bool WaitOne();

在接下来的演示中,我使用了第一个重载版本,并提供了一个可选的超时值,单位为毫秒。如果您展开该方法,您会看到以下与之相关联的摘要。

// Summary:
// Blocks the current thread until the current System.Threading.WaitHandle // receives a signal, using a 32-bit signed integer to specify the time // interval in milliseconds.
//(Some other details omitted)
public virtual bool WaitOne(int millisecondsTimeout);

因此,很明显,通过使用WaitHandle,,你可以等待一个委托线程完成它的工作。在下面的程序中,如果等待成功,控制将从while循环中退出。但是如果发生超时,WaitOne()返回 false,并且while循环继续并在控制台中打印星号(*)。

演示 6

using System;
using System.Threading;
//RUN THIS PROGRAM ON .NET FRAMEWORK.

namespace UsingWaitHandleInDotNetFramework
{
    class Program
    {
        public delegate void Method1Delegate(int sleepTimeinMilliSec);
        static void Main(string[] args)
        {
            Console.WriteLine("***Polling and WaitHandle Demo.***");
            Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            // Synchronous call
            //ExecuteMethodOne(3000);
            // Asynchrous call using a delegate
            Method1Delegate method1Del = ExecuteMethodOne;
            IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
            ExecuteMethodTwo();
            while (true)
            {
                // Keep working in main thread
                Console.Write("*");
                /*
                 There are 5 different overload method for WaitOne().Following method blocks the current thread until the
                 current System.Threading.WaitHandle receives a signal,using a 32-bit signed integer to specify the time interval in milliseconds.
                */
                if (asyncResult.AsyncWaitHandle.WaitOne(10))
                {
                    Console.Write("\nResult is available now.");
                    break;
                }
            }
            method1Del.EndInvoke(asyncResult);
            Console.WriteLine("\nExiting Main().");
            Console.ReadKey();
        }

        // First Method
        private static void ExecuteMethodOne(int sleepTimeInMilliSec)
        {
            Console.WriteLine("MethodOne has started.");
            // It will have a different thread id
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(sleepTimeInMilliSec);
            Console.WriteLine("\nMethodOne has finished.");
        }

        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }
    }
}

输出

这是一个可能的输出。

***Polling and WaitHandle Demo.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
*******************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.
***
Result is available now.
Exiting Main().

分析

如果您将这个演示与上一个进行比较,您将会看到异步操作以不同的方式完成。这次,你没有使用IsCompleted属性,而是使用了我向你展示的IAsyncResult.AsyncWaitHandle属性,这两种属性可以在不同的应用中看到。

使用异步回调

回顾一下前面两个演示中使用的BeginInvoke方法。让我们回顾一下我是如何使用它的。

// Asynchrous call using a delegate
Method1Delegate method1Del = ExecuteMethodOne;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);

这段代码显示,在BeginInvoke方法中,我为最后两个方法参数传递了两个null参数。如果您将鼠标悬停在这些先前演示的行上,您会注意到BeginInvoke期望一个IAsyncCallback委托作为第二个参数,在本例中期望一个object作为第三个参数。

让我们调查一下IAsyncCallback代表。Visual Studio IDE 告诉我们这个委托是在System命名空间中定义的,它有如下描述。

//
// Summary:
//     References a method to be called when a corresponding asynchronous //     operation completes.
//
// Parameters:
//   ar:
//     The result of the asynchronous operation.
  [ComVisible(true)]
  public delegate void AsyncCallback(IAsyncResult ar);

你可以使用一个callback方法来执行一些有用的东西(比如一些内务工作)。AsyncCallback委托有一个void返回类型,它接受一个IAsyncResult参数。所以,让我们定义一个可以匹配这个委托签名的方法,并在Method1Del实例完成执行后调用这个方法。下面是一个示例方法(姑且称之为ExecuteCallbackMethod),它将在接下来的演示中使用。

/*
It's a callback method.This method will be invoked
when Method1Delegate completes its work.
*/
private static void ExecuteCallbackMethod(IAsyncResult asyncResult)
{
 //if null you can throw some exception

    if (asyncResult != null)
    {
     Console.WriteLine("\nCallbackMethod has started.");
     Console.WriteLine($"Inside ExecuteCallbackMethod(...), Thread id {Thread.CurrentThread.ManagedThreadId} .");
     // Do some housekeeping work/ clean-up operation
     Thread.Sleep(100);
     Console.WriteLine("CallbackMethod has finished.");
    }
   }

演示 7

现在查看完整的实现。

using System;
using System.Threading;

namespace UsingAsynchronousCallback
{
    class Program
    {
        public delegate void Method1Delegate(int sleepTimeinMilliSec);
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Asynchronous Callback.***");
            Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);

            // Asynchrous call using a delegate
            Method1Delegate method1Del = ExecuteMethodOne;
            IAsyncResult asyncResult = method1Del.BeginInvoke(3000, ExecuteCallbackMethod, null);

            ExecuteMethodTwo();
            while (!asyncResult.IsCompleted)
            {
                // Keep working in main thread
                Console.Write("*");
                Thread.Sleep(5);
            }

            method1Del.EndInvoke(asyncResult);
            Console.WriteLine("Exit Main().");
            Console.ReadKey();
        }
        // First Method
        private static void ExecuteMethodOne(int sleepTimeInMilliSec)
        {
            Console.WriteLine("MethodOne has started.");
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(sleepTimeInMilliSec);
            Console.WriteLine("\nMethodOne has finished.");
        }

        // Second Method
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some small task
            Thread.Sleep(100);
            Console.WriteLine("MethodTwo has finished.");
        }

        /*
         It's a callback method.This method will be invoked
         when Method1Delegate instance completes its work.
         */
        private static void ExecuteCallbackMethod(IAsyncResult asyncResult)
        {
            if (asyncResult != null)//if null you can throw some exception
            {
                Console.WriteLine("\nCallbackMethod has started.");
                Console.WriteLine($"Inside ExecuteCallbackMethod(...),Thread id {Thread.CurrentThread.ManagedThreadId} .");
                // Do some housekeeping work/ clean-up operation
                Thread.Sleep(100);
                Console.WriteLine("CallbackMethod has finished.");
            }
        }
    }
}

输出

下面是一个可能的输出。

***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.

CallbackMethod has started.
Inside ExecuteCallbackMethod(...),Thread id 3 .
Exit Main().
CallbackMethod has finished.

分析

回调方法仅在ExecuteMethodOne完成执行后才开始工作。另外,注意ExecuteMethodOneExecuteCallbackMethod线程 id 是相同的。这是因为回调方法是从运行ExecuteMethodOne的线程中调用的。

问答环节

27.4 什么是 回调方法

通常,它是在特定操作完成后调用的方法。在异步编程中,当您不知道某个操作的确切完成时间,但希望在某个任务完成后开始一个新任务时,您经常会看到这种方法。例如,在前面的例子中,如果ExecuteMethodOne在其执行期间分配了一些资源,ExecuteCallbackMethod可以执行一些清理工作。

我发现回调方法不是从主线程调用的。是否在意料之中?

是的。在这个例子中,ExecuteCallbackMethod是回调方法,它只有在ExecuteMethodOne完成工作后才能开始执行。因此,从运行ExecuteMethodOne的同一个线程中调用ExecuteCallbackMethod是有意义的。

我可以在这个例子中使用 lambda 表达式吗?

接得好。为了获得类似的输出,在前面的演示中,没有创建一个新的ExecuteCallbackMethod方法并使用下面的代码行,

IAsyncResult asyncResult = method1Del.BeginInvoke(3000, ExecuteCallbackMethod, null);

您可以使用 lambda 表达式替换它,如下所示。

IAsyncResult asyncResult = method1Del.BeginInvoke(3000,
 (result) =>
{
    if (result != null)//if null you can throw some exception
    {
        Console.WriteLine("\nCallbackMethod has started.");
        Console.WriteLine($"Inside ExecuteCallbackMethod(),Thread id { Thread.CurrentThread.ManagedThreadId }.");
        // Do some housekeeping work/ clean-up operation
        Thread.Sleep(100);
        Console.WriteLine("CallbackMethod has finished.");
    }
 },
null);

27.7 我看到你在 BeginInvoke 方法内部使用回调方法的时候,没有传递一个对象作为最终参数,而是传递了一个空值。这有什么具体原因吗?

不,我没有在这些演示中使用该参数。因为它是一个对象参数,所以可以传递任何对你有意义的东西。使用回调方法时,可以传递委托实例本身。它可以帮助您的回调方法分析异步方法的结果。

但是为了简单起见,让我们修改前面的演示并传递一个字符串消息作为BeginInvoke中的最后一个参数。让我们假设现在您正在修改现有的代码行

IAsyncResult asyncResult = method1Del.BeginInvoke(3000,ExecuteCallbackMethod, null);

有了下面这个。

IAsyncResult asyncResult = method1Del.BeginInvoke(3000, ExecuteCallbackMethod, "Method1Delegate, Thank you for using me." );

To accommodate this change, lets modify the ExecuteCallbackMethod() method too.The newly added lines are shown in bold.
private static void ExecuteCallbackMethod(IAsyncResult asyncResult)
{
   if (asyncResult != null)//if null you can throw some exception
    {
     Console.WriteLine("\nCallbackMethod has started.");
     Console.WriteLine($"Inside ExecuteCallbackMethod(...),Thread id { Thread.CurrentThread.ManagedThreadId} .");
     // Do some housekeeping work/ clean-up operation
     Thread.Sleep(100);
     // For Q&A 27.7
     string msg = (string)asyncResult.AsyncState;
     Console.WriteLine($"Callback method says : ‘{msg}’");
     Console.WriteLine("CallbackMethod has finished.");
     }
  }
If you run the program again, this time you can see the following output which conforms the new string message:

***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
MethodTwo has started.
Inside ExecuteMethodTwo(),Thread id 1.
MethodOne has started.
Inside ExecuteMethodOne(),Thread id 3.
MethodTwo has finished.
********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
MethodOne has finished.

CallbackMethod has started.
Exit Main().
Inside ExecuteCallbackMethod(...),Thread id 3 .
Callback method says : `Method1Delegate, Thank you for using me.'
CallbackMethod has finished.

Points to Remember

您已经看到了使用委托实现轮询、等待句柄和异步回调。这种编程模型可以在。NET 框架也有,比如HttpWebRequest类的BeginGetResponseBeginGetRequestStream或者SqlCommand类的BeginExecuteNonQuery(), BeginExecuteReader()BeginExecuteXmlReader()。这些方法也有重载版本。

使用基于事件的异步模式

在本节中,您将看到基于事件的异步模式的使用,这种模式最初很难理解。根据应用的复杂性,这种模式可以有多种形式。以下是这种模式的一些关键特征。

  • 一般来说,异步方法可以是其同步版本的副本,但是当您调用它时,它在一个单独的线程上启动,然后立即返回。这种机制允许您调用一个线程来继续,而预期的操作在后台运行。这些操作的例子可以是长时间运行的过程,例如加载大图像、下载大文件、连接、建立到数据库的连接等等。基于事件的异步模式在这些情况下很有帮助。例如,一旦长时间运行的下载操作完成,就可以引发一个事件来通知信息。事件的订阅者可以根据该通知立即采取行动。

  • 您可以同时执行多个方法,并在每个方法完成时收到通知。

  • 使用这种模式,您可以利用多线程,但同时也隐藏了整体的复杂性。

  • 在最简单的情况下,您的方法名有一个Async后缀,告诉其他人您正在使用该方法的异步版本。同时,您有一个带有Completed后缀的相应事件。在理想情况下,您应该有一个相应的 cancel 方法,并且它应该支持显示进度条/报告。支持取消操作的方法也可以命名为MethodNameAsyncCancel(或者简称为CancelAsync)。

  • SoundPlayer、PictureBox、WebClient 和 BackgroundWorker 等组件通常是这种模式的代表。

我使用 WebClient 制作了一个简单的应用。我们来看看。

演示 8

在程序的开始,您会看到我需要包含一些特定的名称空间。我用注释告诉你它们在这个演示中的重要性。

在本例中,我想将一个文件下载到我的本地系统中。但是我没有使用来自互联网的真实 URL,而是将源文件存储在本地系统中。这有两个主要好处。

  • 运行此应用不需要互联网连接。

  • 由于您没有使用互联网连接,下载操作相对较快。

现在,在看到完整的示例之前,请看下面的代码块。

WebClient webClient = new WebClient();
// File location
Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
// Target location for download
string targetLocation = @"C:\TestData\downloaded_file.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

到目前为止,事情简单明了。但是我提请你注意下面几行代码。

webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

你可以看到在第一行中,我使用了一个在WebClient中定义的叫做DownloadFileAsync的方法。在 Visual Studio 中,方法描述告诉我们以下内容。

// Summary:
//     Downloads, to a local file, the resource with the specified URI. This method does not block the calling thread.
//
// Parameters:
//   address:
//     The URI of the resource to download.
//
//   fileName:
//     The name of the file to be placed on the local computer.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     The address parameter is null. -or- The fileName parameter is null.
//
//   T:System.Net.WebException:
//     The URI formed by combining System.Net.WebClient.BaseAddress and address is invalid.
//     -or- An error occurred while downloading the resource.
//
//   T:System.InvalidOperationException:
//     The local file specified by fileName is in use by another thread.
public void DownloadFileAsync(Uri address, string fileName);

使用此方法时,调用线程不会被阻塞。(实际上,DownloadFileAsyncDownloadFile方法的异步版本,也是在 WebClient .中定义的)

现在我们来看下一行代码。

webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

Visual Studio 对DownloadFileCompleted事件描述如下。

/ Summary:
//     Occurs when an asynchronous file download operation completes.
public event AsyncCompletedEventHandler DownloadFileCompleted;

它对AsyncCompletedEventHandler进一步描述如下。

// Summary:
//     Represents the method that will handle the MethodNameCompleted event
//     of an asynchronous operation.
//
// Parameters:
//   sender:
//     The source of the event.
//
//   e:
//     An System.ComponentModel.AsyncCompletedEventArgs that contains the //     event data.
public delegate void AsyncCompletedEventHandler(object sender, AsyncCompletedEventArgs e);

您可以订阅DownloadFileCompleted事件来显示下载操作完成的通知。为此,我使用了以下方法。

private static void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
    Console.WriteLine("Successfully downloaded the file now.");
}

Note

DownloadCompleted方法匹配AsyncCompletedEventHandler委托的签名。

我假设您在运行这个应用之前已经掌握了委托和事件的概念。你知道我可以替换这一行代码。

webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);

使用下面的代码行。

webClient.DownloadFileCompleted += DownloadCompleted;

但是为了更好的可读性,我喜欢保留长版本。现在查看完整的示例和输出。

using System;
// For AsyncCompletedEventHandler delegate
using System.ComponentModel;
using System.Net; // For WebClient
using System.Threading; // For Thread.Sleep() method

namespace UsingWebClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Event Based Asynchronous Program Demo.***");
            // Method1();
            #region The lenghty operation(download)
            Console.WriteLine("Starting a download operation.");
            WebClient webClient = new WebClient();
            // File location
            Uri myLocation = new Uri(@"C:\TestData\OriginalFile.txt");
            // Target location for download
            string targetLocation = @"C:\TestData\DownloadedFile.txt";
            webClient.DownloadFileAsync(myLocation, targetLocation);
            webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
            #endregion
            ExecuteMethodTwo();
            Console.WriteLine("End Main()...");
            Console.ReadKey();
        }
        // ExecuteMethodTwo
        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("MethodTwo has started.");
            // Some very small task
            Thread.Sleep(10);
            Console.WriteLine("MethodTwo has finished.");
        }

        private static void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
        {
            Console.WriteLine("Successfully downloaded the file now.");
        }
    }
}

输出

下面是一个可能的输出。

***Event Based Asynchronous Program Demo.***
Starting a download operation.
MethodTwo has started.
MethodTwo has finished.
End Main()...
Successfully downloaded the file now.

分析

您可以看到下载操作是在ExecuteMethodTwo()开始执行之前开始的。然而,ExecuteMethodTwo()在下载操作完成之前完成了它的任务。如果你对Original.txt的内容感兴趣,这里有。

Dear Reader,
This is my test file.It is originally stored at C:\TestData in my system.

您可以使用类似的文件和内容进行测试,以便在您的终端进行快速验证。

附加说明

当你引入一个进度条时,你可以使这个例子更好。在这种情况下,您可以使用 Windows 窗体应用来获得对进度条的内置支持。我们暂且忽略ExecuteMethodTwo(),单独关注异步下载操作。你可以做一个基本的表单,如图 27-2 所示,包含三个简单的按钮和一个进度条。(你需要先将这些控件拖放到你的表单上,并将其命名为如图 27-2 所示。我假设你知道这些简单的活动。)

img/463942_2_En_27_Fig2_HTML.jpg

图 27-2

一个简单的 UI 应用,演示基于事件的异步

下面这段代码是不言自明的。

using System;
using System.ComponentModel;
using System.Net;
using System.Windows.Forms;

namespace UsingWebClentWithWinForm
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void StartDownload_Click(object sender, EventArgs e)
        {
         WebClient webClient = new WebClient();
         Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
         string targetLocation = @"C:\TestData\downloaded_file.txt";
         webClient.DownloadFileAsync(myLocation, targetLocation);
         webClient.DownloadFileCompleted += new      AsyncCompletedEventHandler(DownloadCompleted);
         webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
         MessageBox.Show("Executed download operation.");
    }
    private void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
    {
         MessageBox.Show("Successfully downloaded the file now.");
    }
    private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
    {
         progressBar.Value = e.ProgressPercentage;
    }

    private void ResetButton_Click(object sender, EventArgs e)
    {
         progressBar.Value = 0;
    }

    private void ExitButton_Click(object sender, EventArgs e)
    {
        this.Close();
    }
    }
}

Note

您可以从 Apress 网站下载该应用的完整代码。

输出

一旦你点击StartDownloadButton,你会得到如图 27-3 和图 27-4 所示的输出。

img/463942_2_En_27_Fig3_HTML.jpg

图 27-3

UI 应用的运行时屏幕截图

点击 OK 按钮后,你会看到如图 27-4 所示的消息框。

img/463942_2_En_27_Fig4_HTML.jpg

图 27-4

当您单击“确定”按钮时,会弹出另一个消息框

问答环节

27.8 与基于事件的异步程序相关的有哪些优点和缺点?

**以下是与这种方法相关的一些常见的优点和缺点。

赞成的意见

  • 您可以调用一个长时间运行的方法并立即返回。当方法完成时,您可以得到一个通知,您可以有效地使用它。

骗局

  • 因为你有分离的代码,所以理解、调试和维护通常很困难。

  • 当您订阅了一个事件,但后来忘记取消订阅时,就会出现一个大问题。这个错误会导致应用中的内存泄漏,影响可能非常严重;例如,您的系统挂起或没有响应,您需要经常重新启动它。

了解任务

要理解基于任务的异步模式(TAP),首先,你必须知道什么是任务。任务只是您想要执行的一个工作单元。您可以在同一个线程或不同的线程中完成这项工作。使用任务,您可以更好地控制线程;例如,您可以在特定任务完成后执行后续工作。父任务可以创建子任务,因此您可以组织层次结构。当你级联你的消息时,这种层次结构是很重要的。考虑一个例子。在您的应用中,一旦父任务被取消,子任务也应该被取消。

您可以用不同的方式创建任务。在下面的演示中,我用三种不同的方式创建了三个任务。以下代码段有支持注释。

#region Different ways to create and execute task
// Using constructor
Task taskOne = new Task(MyMethod);
taskOne.Start();
// Using task factory
TaskFactory taskFactory = new TaskFactory();
// StartNew Method creates and starts a task.
// It has different overloaded version.
Task taskTwo = taskFactory.StartNew(MyMethod);
// Using task factory via a task
Task taskThree = Task.Factory.StartNew(MyMethod);
#endregion

你可以看到所有三个任务(taskOne, taskTwo, taskThree)都试图做一个相似的操作:它们只是执行MyMethod(),描述如下。

private static void MyMethod()
{
    Console.WriteLine("Task.id={0} with Thread id {1} has started.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
    // Some task
    Thread.Sleep(100);
    Console.WriteLine("MyMethod for Task.id={0} and Thread id {1} is completed.", Task.CurrentId,  Thread.CurrentThread.ManagedThreadId);
    }

你可以看到在MyMethod()里面,为了区分任务和线程,我在控制台里打印了它们对应的 id。除此之外,我将方法名作为参数传递给了StartNew()方法。这个方法有 16 个重载版本(在撰写本文时),我使用的是如下定义的那个版本。

//
// Summary:
//     Creates and starts a task.
//
// Parameters:
//   action:
//     The action delegate to execute asynchronously.
//
// Returns:
//     The started task.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     The action argument is null.
public Task StartNew(Action action);

因为在这种情况下,MyMethod()匹配Action委托的签名,所以我对StartNew使用这个方法没有问题。

Points to Remember

让我们回忆一下行动代表背后的理论,供你参考。下面代码的方法总结:

    public delegate void Action();

它封装了一个没有参数也不返回值的方法。

在接下来的例子(演示 9)中,你看到MyMethod()不接受任何参数,它的返回类型是 void 这就是为什么我可以在StartNew()中使用方法名。

但是需要注意的是,在高级编程中,您经常会看到通用版本的动作委托。我从我的书高级 C# 中选择了以下几行(2020 年出版):

动作委托可以接受 1 到 16 个输入参数,但没有返回类型。重载版本如下:

Action<in T>
Action<in T1,in T2>
Action<in T1,in T2, in T3>
....
Action<in T1, in T2, in T3,in T4, in T5, in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>

例如,如果您有一个名为CalculateSumOfThreeInts的方法,它将三个 int 作为输入参数,其返回类型为 void,如下所示:

private static void CalculateSumOfThreeInts(int i1, int i2, int i3)
{
    int sum = i1 + i2 + i3;
    Console.WriteLine("Sum of {0},{1} and {2} is: {3}", i1, i2, i3, sum);
}

您可以使用动作委托来获取三个整数的和,如下所示:

Action<int, int, int> sum = new Action<int, int, int>( CalculateSumOfThreeInts);
sum(10, 3, 7);

否则,您可以使用如下简称:

Action<int, int, int> sum = CalculateSumOfThreeInts;
sum(10, 3, 7);

演示 9

现在进行完整的演示和输出。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace DifferentWaysToCreateTask
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using different ways to create tasks.****");
            Console.WriteLine($"Inside Main().Thread ID:{Thread.CurrentThread.ManagedThreadId}");

            #region Different ways to create and execute task
            // Using constructor.
            Task taskOne = new Task(MyMethod);
            taskOne.Start();
            // Using task factory.
            TaskFactory taskFactory = new TaskFactory();
            // StartNew Method creates and starts a task.
            // It has different overloaded versions.
            Task taskTwo = taskFactory.StartNew(MyMethod);
            // Using task factory via a task.
            Task taskThree = Task.Factory.StartNew(MyMethod);
            #endregion
            Console.ReadKey();
        }

        private static void MyMethod()
        {
            Console.WriteLine($"Task.id={Task.CurrentId} with Thread id {Thread.CurrentThread.ManagedThreadId} has started.");
            Thread.Sleep(100);
            Console.WriteLine($"MyMethod for Task.id={Task.CurrentId} and Thread id {Thread.CurrentThread.ManagedThreadId} is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Using different ways to create tasks.****
Inside Main().Thread ID:1
Task.id=3 with Thread id 6 has started.
Task.id=2 with Thread id 4 has started.
Task.id=1 with Thread id 5 has started.
MyMethod for Task.id=3 and Thread id 6 is completed.
MyMethod for Task.id=1 and Thread id 5 is completed.
MyMethod for Task.id=2 and Thread id 4 is completed.

Note

ManagedThreadId获取一个特定托管线程的唯一标识符 only 。在您的机器上运行应用时,您可能会注意到一个不同的值。所以,你不应该觉得既然你已经创建了 n 个线程,你应该只看到 1 到 n 之间的线程 id。可能有其他线程在后台运行。

问答环节

27.9 StartNew() 可用于匹配动作委托签名的方法。这是正确的吗?

一点也不。我在一个接受参数的StartNew重载中使用了它,参数是匹配动作委托签名的方法的名称。但是,还有其他过载版本的StartNew;例如,考虑下面的例子,你可以看到Func代表。

public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, TaskCreationOptions creationOptions);

或者,

public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, CancellationToken cancellationToken);

27.10 在之前的一个 Q & A 中,我看到了 TaskCreationOptions 。这是什么意思?

这是一个enum。您可以使用它来设定任务的行为。这是它的细节。

public enum TaskCreationOptions
{
        None = 0,
        PreferFairness = 1,
        LongRunning = 2,
        AttachedToParent = 4,
        DenyChildAttach = 8,
        HideScheduler = 16,
        RunContinuationsAsynchronously = 64,
}

在接下来的演示中,您会看到另一个重要的叫做TaskContinuationOptionsenum,它也可以帮助您设置任务行为。

使用基于任务的异步模式(TAP)

基于任务的异步模式(TAP)来自 C# 4.0。它是 C# 5.0 中的async/await的基础。TAP 引入了Task类,当异步代码块的返回值不是大问题时,使用它的通用变体Task<TResult>. Task。但是当你真的关心这个返回值时,你应该使用通用版本,Task<TResult>.你已经对Task有了一个大概的了解。让我们使用这个概念,使用ExecuteMethodOne()ExecuteMethodTwo() .实现一个基于任务的异步模式

演示 10

这是一个完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace UsingTAP
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Task-based Asynchronous Pattern.****");
            Console.WriteLine($"Inside Main().The thread ID:{Thread.CurrentThread.ManagedThreadId}");
            Task taskForMethod1 = new Task(ExecuteMethodOne);
            taskForMethod1.Start();
            ExecuteMethodTwo();
            Console.ReadKey();
        }

        private static void ExecuteMethodOne()
        {
            Console.WriteLine("Method1 has started.");
            Console.WriteLine($"Inside ExecuteMethodOne(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("Method1 has completed its job now.");
        }

        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("Method2 has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            Thread.Sleep(100);
            Console.WriteLine("Method2 is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Using Task-based Asynchronous Pattern.****
Inside Main().The thread ID:1
Method2 has started.
Inside ExecuteMethodTwo(),Thread id 1.
Method1 has started.
Inside ExecuteMethodOne(),Thread id 4.
Method2 is completed.
Method1 has completed its job now.

您刚刚看到了一个基于任务的异步模式的示例演示。我不关心ExecuteMethodOne().的返回值,但是假设你对ExecuteMethodOne()是否成功执行感兴趣。为了简单起见,在接下来的例子中,我使用了一个string消息来表示成功完成。这次,你会看到Task,的一个通用变体,在这个例子中是Task<string>。对于 lambda 表达式爱好者,我在本例中用 lambda 表达式修改了ExecuteMethodOne(),为了满足关键需求,我调整了返回类型。

在这个例子中,我添加了另一个名为ExecuteMethodThree()的方法。为了比较,这个方法最初被注释掉;执行程序,并分析输出。稍后,我取消了对它的注释,并使用方法创建了一个任务层次结构。一旦完成,程序再次执行,你会注意到当ExecuteMethodOne()完成它的任务时ExecuteMethodThree()开始运行。我保留了这些评论来帮助你理解。

现在进行演示 11。

演示 11

这是一个完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TAPDemonstration2
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using Task-based Asynchronous Pattern.Using lambda expression into it.****");
            Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
            // Task taskForMethod1 = new Task(Method1);
            // taskForMethod1.Start();
            Task<string> taskForMethod1 = ExecuteMethodOne();
            /*
             Wait for task to complete.
             If you use Wait() method as follows, you'll not see the  asynchonous behavior.
             */
            // taskForMethod1.Wait();
            // Continue the task
            // The taskForMethod3 will continue once taskForMethod1 is // finished
            // Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree, TaskContinuationOptions.OnlyOnRanToCompletion);
            ExecuteMethodTwo();
            Console.WriteLine($"Task for Method1 was a : {taskForMethod1.Result}");
            Console.ReadKey();
        }
        // Using lambda expression
        private static Task<string> ExecuteMethodOne()
        {
            return Task.Run(() =>
            {
                string result = "Failure";
                try
                {
                    Console.WriteLine("Method1 has started.");
                    Console.WriteLine($"Inside Method1(),Task.id={Task.CurrentId}");
                    Console.WriteLine($"Inside Method1(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
                    //Some big task
                    Thread.Sleep(1000);
                    Console.WriteLine("Method1 has completed its job now.");
                    result = "Success";
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Exception caught:{0}", ex.Message);
                }
                return result;
            }
            );
        }

        private static void ExecuteMethodTwo()
        {
            Console.WriteLine("Method2 has started.");
            Console.WriteLine($"Inside ExecuteMethodTwo(),Thread id {Thread.CurrentThread.ManagedThreadId}.");
            Thread.Sleep(100);
            Console.WriteLine("Method2 is completed.");
        }
        private static void ExecuteMethodThree(Task task)
        {
            Console.WriteLine("Method3 starts now.");
            Console.WriteLine($"Task.id is:{Task.CurrentId} with Thread id is:{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(20);
            Console.WriteLine($"Method3 with Task.id {Task.CurrentId} and Thread id {Thread.CurrentThread.ManagedThreadId} is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2 has started.
Inside ExecuteMethodTwo(),Thread id 1.
Method1 has started.
Inside Method1(),Task.id=1
Inside Method1(),Thread id 4.
Method2 is completed.
Method1 has completed its job now.
Task for Method1 was a : Success

分析

你有没有注意到,这一次,我没有对taskForMethod1使用Start()方法?相反,我使用了Task类中的Run()方法来执行Method1().,我为什么要这么做呢?嗯,在Task类里面,Run是一个静态方法。Visual Studio 中的方法总结对这个Run方法做了如下陈述:"Queues the specified work to run on the thread pool and returns a System.Threading.Tasks.Task1 object that represents that work."`在编写的时候,这个方法有八个重载版本,如下。

public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<TResult> function);
public static Task<TResult> Run<TResult>(Func<TResult> function, CancellationToken cancellationToken);
public static Task Run(Func<Task> function);
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);

现在检查这个例子中的另一个要点。如果取消对下面一行的注释,

// Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree, TaskContinuationOptions.OnlyOnRanToCompletion);

并再次运行该应用,您会得到类似如下的输出。

***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2 has started.
Inside ExecuteMethodTwo(),Thread id 1.
Method1 has started.
Inside Method1(),Task.id=1
Inside Method1(),Thread id 4.
Method2 is completed.
Method1 has completed its job now.
Task for Method1 was a : Success
Method3 starts now.
Task.id is:2 with Thread id is:5
Method3 with Task.id 2 and Thread id 5 is completed.

你可以看到ContinueWith()方法有助于继续一项任务。您可能还会注意到以下内容。

TaskContinuationOptions.OnlyOnRanToCompletion

它只是声明当taskForMethod1完成它的工作时,任务将继续。同样,您可以使用enum TaskContinuationOptions选择其他选项,其描述如下。

public enum TaskContinuationOptions
{
    None = 0,
    PreferFairness = 1,
    LongRunning = 2,
    AttachedToParent = 4,
    DenyChildAttach = 8,
    HideScheduler = 16,
    LazyCancellation = 32,
    RunContinuationsAsynchronously = 64,
    NotOnRanToCompletion = 65536,
    NotOnFaulted = 131072,
    OnlyOnCanceled = 196608,
    NotOnCanceled = 262144,
    OnlyOnFaulted = 327680,
    OnlyOnRanToCompletion = 393216,
    ExecuteSynchronously = 524288
}

问答环节

27.11 我可以一次分配多项任务吗?

是的,你可以。例如,在前面修改的示例中,如果您有另一个名为 Execute MethodFour的方法,描述如下。

private static void ExecuteMethodFour(Task task)
{
    Console.WriteLine("Method4 starts now.");
    Console.WriteLine($"Task.id is:{ Task.CurrentId } with Thread id is :{ Thread.CurrentThread.ManagedThreadId } ");
            Thread.Sleep(10);
    Console.WriteLine($"Method4 with Task.id { Task.CurrentId } and Thread id { Thread.CurrentThread.ManagedThreadId } is completed."); ,
}

你可以写下面几行。

Task<string> taskForMethod1 = Method1();
Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree, TaskContinuationOptions.OnlyOnRanToCompletion);
 taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodFour, TaskContinuationOptions.OnlyOnRanToCompletion);

这意味着一旦taskForMethod1完成任务,你会看到taskForMethod3,的继续工作,它执行ExecuteMethodThreeExecuteMethodFour

还需要注意的是,一个延续作品可以有另一个延续作品。举个例子,如果你想要下面这样的东西。

  • 一旦 taskForMethod1 完成,则继续 taskForMethod3 和

  • 一旦 taskForMethod3 完成,就只能继续 taskForMethod4

你可以写类似下面的东西。

// Method1 starts
Task<string> taskForMethod1 = Method1();
// Task taskForMethod3 starts after taskForMethod1
Task taskForMethod3 = taskForMethod1.ContinueWith(ExecuteMethodThree,
TaskContinuationOptions.OnlyOnRanToCompletion);
// Task taskForMethod4 starts after taskForMethod3
Task taskForMethod4 = taskForMethod3.ContinueWith(ExecuteMethodFour, TaskContinuationOptions.OnlyOnRanToCompletion);

使用 async 和 await 关键字

asyncawait关键字使点击模式非常灵活。从本章开始,我使用了两种方法。第一种方法是长时间运行的方法,比第二种方法需要更多的时间来完成。在接下来的例子中,我将继续使用类似的方法进行案例研究。为简单起见,我们分别称它们为Method1()Method2(),

最初,我使用 d 一个非 lambda 版本,但是在分析部分,我使用了代码的 lambda 表达式变体首先,我们再来看看Method1()

private static void Method1()
{
    Console.WriteLine("Method1 has started.");
    Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
    // Some big task
    Thread.Sleep(1000);
    Console.WriteLine("Method1 has completed its job now.");
}

当您使用 lambda 表达式并使用async/await对时,您的代码可能如下所示。

// Using lambda expression
private static async Task Method1()
{
    await Task.Run(() =>
    {
        Console.WriteLine("Method1 has started.");
        Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
        // Some big task
        Thread.Sleep(1000);
        Console.WriteLine("Method1 has completed its job now.");
    }
    );
}

你注意到一个有趣的事实了吗?同步版本和异步版本的方法体非常相似。但是许多早期实现异步编程的解决方案并不是这样的。(它们也很复杂。)

那么,await是做什么的呢?当你分析代码时,你会发现一旦你得到一个await,调用线程就会跳出这个方法,继续做别的事情。

在接下来的演示中,我使用了Task.Run,,它导致异步调用在一个单独的线程上继续。这并不意味着延续工作应该总是在一个新的线程上完成,因为有时你并不担心不同的线程;例如,当您的呼叫等待通过网络建立连接以下载某些内容时。

最后,在 nonlambda 版本(演示 12)中,我使用了下面的代码块。

private static async Task ExecuteTaskOne()
{
    await Task.Run(Method1);
}

而在Main()内部,我没有调用Method1(),而是用ExecuteTaskOne()异步执行Method1()。你可以看到我在Run方法中传递了方法名Method1。您可以看出我在这里使用了最短的重载版本的Run方法。因为Method1匹配Action委托的签名,所以您可以在Task类的Run方法中传递这个方法名作为参数。

演示 12

这是完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace UsingAsyncAwait
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
            Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
            /*
             This call is not awaited.So,the current method
             continues before the call is completed.
             i.e., following async call is not awaited.
             */
            ExecuteTaskOne();
            Method2();
            Console.ReadKey();
        }

        private static async Task ExecuteTaskOne()
        {
            await Task.Run(Method1);
        }
        private static void Method1()
        {
            Console.WriteLine("Method1() has started.");
            Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            // Some big task
            Thread.Sleep(1000);
            Console.WriteLine("Method1() has completed its job now.");
        }

        private static void Method2()
        {
            Console.WriteLine("Method2() has started.");
            Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            //Some small task
            Thread.Sleep(100);
            Console.WriteLine("Method2() is completed.");
        }
    }
}

Note

我建议您在 Visual Studio 2019 的最新版本中执行基于任务的异步程序,以避免一些错误行为,这些行为在 Visual Studio 的旧版本中出现过。

输出

下面是一个可能的输出。

***Exploring task-based asynchronous pattern(TAP) using async and await.****
Inside Main().Thread ID:1
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.

分析

在前面的输出中,您可以看到Method1()被提前调用,但是Method2()的执行并没有因此而被阻塞。请注意,此输出可能会有所不同。所以,在某些情况下,你可能还会看到Method2()Method1()之前开始。所以,如果你想让Method1()先开始,你可以在Method2()执行之前放一个小的Sleep()。您可以看到Method2()在主线程中运行,而Method1()在不同的线程中执行。

如果你喜欢使用 lambda 表达式,你可以替换下面的代码段

private static async Task ExecuteTaskOne()
{
        await Task.Run(Method1);
}

private static void Method1()
{
        Console.WriteLine("Method1() has started.");
        Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
        // Some big task
        Thread.Sleep(1000);
        Console.WriteLine("Method1() has completed its job now.");
}

用这个。

// Using lambda expression
private static async Task ExecuteMethod1()
{
    await Task.Run(() =>
    {
           Console.WriteLine("Method1() has started.");
           Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
           // Some big task
           Thread.Sleep(1000);
           Console.WriteLine("Method1() has completed its job now.");
        }
    );
}

现在在前面的演示中,您可以直接调用ExecuteMethod1()方法来获得类似的输出,而不是调用ExecuteTaskOne()

Note

在前面的示例中,您会看到下面一行的警告消息:ExecuteMethod1();,它告诉您以下内容:

Warning CS4014 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

如果您将鼠标悬停在此处,您会得到两个建议:其中一个建议告诉您应用丢弃,如下所示。

_ = ExecuteMethod1(); // applying discard

Note

从 C# 7.0 开始就支持丢弃。这些是应用中临时的、虚拟的和未使用的变量。因为这些变量可能不在已分配的存储上,所以它们可以减少内存分配。这些变量可以增强可读性和可维护性。使用下划线(_)来表示应用中的丢弃变量。

但是如果您遵循第二个建议,在该行之前插入await,如下所示。

await ExecuteMethod1();

编译器会引发另一个错误,内容如下。

Error CS4033 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.

为了消除这个错误,你需要使包含async的方法(也就是说,现在你从下面一行开始。

static async Task Main(string[] args)

在应用了async/await对之后,Main()方法可能如下所示。

class Program
{
    // static void Main(string[] args)
    static async Task Main(string[] args)
    {
        Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
        Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
        await ExecuteMethod1();
        // remaining code

这个整体的讨论是为了提醒你,要把async/await一起应用,并适当放置。

我用最后一个演示结束了这一章,这一次,我稍微修改了应用的调用顺序。现在我介绍另一种叫做Method3(),的方法,类似于Method2()。这个新添加的方法可以从ExecuteTaskOne(),中调用,其结构如下。

private static async Task ExecuteTaskOne()
{
        Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
        int value=await Task.Run(Method1);
        Console.WriteLine("ExecuteTaskOne(), after await() call.");
        // Method3 will be called if Method1 executes successfully
        if (value = = 0)
        {
             Method3();
        }
}

看一下前面的代码段。它只是说我想从Method1(),获取返回值,并基于该值,我决定是否调用Method3()。所以,这一次,Method1()的返回类型不是void;相反,它返回一个int ( 0表示成功完成,否则为-1),这个方法用如下的try-catch块重新构造。

private static int Method1()
{
    int flag = 0;
    try
    {
           Console.WriteLine("Method1() has started.");
           Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
           // Some big task
           Thread.Sleep(1000);
           Console.WriteLine("Method1() has completed its job now.");
 }
 catch (Exception e)
 {
        Console.WriteLine("Caught Exception {0}", e);
        flag = -1;
 }
 return flag;
}

现在来看看下面的例子。

演示 13

这是完整的演示。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncAwaitAlternateDemonstration
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
            Console.WriteLine("***This is a modified example with three methods.***");
            Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
            /*
             This call is not awaited.So,the current method
             continues before the call is completed.
             i.e., following async call is not awaited.
             */
            _ = ExecuteTaskOne();
            Method2();
            Console.ReadKey();
        }

        private static async Task ExecuteTaskOne()
        {
            Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
            int value = await Task.Run(Method1);
            Console.WriteLine("Inside ExecuteTaskOne(), after await() call.");
            /*
            Method3() will be called if Method1()
            executes successfully(i.e. if it returns 0)
            */
            if (value == 0)
            {
                Method3();
            }
        }

        private static int Method1()
        {
            int flag = 0;
            try
            {
                Console.WriteLine("Method1() has started.");
                Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
                //Some big task
                Thread.Sleep(3000);
                Console.WriteLine("Method1() has completed its job now.");
            }
            catch (Exception e)
            {
                Console.WriteLine("Caught Exception {0}", e);
                flag = -1;
            }
            return flag;
        }
        private static void Method2()
        {
            Console.WriteLine("Method2() has started.");
            Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
            Console.WriteLine("Method2() is completed.");
        }
        private static void Method3()
        {
            Console.WriteLine("Method3() has started.");
            Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
            Console.WriteLine("Method3() is completed.");
        }
    }
}

输出

下面是一个可能的输出。

***Exploring task-based asynchronous pattern(TAP) using async and await.****
***This is a modified example with three methods.***
Inside Main().Thread ID:1
Inside ExecuteTaskOne(), prior to await() call.
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
Inside ExecuteTaskOne(), after await() call.
Method3() has started.
Inside Method3(),Thread id 4 .
Method3() is completed.

分析

仔细看看输出。你可以看到Method3()需要等待Method1()的完成,但是Method2()可以在Method1()结束执行之前完成它的执行。这里,只有当Method1()返回的值为 0 时Method3()才能继续(如果Method1()内部出现任何异常,我将标志值设置为–1)。因此,这个场景类似于演示 11 中的ContinueWith()方法。

Point to Note

在演示 13 中,请注意ExecuteTaskOne()中的以下代码行。

int value=await Task.Run(Method1);

它简单地将代码段分为两部分:前调用等待后调用等待。这个语法类似于任何同步调用,但是通过使用await(在一个async方法中),您应用了一个暂停点并使用了异步编程的力量。

我用微软的一些有趣的笔记来结束这一章。当您进一步探索 async/await 关键字时,它们会很方便。记住以下几点。

  • await运算符不能出现在 lock 语句的正文中。

  • 您可能会在一个async方法的主体中看到多个await操作符。但是如果它不存在,这不会引发任何编译时错误。相反,您会得到一个警告,并且该方法会同步执行。因此,您可能会在类似的上下文中注意到下面的警告:Warning CS1998 This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread

一大章!希望我能够揭开异步编程中不同模式的神秘面纱。尽管 IAsyncResult 模式和基于事件的异步在接下来的章节中并不推荐,但我在本章中讨论了它们,因为它们有助于您理解遗留代码,它们向您展示了异步编程的发展。你可能会发现它们在将来很有用。

我对模式的讨论到此结束。我希望你喜欢学习这些模式。现在,您已经准备好使用各种模式跳入编程的汪洋大海。下面就来探讨一下剩下的边角案例,没有实践是无法掌握的。所以,继续编码吧。*****

二十八、对设计模式的批评

设计模式让你受益于他人的经验,这通常被称为经验重用。你学习他们如何解决问题,他们如何试图在他们的系统中采用新的行为,等等。一个模式可能并不完全适合你的工作,但是如果你在开始的时候专注于最佳实践以及模式的问题,你就更有可能做出更好的应用。这就是为什么我现在将讨论设计模式的批评。了解他们可以为你提供一些真正的价值。如果你在设计软件之前批判性地思考模式,你可以在某种程度上预测你的投资回报。让我们回顾一下一些开发人员经常提出的以下几点:

  • 模式的概念来自克里斯托弗·亚历山大。他是一名建筑师,但不是计算机程序员。他考虑了多年来变化不大的领域(与软件行业相比)。相反,软件行业总是在变化,软件开发的变化比任何其他领域都要快。这就是为什么批评家经常说你不能从克里斯托弗·亚历山大考虑过的领域(建筑和城镇)出发。

  • 与今天相比,你在编程早期编写程序的方式是非常不同的。与早期的编程相比,现在你享受到了更多的便利(例如,更大的存储空间,超快的计算能力等)。因此,当您基于旧的实践提取模式时,您对它们表现出额外的尊重。

  • 许多模式都是相似的,每种模式都有利弊。(我在每章末尾的“问答环节”中讨论了它们。)一种情况下的缺点在另一种情况下可能是真正的优点。

  • 由于软件行业的不断变化,今天给你带来满意结果的模式将来可能会成为你的一个大负担。

  • 用有限数量的设计模式很难很好地设计出无限数量的需求。

  • 设计软件是一门艺术。对于最好的艺术没有定义或标准。

  • 设计模式给你的是想法,而不是实现(比如库和框架)。你知道每个人的思想都是独一无二的。因此,每个工程师可能对实现类似的概念有自己的偏好,如果心态差异很大,这可能会在团队中造成混乱。

  • 考虑一个简单的例子。模式鼓励人们编写超类型(抽象类/接口)的代码。但是对于一个简单的应用,您知道没有即将到来的变化,或者应用只是为了演示的目的而创建的,这个想法可能对您没有太大的意义。

  • 类似地,在一些较小的应用中,您可能会发现强制执行设计模式的规则会增加您的代码大小和维护成本。

  • 抹去旧的,接受新的并不总是容易的。例如,当你第一次了解到遗传时,你很兴奋。你可能想以多种方式使用它,并且只看到这个概念的好处。但是后来,当您开始尝试设计模式时,您开始了解到在许多情况下,组合比继承更受欢迎。这种编程心态的转变并不容易。

  • 设计模式基于一些关键原则,其中之一是识别可能变化的代码,然后将其从代码的其余部分中分离出来。从理论角度听起来很不错。但是在现实世界的实现中,谁能保证你的判断是完美的呢?软件行业总是在变化,它需要不断适应新的需求。

  • 许多模式已经集成到现代语言中。您可以使用语言构造中的内置支持,而不是从头开始实现模式。

  • 不恰当地使用模式会导致反模式(例如,不恰当地使用中介模式会导致 God 类反模式)。我在第二十九章中提供了反模式的概述。

  • 许多人认为设计模式的概念只是表明一种编程语言可能需要一些额外的特性。因此,随着现代编程语言能力的提高,模式变得不那么重要。维基百科说,计算机科学家 Peter Norvig 认为,GoF 设计模式中的 23 种模式中有 16 种通过 Lisp 或 Dylan 的直接语言支持被简化或消除。你可以在 https://en.wikipedia.org/wiki/Software_design_pattern 看到类似的想法。

  • 我在本书中讨论的模式完全基于面向对象编程。这些模式的效率和适用性在其他领域是有问题的。

  • 这些模式不可互换。

  • 最后,设计模式帮助你从他人的经验中获益。你了解他们的想法;您开始了解他们是如何遇到挑战的,他们是如何在他们的系统中实现新行为的,等等。但是,如果你深入到基本思想,你会发现你开始的假设是,一个初学者或相对缺乏经验的人不能比他/她的前辈更好地解决问题。有时候,一个相对缺乏经验的人比他的前辈有更好的眼光,他证明自己更有效。

问答环节

有这些模式的目录吗?

我从 GoF 的 23 种设计模式开始,然后在本书中讨论了更多的模式。GoF 的目录被认为是最基本的模式目录。

许多其他目录侧重于域。波特兰模式库和希尔赛德集团的网站在这方面是众所周知的。你可以从这些资源中获得有价值的见解和想法。希尔赛德集团网站还提供了各种会议和研讨会的信息。

作为起点,你可以参观 https://wiki.c2.com/?PortlandPatternRepositoryhttps://hillside.net/patterns/patterns-catalog

Note

在撰写本文时,本书中提到的 URL 运行良好,但它们可能会在未来发生变化。

28.2 你为什么对其他模式保持沉默?

这些是我个人的信念。

  • 计算机科学会不断发展,你会不断得到新的模式。

  • 如果您不熟悉基本模式,您就不能评估剩余的或即将到来的模式的真正需求。比如你很了解 MVC,你就能看出它和模型-视图-展示者(MVP)有什么不同,明白为什么需要 MVP。

  • 这本书已经很大了。对每个模式更详细的讨论需要更多的页面,这将使本书太大而难以消化。

因此,在这本书里,我把重点放在在今天的编程世界中仍然相关的基本模式上。

28.3 我经常看到 这个词与设计模式的描述。这是什么意思?

这是开发人员证明其开发合理性的标准。概括地说,你的目标和当前的约束是你的力量的两个重要部分。因此,当您开发您的应用时,您可以用这些部分来证明您的开发。

在各种论坛上,我看到人们在为模式的定义争论不休,并且说“模式是在一个环境中对一个问题的一个被证明的解决方案。”这是什么意思?

这是一个简单易记的模式定义。但是简单地把它分成三个部分(问题、背景和解决方案)是不够的。

比如你在去机场的路上,你很着急。突然,你意识到你把登机牌落在家里了。我们来分析一下情况。

问题:你需要准时到达机场。

背景:你把登机牌落在家里了。

你可能想到的解决办法就是折返,赶回家拿登机牌。

这个解决方案可能只工作一次,但是你能重复应用同样的程序吗?你知道答案。这不是一个明智的解决方案,因为它取决于你目前有多少时间回家拿登机牌,然后到达机场。还要看现在的流量等很多因素。所以,即使你成功了一次,你也要为将来类似的情况准备更好的解决方案。

努力学习意思、意图、上下文等等,以便清楚地理解一个模式。

当我看到两种不同模式的相似 UML 图时,我感到困惑。此外,在许多情况下,我还对模式的分类感到困惑。

这是非常自然的。您越是阅读和分析这些实现,越是试图理解这些设计背后的意图,它们之间的区别就会变得越清晰。

我应该什么时候考虑写一个新的模式?

写一个新的模式并不容易。您需要研究和评估可用的模式。但是如果您找不到现有的模式来满足您特定领域的需求,您可能需要编写自己的模式。如果您的解决方案通过了三个的规则,这是最好的,该规则说,要实现一个标签模式,一个解决方案需要在现实世界的解决方案中成功应用至少三次。一旦你做到了这一点,你就可以让其他人知道它,参与讨论论坛,并从其他人那里得到反馈。这项活动对您和开发社区都有帮助。

二十九、反模式

不讨论反模式,设计模式的讨论就不完整。下一章简要概述了反模式。我们开始吧。

概观

在现实世界的应用开发中,有时您可能会遵循一些在开始时非常吸引人的方法,但是从长远来看,它们会产生问题。例如,你可以尝试快速解决问题,以满足交货期限。但是如果你没有意识到潜在的陷阱,你可能需要为这些错误付出巨大的代价。

反模式提醒您可能导致问题的糟糕解决方案的常见错误,以便您可以采取预防措施。“预防胜于治疗”这句谚语适用于这种情况。

Points to Remember

反模式通过描述有吸引力的方法如何在将来使您的生活变得困难,提醒您常见的错误。与此同时,他们提出了一些替代解决方案,这些方案在开始时可能看起来很难,但最终会帮助你建立一个更好的解决方案。简而言之,反模式识别既定实践中的问题,它们可以将一般情况映射到特定类别的高效解决方案。他们还可以为你提供更好的计划来扭转一些不好的做法,使这些健康的解决方案。

反模式简史

设计模式的最初想法来自建筑建筑师克里斯托弗·亚历山大,他是伯克利的教授。他分享了在规划良好的城镇中建造建筑的想法。渐渐地,这些概念进入了软件开发,并通过像沃德·坎宁安和肯特·贝克这样的前沿软件开发人员而流行起来。1994 年,通过一个名为程序设计模式语言(PLoP)关于设计模式的行业会议,设计模式的思想进入了面向对象软件开发的主流。Hillside Group 主办了它,Jim Coplien 的论文“一种开发过程生成模式语言”因其上下文而闻名。随着 GoF 推出经典教材Design Patterns:Elements of Reusable Object-Oriented Software,设计模式的思想变得非常流行。

毫无疑问,这些伟大的设计模式帮助了(并且仍然在帮助)程序员开发高质量的软件。但在某些情况下,人们也开始注意到负面影响。这里有一个常见的例子。许多开发人员想要展示他们的专业知识,而没有对这些模式在他们特定领域的真实评估或结果。作为一个明显的副作用,模式被植入了错误的上下文,产生了低质量的软件,并最终导致了对开发人员或他们的组织的巨大惩罚。

因此,软件行业需要关注类似错误的负面后果,最终,反模式的思想得到了发展。许多专家开始在这一领域做出贡献,但是第一个结构良好的模型来自 Michael Akroyd 题为“反模式:针对对象误用的疫苗”的演讲这是 GoF 设计模式的对外观。

术语反模式随着威廉·布朗等人的书反模式:重构软件、架构和危机中的项目而流行起来。以下摘自该书。

因为反模式有如此多的贡献者,将反模式的最初想法分配给单一来源是不公平的。相反,反模式是补充设计模式运动和扩展设计模式模型的自然步骤。

反模式的例子

这些是反模式及其背后的概念/思维模式的一些例子。

  • 过度使用模式开发人员可能会不惜任何代价尝试使用模式,不管它是否合适。

  • 神类试图用许多不相关的方法控制几乎一切的大物体。不恰当地使用中介模式可能会导致反模式。

  • 不是在这里发明的我是一家大公司,我想从零开始打造一切。虽然已经有一个由另一家公司开发的库,但我不会使用它。我会自己做所有的东西,一旦开发出来,我会用我的品牌价值宣布,“嘿伙计们,终极图书馆已经为你们推出了。”

  • 零表示空举个常见的例子,开发者认为没有人愿意在纬度零、经度零。另一种常见的变化是当程序员使用–1,999 或类似的数字来表示不合适的整数值。另一个错误的用例是用户在应用中将“09/09/9999”视为空日期。因此,在前面的例子中,如果用户需要数字-1 或 999,或者日期“09/09/9999”,他将得不到。

  • 金锤X 先生相信技术 T 永远是最好的。所以,如果他需要开发一个新系统(这需要新的学习),他会更喜欢 T,即使它不合适。他想,“我很忙。如果我能设法应付,我就不需要再学习任何技术了。”

  • 拍信使我已经有压力了,节目截止日期快到了。测试人员 John 总是能找到难以修复的典型缺陷。另外,约翰不喜欢我,所以他喜欢在我的代码中寻找缺陷。所以,在这个阶段,我不想把他牵扯进来;他会发现更多的缺陷,而我会错过目标期限。

  • 瑞士军刀对能够满足客户各种需求的产品的需求,比如可以治愈所有疾病的药物,为具有不同需求的广大客户服务的软件——界面有多复杂并不重要。

  • 复制粘贴编程我需要解决一个问题,但我已经有了一段处理类似情况的代码。因此,我可以复制工作的旧代码,然后在需要时修改它。但是当你从一个现有的拷贝开始时,你基本上继承了所有与之相关的潜在缺陷。此外,如果将来需要修改原始代码,您需要在多个地方实现修改。这种做法也违反了不重复自己(干)的原则。

  • 架构师不编码我是一名架构师。我的时间很宝贵。我只展示路径或做关于编码的精彩讲座。有足够多的实施者应该实施我的想法。架构师打高尔夫也是这个反模式的姐妹。

  • 隐藏和悬停不要显示所有编辑或删除链接,直到用户悬停在元素上。

  • 伪装的链接和广告愚弄你的用户,当他们点击一个链接或一个广告时,他们就获得收入,尽管他们不能得到他们想要的。

  • 数字管理提交次数越多,代码行数越多,或者缺陷修复量越大,都是优秀开发人员的标志。

用代码行来衡量编程进度,就像用重量来衡量飞机制造进度。

—比尔·盖茨

Points to Note

反模式的类型

反模式可以属于不同的类别。甚至一个典型的反模式也可以属于多个类别。以下是一些常见的分类。

  • 架构反模式瑞士军刀反模式就是这一类的例子。

  • 开发反模式上帝类,过度使用模式就是这一类的例子。

  • 管理反模式拍摄信使反模式就属于这一类。

  • 组织反模式架构师不编码,打高尔夫的架构师属于这一类。

  • 用户界面反模式的例子包括伪装的链接/广告。

Note

伪装的链接/广告也被称为暗模式。

问答环节

29.1 反模式与 设计模式 有什么关系?

当你使用设计模式时,你重用了在你之前的人的经验。当你开始仅仅为了使用而盲目地使用这些概念时,你就陷入了重复使用循环解决方案的陷阱。这可能会导致您以后陷入糟糕的境地,然后您会发现您的投资回报率(ROI)在下降,但维护成本却在增加。简单来说,简单而有吸引力的解决方案(或模式)可能会在将来给你带来更多的问题。

29.2 设计模式可能会变成反模式。这是正确的吗?

是的,如果你在一个错误的环境中应用一个设计模式,会导致比它所解决的问题更多的麻烦,最终它会变成一个反模式。所以,在你开始之前,理解问题的本质和背景是非常重要的。

29.3 反模式仅与软件开发人员相关。这是正确的吗?

不。反模式的用处不仅限于开发人员。它可能适用于其他人;例如,它对管理人员和技术架构师也很有用。

29.4 即使你现在没有从反模式中获得太多好处,这些也可以帮助你在将来以更低的维护成本轻松地适应新的特性。这是正确的吗?

是的。

29.5 反模式的 原因 可能有哪些?

它们可能来自不同的来源或心态。下面列出了一些某人可能会说(或想)的常见例子。

  • “我们需要尽快交付产品。”

  • “我们与客户的关系非常好。因此,目前我们不需要分析未来的影响。”

  • “我是重用专家。我非常了解设计模式。”

  • “我们使用最新的技术和功能来打动我们的客户。我们不需要担心遗留系统。”

  • "更复杂的代码反映了我在这方面的专业知识."

29.6 你能列出一些反模式的 症状 吗?

在面向对象编程(OOP)中,最常见的症状是您的系统不容易适应新的特性。此外,维护成本持续增加。您可能还会注意到,您已经失去了关键的面向对象特性的能力,比如继承、多态等等。

除此之外,您可能会看到以下症状。

  • 全局变量的使用

  • 代码复制

  • 有限/没有代码重用

  • 一大类(神类)

  • 大量的无参数方法等。

29.7 如果检测到反模式,有什么补救措施?

你可能需要重构你的代码,找到一个更好的解决方案。例如,以下是一些针对以下反模式的解决方案。

  • 金锤试着通过一些适当的训练来教育 X 先生。

  • 零表示空值使用一个额外的布尔变量,对你来说更明智的是正确地表示空值。

  • 数字管理如果你明智地使用数字,数字就是好的。你不能根据一个程序员每周修复的缺陷数量来判断他的能力。质量也很重要。一个典型的例子是,修复一个简单的 UI 布局比修复系统中严重的内存泄漏要容易得多。考虑另一个例子。“大量测试通过”并不意味着您的系统更加稳定,除非这些测试使用不同的代码路径/分支。

  • 拍摄信使欢迎测试员约翰,并立即让他参与进来。不要把他当作你的对手。你可以适当地分析他的发现,并尽早修复真正的缺陷,以避免最后一刻的惊喜。

  • 复制粘贴编程你可以重构代码,而不是寻找快速的解决方案。您还可以创建一个公共位置来维护常用的方法,以避免重复并使维护更容易。

  • 架构师不编码让架构师参与实现阶段的某些部分。这对组织和架构师都有帮助。这让他们对产品的真正功能有了更清晰的了解。这个过程也有助于他们重视你的努力。

29.8 什么叫 重构

在编码领域,术语重构意味着改进现有代码的设计,而不改变系统/应用的外部行为。这个过程有助于您获得更具可读性的代码。同时,这些代码应该更能适应新的需求(或者变更请求),并且更易于维护。

三十、常见问题解答

本章是本书所有章节的“问答”部分的子集。这些问题中有许多没有在具体的章节中讨论,因为相关的模式还没有涉及到。除了下面的问答,我强烈推荐你浏览本书的所有“问答”部分,以便更好地理解这些模式。

30.1 你最喜欢哪个 的设计模式

这取决于许多因素,如背景、情况、需求、制约因素等等。如果你知道所有的模式,你会有更多的选择。

30.2 开发人员为什么要使用设计模式?

一个常见的答案是,它们是现实软件开发中重复出现的软件设计问题的可重用解决方案。但是我之前提到过(比如在第二十八章的问答环节),你需要分析各方面的情况,比如在你实现一个模式之前,问题的上下文和意图。

30.3 命令模式和纪念模式有什么区别?

命令模式存储所有的动作,但是记忆模式只在请求时保存状态。此外,命令模式可以支持每个动作的撤销操作,但是 Memento 模式不需要这样。我强烈建议你访问第十九章的问答 19.4,清楚地了解其中的区别。

30.4 门面模式和建造者模式有什么区别?

Facade 模式旨在使代码的特定部分更易于使用。它从开发者那里抽象出细节。

构建器模式将对象的构造与其表示分离开来。在第三章中,导演调用了同样的方法,Construct() in演示 1 和ConstructCar()演示 2,来创造不同类型的车辆。换句话说,您可以使用相同的构造过程来创建多种类型。

30.5 构建者模式和策略模式有什么区别?它们有相似的 UML 表示。

首先,你必须检查意图。构建者模式属于创造模式的范畴,而策略模式属于行为模式的范畴。他们关注的领域不同。当你考虑构建器模式时,你可以使用相同的构建过程来创建不同的类型,当你使用策略模式时,你可以在运行时自由选择算法。

30.6 命令模式和 解释器模式 有什么区别?

在命令模式中,命令是对象。在解释器模式中,命令是句子。在解释器模式中,您可以制定自己的评估规则并构建语法树。对于一个简单的语法来说,这很好,但是当你的语法很复杂的时候,就很难实现了。这是因为建立一个解释器的成本对你来说是一个大问题。

30.7 责任链模式和观察者模式有什么区别?

对于 Observer 模式,所有注册用户都被并行通知或收到请求(主题的更改)。对于责任链模式,您可能不会到达链的末端,因此所有用户不需要处理相同的场景。位于链开始的某个用户可以更早地处理请求。建议你参考问答 14.4。

30.8 责任链模式和装饰者模式的区别是什么?

它们一点也不一样,但你可能认为它们的结构相似。像 FAQ 30.7 一样,在责任链模式中,通常只有一个类处理一个请求,但是在装饰模式中,所有的类都处理一个请求。您必须记住,装饰器只在添加和删除责任的上下文中有效。如果您可以将装饰模式与单一责任原则结合起来,那么您就可以在运行时添加(或删除)单一责任。

30.9****调停者模式 观察者模式 有什么区别?

GoF 说,“这是相互竞争的模式。它们之间的区别在于,观察者通过引入观察者和主体对象来分发通信,而中介对象封装了其他对象之间的通信。”

这里我建议你考虑一下第二十一章中的中介模式的例子。在演示 2 中,我解释了如果发送者在线,他可以接收发送给目标接收者的消息。我描述了如何限制一个局外人并提高安全性。但是在观察者模式中,主体/广播者通常不关心其观察者的状态。它只是广播消息。

GoF 的书告诉我们,在制作可重用的观察者和主体时,你可能比制作可重用的中介者面临更少的挑战,但是关于交流的流程,中介者比观察者得分更高。

30.10****单胎班 静态班 你更喜欢哪个?

看情况。首先,您可以创建单例类的对象,这对于静态类是不可能的。因此,继承和多态的概念可以用单例类来实现。此外,一些开发人员认为在现实世界的应用中模仿静态类(例如,考虑单元测试场景)是具有挑战性的。

30.11 如何区分 代理 适配器

代理在与它们的主体相似的接口上工作。适配器在不同的接口上工作(对于它们所适配的对象)。

代理和装饰者有什么不同?

有不同类型的代理,它们因实现而异。因此,这些实现中的一些可能接近装饰者。例如,保护代理可以像装饰器一样实现。但是你必须记住,装饰者专注于增加责任,而代理者专注于控制对对象的访问。

30.13 中介与门面有何不同?

总的来说,两者都简化了复杂的系统。在中介模式中,中介和内部子系统之间存在双向连接。相反,在 Facade 模式中,您通常提供单向连接(子系统不知道 Facade)。

30.14 享元模式和国家模式之间有什么联系吗?

GoF 书提到 Flyweight 模式可以帮助您决定何时以及如何共享状态对象。

30.15****简单工厂 工厂方法 抽象工厂设计模式 有什么相似之处?

所有这些都封装了对象创建,这意味着您要对抽象(接口)而不是具体的类进行编码。简单地说,每个工厂都通过减少对具体类的依赖来促进松散耦合。

简单工厂、工厂方法和抽象工厂设计模式之间有什么区别?

这是你在各种工作面试中可能会面临的一个重要问题。首先参考第五章中的问答 5.3,如果需要,遍历第 4 和 5 章中的所有问答环节。

如何区分**和** 工厂方法模式

**单例模式确保您每次都能获得一个唯一的实例。它还限制创建其他实例。

但是工厂方法模式并没有说你只能得到一个唯一的实例。通常,这种模式会根据您的需要创建任意多的实例,并且这些实例不一定是唯一的。这些新类型化的实例可以实现一个公共基类。(请记住,根据 GoF 定义,工厂方法让一个类将实例化推迟到子类。)

30.18****模板方法模式 与策略模式有何不同?

在策略模式中,您可以使用委托来改变整个算法。另一方面,使用 Template Method 模式,您只改变了使用继承的算法中的某些步骤,但是算法的整体流程是不变的。

30.19 如何区分 访客模式 和策略模式?

在策略模式中,每个子类使用不同的算法来解决一个共同的问题。但是在访问者设计模式中,每个访问者子类可以提供彼此不同的功能。

30.20 空对象和代理有什么不同?

一般来说,代理在某个时间点作用于真实对象,它们也可能提供一些行为。但是空对象不做任何这样的操作。

如何区分解释者模式和访问者模式?

使用解释器模式,您可以将简单的语法表示为一个对象结构,但是在访问者模式中,您可以定义一些想要在对象结构上使用的特定操作。除此之外,解释器可以直接访问所需的属性,但是在访问者模式中,您需要特殊的功能(类似于观察者)来访问它们。

30.22 如何区分 Flyweight 模式 对象池模式

我没有在本书中讨论对象池模式。但是如果你已经知道对象池模式,你会注意到在 Flyweight 模式中,flyweights 可以有内部和外部状态。因此,如果一个 flyweight 有两种状态,那么它的状态是分开的,客户端需要将一部分状态传递给它。此外,通常,客户端不会更改固有状态,因为它是共享的。

对象池不存储外部状态的任何部分;所有状态信息都存储/封装在池化对象中。此外,客户端可以更改池化对象的状态。

30.23 库或框架与设计模式有何相似和不同之处?

它们不是设计模式。它们提供了可以直接在应用中使用的实现。但是他们可以在那些实现中使用模式的概念。

30.24 什么是 回调法

它是一种在您执行一些特定操作后可以调用的方法。您将经常在异步编程中看到这种方法的使用,当您不知道前一个操作的确切完成时间,但希望在前一个任务结束后开始某个特定任务时,这种方法会很有用。你应该参考第二十七章中的演示 7 来更好地理解它。**

第一部分:四种设计模式

第二部分:附加设计模式

第三部分:关于设计模式的最后思考

posted @ 2024-08-10 19:03  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报