C# 设计模式(六)

原文:Design Patterns in C#

协议:CC BY-NC-SA 4.0

二十一、中介模式

本章涵盖了中介模式。

GoF 定义

定义一个封装一组对象如何交互的对象。Mediator 通过防止对象显式地相互引用来促进松散耦合,并允许您独立地改变它们的交互。

概念

中介是一组对象通过其进行通信的中介,但它们不能直接相互引用。中介负责控制和协调它们之间的交互。因此,您可以减少不同对象之间的直接互连数量。因此,使用这种模式,您可以减少应用中的耦合。

真实世界的例子

当飞机需要起飞时,会进行一系列的验证。这些类型的验证确认所有组件和单个零件(可能相互依赖)都处于完美状态。

另一个例子是当不同飞机的飞行员(他们正在接近或离开终端区域)与机场塔台通信时。他们不明确地与不同航空公司的其他飞行员交流。他们只是把他们的状态发送给控制塔。这些塔发送信号来确认谁可以起飞(或降落)。你必须注意,这些塔并不控制整个飞行。它们仅在端子区域实施约束。

计算机世界的例子

当客户端处理业务应用时,您可能需要实现一些约束。例如,假设您有一个表单,客户需要提供他们的用户 id 和密码来访问他们的帐户。在同一表单中,您可能需要提供其他必填字段,如电子邮件 ID、通信地址、年龄等。让我们假设您正在应用如下的约束。

首先,检查用户提供的用户 ID 是否有效。如果是有效的 id,则仅启用密码字段。提供这两个字段后,您可能需要检查用户是否提供了电子邮件 ID。让我们进一步假设,在提供了所有这些信息(有效的用户 id、密码、格式正确的电子邮件 ID 等等)之后,您的 Submit 按钮被启用。换句话说,如果用户提供了有效的用户 id、密码、有效的电子邮件 ID 和其他必需的详细信息,则 Submit 按钮被启用。您还可以确保用户 ID 是一个整数,因此如果用户错误地在该字段中提供了任何字符,提交按钮将保持禁用模式。在这种情况下,中介模式变得非常方便。

简而言之,当一个程序由许多类组成,并且逻辑分布在它们之间时,代码变得更难阅读和维护。在这些场景中,如果您想要对系统的行为进行新的更改,这可能会很困难,除非您使用中介模式。

履行

维基百科描述了 Mediator 模式,如图 21-1 (摘自 GoF)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了类图的重要部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-2

类图

解决方案资源管理器视图

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 更好地描绘了这一场景。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-5

案例 2:有调解人

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 个处理器的链。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 为类图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22-2

类图

解决方案资源管理器视图

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 的图表来描述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 为类图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 23-2

类图

解决方案资源管理器视图

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 为类图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 23-4

类图

解决方案资源管理器视图

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 为类图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 24-1

类图

解决方案资源管理器视图

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 24-3

由于输入无效,出现异常

问答环节

24.1 在这个例子中,我看到客户通过简单的工厂模式委托 对象的创建 。但是他们可以用 new 操作符直接创建对象。这是正确的吗?

不。这些是之前设计背后的主要原因。

  • 面向对象设计的关键原则之一是将代码中最有可能发生变化的部分与其余部分分开。

  • 在这种情况下,只有对象的创建过程会发生变化。您可以假设有代码片段来描述关于动物的一些事情,并且该部分代码不需要在客户端代码中变化。所以,在将来,如果在创建过程中需要任何更改,您只需要更改SimpleFactory类的CreateAnimal()方法。客户端代码不会因为这些更改而受到影响。

  • 您不希望在客户端主体中放置大量的if-else块(或switch语句)。这使得你的代码笨拙。

  • 客户端代码看不到您是如何创建对象的。这种抽象提高了安全性。

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

如果要添加新的动物或者删除已有的动物,需要修改CreateAnimal()方法。这个过程违反了 SOLID 原则的开放/封闭原则(即代码模块应该对扩展开放,但对修改关闭)。

Note

罗伯特·c·马丁提出了坚实的原则。有许多在线资源可用。如果你对快速介绍感兴趣,去 https://en.wikipedia.org/wiki/SOLID

24.3 你能让工厂类成为静态的吗?

可以,但是必须记住与静态类相关的限制。例如,您不能继承它们,等等。当您处理一些没有实现类或单独接口的值对象时,这是有意义的。当您使用不可变的类时,它也很有用,并且您的工厂类不需要在每次使用它时都返回一个全新的对象。

简而言之,值对象是其相等性基于值而不是身份的对象。值对象最重要的特征是,没有身份,它是不可变的。

一个简单的现实生活中的例子可以用印度的五卢比纸币和五卢比硬币来给出。它们的货币价值是相同的,但它们是不同的实例。

一般来说,静态工厂类可以提升全局状态,这对于面向对象编程来说并不理想。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值