C--设计模式-四-

78 阅读46分钟

C# 设计模式(四)

原文:Design Patterns in C#

协议:CC BY-NC-SA 4.0

十三、访问者模式

本章介绍访问者模式。

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 这种模式与策略模式 有何不同?

你找到了一个好的切入点。是的,策略和模板方法模式有相似之处。在策略中,您可以使用委托来改变整个算法;然而,模板方法模式建议您使用继承来改变算法中的某些步骤,但是算法的整体流程是不变的。