什么时候应该真正使用访客模式

时间:2015-10-31 21:43:28

标签: c++ design-patterns visitor

好的,在将此标记为重复之前,让我自己澄清一下。 我正在阅读访客模式及其适用用途。

我偶然发现了这篇文章: When should I use the Visitor Design Pattern?

并且编写第一个答案的用户说如下:

  

现在我们要在层次结构中添加一个新操作,即我们想要的   每只动物发出声音。至于层次结构这么简单,   你可以用直接多态性来做到这一点:   
...
  但继续这样做   方式,每次要添加操作时都必须修改   接口到层次结构的每个类。

现在,我基本上从他的角度看出为什么需要它,它基本上是一种减少编译时间的方法,因为不是每次你想要将新的多态方法添加到类层次结构时,整个层次结构都会被重新编译。

但他也说,只要它是一个“简单”的层次结构,就可以在层次结构中添加一个新的多态方法。但我的问题是你什么时候设定你的界限并决定什么是简单的,什么不是 此外,如果层次结构是一个复杂的层次结构,但添加一个新的方法只是使得和实例方法完全合理,而不是将操作放在一个完全不同的类中呢?

再搜索一下,我发现这篇文章解释了访问者模式及其用途 http://butunclebob.com/ArticleS.UncleBob.IuseVisitor

作者给出了一个示例,其中编写实例方法使对象耦合到某个东西并将该方法移动到另一个类(访问者)打破了解耦。这对我来说更有意义,但我仍然不太确定何时应该真正使用这个模式,“每次你想要添加一个新的多态方法时改变层次结构时间的第一个论点......”在我看来似乎一个借口,因为如果一个方法似乎适合层次结构它应该在逻辑上,假设动物示例是一个非常复杂的层次结构,我会决定添加make sound方法,添加一个实例方法将是一个合乎逻辑的选择(在我的记)。
但也许我错了,所以我在这里要求更多地了解这一点,也许有人可以启发我。

5 个答案:

答案 0 :(得分:6)

访问者模式适用于我们需要双重调度的情况,并且编程语言不支持它。

我是C#开发人员& C#不直接支持它,我认为C ++也不支持。 (虽然在较新版本的C#中,发布了C#4.0,但有dynamic关键字可以解决这个问题。

我将举一个例子来说明我们需要双重调度的情况。访客如何帮助我们这样做。 (因为我是C#开发人员,我的代码库是C#,请耐心等待,但我保证,我已尽力保持语言中性)

示例:

假设我有3种类型的移动设备 - iPhone,Android,Windows Mobile。

所有这三款设备都安装了蓝牙无线电。

让我们假设蓝牙收音机可以来自两个独立的原始设备制造商 - 英特尔和博通。

为了使这个例子与我们的讨论相关,我们还假设英特尔无线电公开的API与Broadcom无线电公开的API不同。

这是我的课程的样子 -

enter image description here enter image description here

现在,我想介绍一项操作 - 在移动设备上打开蓝牙。

它的功能签名应该是这样的 -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

因此,取决于正确类型的设备取决于正确类型的蓝牙无线电,可以通过调用适当的步骤或算法来打开

原则上,它变成一个3 x 2矩阵,其中我试图根据所涉及的对象的正确类型来引导正确的操作。

多态行为取决于两个参数的类型。

enter image description here

现在,我将介绍此问题的访问者模式。灵感来自维基百科页面说明 - “实质上,访问者允许一个人在不修改类本身的情况下向一个类族添加新的虚函数;相反,我们创建了一个访问者类,它实现了虚函数的所有适当的特化。访问者将实例引用作为输入,并通过双重调度实现目标。“

由于3x2矩阵

,双重调度是必要的

在代码中引入访客模式 -

我必须先做出决定,哪个类层次结构更稳定(不易改变) - 设备类层次结构或蓝牙类层次结构。 一个更稳定的将成为可访问的类和&不太稳定的人将成为访客班级。对于这个例子,我会说设备类更稳定。

这是设置

enter image description here

这是客户代码&测试代码

 class Client
  {
      public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothVisitor blueToothRadio) 
      {
          mobileDevice.TurnOn(blueToothRadio);        
      }
  }


 [TestClass]
