When should you really use the visitor pattern

久未见 提交于 2019-12-21 02:02:40

问题


Ok before marking this as a duplicate let me clarify myself. I'm reading about the visitor pattern and its applicable uses.

I've stumbled upon this post: When should I use the Visitor Design Pattern?

and the user who wrote the first answer says as follow :

Now we want to add a new operation to the hierarchy, namely we want each animal to make its sound. As far as the hierarchy is this simple, you can do it with straight polymorphism:
...
But proceeding in this way, each time you want to add an operation you must modify the interface to every single class of the hierarchy.

Now, I mostly see why it's needed from his perspective, it's basically a way to cut compilation time by making it so not every time you want to add a new polymorphic method to a class hierarchy, the whole hierarchy will get recompiled.

But he's also saying that it's fine adding a new polymorphic method to the hierarchy as long as it's a "simple" hierarchy. But my question is when do you set your line and decide on what is simple and what is not.
Also, what if an hierarchy is a complex one, but adding a new method just make total sense to be and instance method rather than having the operation in a complete different class ?

Searching a bit more I found this article explaining the visitor pattern and it's uses http://butunclebob.com/ArticleS.UncleBob.IuseVisitor

The author gave an example where writing an instance method makes the object coupled to something and moving the method to a different class (The visitor) breaks the decoupling. This made more sense to me, but I'm still not quite sure when this pattern should really be used, the first argument of "changing the hierarchy time every time you want to add a new polymorphic method..." seems to me like an excuse because if a method seems to fit in the hierarchy it should be there logically, Assuming the Animal example was a really complex hierarchy and I would have decided to add make sound method, adding an instance method would be a logical choice (in my mind).
But maybe I'm wrong so I'm here asking for more insight about this, maybe someone could enlighten me.


回答1:


Visitor pattern is suitable for incases where we need double dispatch, and the programming language does not support it.

I'm a C# developer & C# does not support it directly, And I think neither does C++. (although in newer version of C#, post C# 4.0, there is dynamic key word which may do the trick).

I'll take an example to show a situation where we need double dispatch & how visitor helps us in doing so. (Since I'm a C# developer, my code base is in C#, please bear with me, but I promise, I have tried to keep it as language neutral as possible)

Example :

Lets say I have 3 types of mobile devices - iPhone, Android, Windows Mobile.

All these three devices have a Bluetooth radio installed in them.

Lets assume that the blue tooth radio can be from 2 separate OEMs – Intel & Broadcom.

Just to make the example relevant for our discussion, lets also assume that the APIs exposes by Intel radio are different from the ones exposed by Broadcom radio.

This is how my classes look –

Now, I would like to introduce an operation – Switching On the Bluetooth on mobile device.

Its function signature should like something like this –

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

So depending upon Right type of device and Depending upon right type of Bluetooth radio, it can be switched on by calling appropriate steps or algorithm.

In principal, it becomes a 3 x 2 matrix, where-in I’m trying to vector the right operation depending upon the right type of objects involved.

A polymorphic behaviour depending upon the type of both the arguments.

Now, I’ll introduce Visitor pattern to this problem. Inspiration comes from the Wikipedia page stating – “In essence, the visitor allows one to add new virtual functions to a family of classes without modifying the classes themselves; instead, one creates a visitor class that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch.”

Double dispatch is a necessity here due to the 3x2 matrix

Introducing Visitor pattern in code -

I have to make a decision first, which class hierarchy is more stable (less susceptible to change) – Device class hierarchy or the blue tooth class hierarchy. The one more stable will become the visitable classes & the less stable one will become visitor class. For this example, I’ll say the device class is more stable.

Here is the set-up

Here is client code & test code

 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);
    }
}

Here are the hierarchy of classes

     /// <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
        {

        }

The visitors follow -

/// <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");
    }
}