public class VisitorPattern
{

    Client mClient = new Client();

    [TestMethod]
    public void AndroidOverBroadCom()
    {
        IMobileDevice device = new Android();
        IBlueToothVisitor btVisitor = new BroadComBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }

    [TestMethod]
    public void AndroidOverIntel()
    {
        IMobileDevice device = new Android();
        IBlueToothVisitor btVisitor = new IntelBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }

    [TestMethod]
    public void iPhoneOverBroadCom()
    {
        IMobileDevice device = new iPhone();
        IBlueToothVisitor btVisitor = new BroadComBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }

    [TestMethod]
    public void iPhoneOverIntel()
    {
        IMobileDevice device = new iPhone();
        IBlueToothVisitor btVisitor = new IntelBlueToothVisitor();

        mClient.SwitchOnBlueTooth(device, btVisitor);
    }
}

以下是类的层次结构

     /// <summary>
        /// Visitable class interface 
        /// </summary>
       interface IMobileDevice
        {
           /// <summary>
           /// It is the 'Accept' method of visitable class
           /// </summary>
            /// <param name="blueToothVisitor">Visitor Visiting the class</param>
           void TurnOn(IBlueToothVisitor blueToothVisitor);
        }

       class iPhone : IMobileDevice
       {
           public void TurnOn(IBlueToothVisitor blueToothVisitor)
           {
               blueToothVisitor.SwitchOn(this);
           }
       }

       class Android : IMobileDevice
       {
           public void TurnOn(IBlueToothVisitor blueToothVisitor)
           {
               blueToothVisitor.SwitchOn(this);
           }
       }

       class WindowsMobile : IMobileDevice
       {
           public void TurnOn(IBlueToothVisitor blueToothVisitor)
           {
               blueToothVisitor.SwitchOn(this);
           }
       }

        interface IBlueToothRadio
        {

        }

        class BroadComBlueToothRadio : IBlueToothRadio
        {

        }

        class IntelBlueToothRadio : IBlueToothRadio
        {

        }

访客跟随 -

/// <summary>
/// Wiki Page - The Visitor pattern encodes a logical operation on the whole hierarchy into a single class containing one method per type. 
/// </summary>
interface IBlueToothVisitor
{
    void SwitchOn(iPhone device);
    void SwitchOn(WindowsMobile device);
    void SwitchOn(Android device);
}


class IntelBlueToothVisitor : IBlueToothVisitor
{
    IBlueToothRadio intelRadio = new IntelBlueToothRadio();

    public void SwitchOn(iPhone device)
    {
        Console.WriteLine("Swithing On intel radio on iPhone");
    }

    public void SwitchOn(WindowsMobile device)
    {
        Console.WriteLine("Swithing On intel radio on Windows Mobile");
    }

    public void SwitchOn(Android device)
    {
        Console.WriteLine("Swithing On intel radio on Android");
    }
}

class BroadComBlueToothVisitor : IBlueToothVisitor
{
    IBlueToothRadio broadCom = new BroadComBlueToothRadio();

    public void SwitchOn(iPhone device)
    {
        Console.WriteLine("Swithing On BroadCom radio on iPhone");
    }

    public void SwitchOn(WindowsMobile device)
    {
        Console.WriteLine("Swithing On BroadCom radio on Windows Mobile");
    }

    public void SwitchOn(Android device)
    {
        Console.WriteLine("Swithing On BroadCom radio on Android");
    }
}

让我来看看这个结构的一些要点 -

  1. 我有2个蓝牙访问者,其中包含在每种类型的移动设备上打开蓝牙的算法
  2. 我保留了BluetoothVistor&amp; BluetoothRadio是分开的,以坚持访客的理念 - “添加操作而不修改类本身”。也许其他人想把它合并到BluetoothRadio类本身。
  3. 每个访问者都定义了3个功能 - 每个类型的移动设备一个。
  4. 此外,由于存在6种算法变体(取决于对象的类型)双重调度是必需的。
  5. 正如我上面写的那样,我原来的要求是有一个像这样的函数 - void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio),现在我已经改变了签名的双重调度 - 而不是IBlueToothRadio我使用IBlueToothVisitor
  6. 如果不清楚我们可以进一步讨论,请告诉我。

    PS:我回答了一个问题,有些问题在同一条线上,但它的范围和范围不同。因此,为了清楚起见,我已经从我之前的答案中取出了相关部分。

    编辑1

    根据评论,我正在移除包装器IBlueToothVisitor

    这就是没有这个包装器的访问者模式的样子 - enter image description here

    然而,它仍然是访客模式

    1. IMobileDevice是Visitable类接口。

    2. IMobileDevice.TurnOn是可访问类的“Accept”方法。但现在它接受IBlueToothRadio作为访问者而不是IBlueToothVisitor

    3. IBlueToothRadio成为新的访问者类界面。

    4. 因此,客户端中的函数签名现在已更改为使用     IBlueToothRadio

        public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
      