Let me walk through some points of this structure –

  1. I have 2 Bluetooth visitors, which contain the algorithm to switch on Bluetooth on each type of mobile device
  2. I have kept BluetoothVistor & BluetoothRadio separate so as to stick to the visitor philosophy – “Add operations without modifying the classes themselves ”. May be others would like to merge it into BluetoothRadio class itself.
  3. Each visitor has 3 functions defined – one for each type mobile device.
  4. Also here since 6 variants of algorithm exist (depending upon the type of object) double dispatch is a necessity.
  5. As I wrote above my original requirement was to have a function like this - void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio), now for double dispatch to work I have changed the signature – instead of IBlueToothRadio I use IBlueToothVisitor

Please let me know if any bit is unclear we can discuss it further.

PS: I answered a question, somewhat on the same lines but it had a different scope & references, therefor I have taken out relevant parts from my previous answer here for clarity.


Edit 1

As per the comments, I'm removing the wrapper IBlueToothVisitor

This is how the visitor pattern will look like without this wrapper -

However its still is a visitor pattern

  1. IMobileDevice is the Visitable class interface.

  2. IMobileDevice.TurnOn is the 'Accept' method of visitable class. But now it accepts IBlueToothRadio as the visitor instead of IBlueToothVisitor

  3. IBlueToothRadio becomes the new visitor class interface.

Thereby the The function signature in client is now changed to use IBlueToothRadio

  public void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)



回答2:


I think the important thing is not how complex your hierarchy is, it is how fixed the hierarchy is.

If your hierarchy is unlikely to change but the operations you want to perform on the classes is likely to change then you may have a candidate for the visitor pattern.

It is all a compromise, so it is hard to draw a "line". You just need to decide which design will be more manageable in the long run given your requirements.

For example, you may not want your Animal class to have lots of member functions like printToPDF(), getMigrationReport5(), etc and a visitor pattern would have been better.

Or perhaps, you want to be able to easily add a Tortoise to your hierarchy of Animals without breaking any existing code. In which case the visitor pattern may not be such a good idea.

There is a third option and that is to use some sort of pattern matching. But it is currently hard to do that elegantly in C++ without some external library.




回答3:


The Visitor pattern is for composing things. Basically that's when you use the visitor pattern: you have a non trivial operation which needs to compose primitive operation/properties and may need to adapt its behaviour depending on the type of value it works with. You'd put the primitive operations and properties in the class hierarchy itself, and extract the composition of those things into a visitor pattern type operation.

Essentially the visitor pattern is a crutch for languages which lack 'true' polymorphism[1] and higher order functions. Otherwise you'd simply define the composition of operations as a higher order polymorphic function foo() that simply takes helper functions as parameters to resolve the specifics.

So it's a limitation of the language more than a strength of design. If you use this sort of thing a lot to cut down on compilation times, you should consider whether you are trying to work against the tool or with the tool, i.e. whether you are doing the equivalent of a "real programmer who can program Fortran in any language". (And whether it is perhaps time to pick up/learn a different tool, besides the hammer.)

  1. By which I mean the ability of functions to work on values of "arbitrary types" without having to nail down the type to something specific (A or B), meaning the exact type/signature of the function/call changes depending on what you pass in.



回答4:


When I've seen the Visitor pattern recommended on SO, it is nearly always in order to avoid type checking. The Visitor pattern is therefore applied to a small class hierarchy, particularly when every subclass can be listed out in code.

Questions that lead to this pattern often start with, "I have a long chain of if-else statements where each condition tests for a different class from a hierarchy. How can I avoid so many if-else statements?"

For example, say I have a scheduling application with a class called Day which has seven subclasses (one for each day of the week). The naïve approach would be to implement this scheduler using if-else or switch-case.

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

The Visitor pattern avoids this chain of conditional logic by taking advantage of double dispatch. The seven days become seven methods, with the appropriate method chosen at runtime based on the type of Day argument which is passed.

Note that neither the Visitor pattern nor any other GoF pattern is used to decrease compilation time. Modifying any pre-existing interface can be difficult and error-prone. The Visitor pattern allows new type-based functionality to be added without modifying existing classes.



来源:https://stackoverflow.com/questions/33456948/when-should-you-really-use-the-visitor-pattern

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!