答案 1 :(得分:4)

我认为重要的不是层次结构的复杂程度,而是修复层次结构的方式。

如果您的层次结构不太可能改变,但您想要在上执行的操作可能会发生变化,那么您可能会有访问者模式的候选者。

这完全是妥协,因此很难画出“线”。根据您的要求,您只需确定哪种设计在长期内更易于管理。

例如,您可能不希望Animal类拥有许多成员函数,例如printToPDF()getMigrationReport5()等,访问者模式会更好。

或许,您希望能够轻松地将Tortoise添加到Animal的层次结构中,而不会破坏任何现有代码。在这种情况下,访客模式可能不是一个好主意。

还有第三种选择,即使用某种模式匹配。但是如果没有一些外部库,目前很难在C ++中优雅地做到这一点。

答案 2 :(得分:2)

访客模式用于撰写东西。基本上当你使用访问者模式时:你有一个非常重要的操作,需要组成原始操作/属性,并可能需要根据它使用的值的类型调整其行为。您将原始操作和属性放在类层次结构本身中,并将这些内容的组合提取到访问者模式类型操作中。

基本上,访客模式是缺乏“真实”的语言的拐杖。多态[1]和高阶函数。否则,您只需将操作的组合定义为更高阶的多态函数foo(),它只需将辅助函数作为参数来解析细节。

因此,语言的限制不仅仅是设计的优势。如果你大量使用这类东西来减少编译时间,你应该考虑是否正在尝试使用该工具或使用该工具,即你是否正在做一个真正的程序员,他可以用任何语言编写Fortran&#34;。 (而且除了锤子之外,是否还有时间学习和学习不同的工具。)

  1. 我指的是函数处理&#34;任意类型&#34;的值的能力。无需将类型确定为特定的类型(A或B),这意味着函数/调用的确切类型/签名会根据您传入的内容进行更改

答案 3 :(得分:0)

当我在SO上看到推荐的访问者模式时,几乎总是为了避免类型检查。因此,访问者模式应用于小类层次结构,特别是当每个子类都可以在代码中列出时。

导致这种模式的问题通常始于“我有一长串if-else语句,其中每个条件测试层次结构中不同的类。我怎样才能避免这么多if-else语句?

例如,假设我有一个名为Day的类的调度应用程序,它有七个子类(一周中的每一天)。天真的方法是使用if-else或switch-case来实现这个调度程序。

// pseudocode
if (day == Monday)
    // Monday logic
else if (day == Tuesday)
    // Tuesday logic
// etc.

访问者模式通过利用双重调度来避免这种条件逻辑链。七天成为七种方法,在运行时根据传递的Day参数的类型选择适当的方法。

请注意,访问者模式和任何其他GoF模式都不会用于减少编译时间。修改任何预先存在的界面可能很困难且容易出错。访问者模式允许添加基于类型的新功能,而无需修改现有类。

答案 4 :(得分:0)

您可以使用visitor模式来设计db saver,如下图所示: class diagram for an object saver in different dbs

您可以选择要使用的数据库, 客户端类将类似于以下内容:

    Visitor mongosaver = new MongodbSaver();
    Visitable user = new User();
    //save the user object using mongodb saver
    user.accept(mongosaver);
    Visitable post = new BlogPost();
    Visitor mysqlsaver = new MysqlSaver();
    //save the BlogPost using Mysql saver
    post.accept(mysqlsaver);

您也可以参考以下内容:https://www.oodesign.com/visitor-pattern.html