User extendable visitor pattern in C#
Asked Answered
P

6

5

Is it possible to create a user extendable visitor pattern in C#? (preferably .net 3.5)

I have a set of classes in a library that I wish to add functionality to with the visitor pattern. The problem is that it is also possible for the user of the library to create their own classes. This means that you need to create a special visitor that will accept the new class types but our Accept methods are setup to receive the base type. How can I get the derived classes to call the right method in the derived visitor.

Or is there another way of doing 'if this type, do this"?

Some example code:

/* In library */
namespace VisitorPattern.System
{
   interface IThing
   {
      void Accept(SystemVisitor visitor);
      void ThingMethodA(...);
      void ThingMethodB(...);
   }

   class SystemThingA : IThing
   {
      public void Accept(SystemVisitor visitor) { visitor.Visit(this); }
      ...ThingMethods...
   }
   class SystemThingB : IThing
   {
      public void Accept(SystemVisitor visitor) { visitor.Visit(this); }
      ...ThingMethods...
   }
   class SystemThingC : IThing
   {
      public void Accept(SystemVisitor visitor) { visitor.Visit(this); }
      ...ThingMethods...
   }

   class SystemVisitor
   {
      public SystemVisitor(object specialSystemServices) { }
      public virtual void Visit(SystemThingA thing) { Console.WriteLine("SystemThingA"); }
      public virtual void Visit(SystemThingB thing) { Console.WriteLine("SystemThingB"); }
      public virtual void Visit(SystemThingC thing) { Console.WriteLine("SystemThingC"); }
      public virtual void Visit(IThing thing) { Console.WriteLine("sysvis:IThing"); }
   }
}

/* in user code */
namespace VisitorPattern.User
{
   using VisitorPattern.System;

   class UserThingA : IThing
   {
      public void Accept(SystemVisitor visitor)
      {
         var userVisitor = visitor as UserVisitor; 
         if (userVisitor == null) throw new ArgumentException("visitor"); 
         userVisitor.Visit(this);
      }
      ...ThingMethods...
   }
   class UserThingB : IThing
   {
      public void Accept(SystemVisitor visitor)
      {
         var userVisitor = visitor as UserVisitor;
         if (userVisitor == null) throw new ArgumentException("visitor");
         userVisitor.Visit(this);
      }
      ...ThingMethods...
   }
   class UserThingC : IThing
   {
      public void Accept(SystemVisitor visitor)
      {
         var userVisitor = visitor as UserVisitor;
         if (userVisitor == null) throw new ArgumentException("visitor");
         userVisitor.Visit(this);
      }
      ...ThingMethods...
   }

   // ?????
   class UserVisitor : SystemVisitor
   {
      public UserVisitor(object specialSystemServices, object specialUserServices) : base(specialSystemServices) { }

      public void Visit(UserThingA thing) { Console.WriteLine("UserThingA"); }
      public void Visit(UserThingB thing) { Console.WriteLine("UserThingB"); }
      public void Visit(UserThingC thing) { Console.WriteLine("UserThingC"); }
      public override void Visit(IThing thing) { Console.WriteLine("uservis:IThing"); }
   }

   class Program
   {
      static void Main(string[] args)
      {
         var visitor = new UserVisitor("systemservice", "userservice");
         List<IThing> mylist = new List<IThing> { new UserThingA(), new SystemThingB(), new SystemThingC(), new UserThingC() };
         foreach (var thing in mylist)
         {
            thing.Accept(visitor);
         }
      }
   }
}
Plasm answered 10/6, 2011 at 5:38 Comment(3)
Can you modify SystemVisitor.Visit(SystemThingA thing) to SystemVisitor.Visit(IThingthing)?Wyatan
Good point. Well it compiles, but I'm not sure how to get the visited objects to call the correct method in the visitor.Plasm
Replace interfaces with abstract classes and make methods virtual.Redistrict
E
3

No, it is not possible to mix the visitor pattern with visions of an extensible class hierarchy. They are mutually exclusive.

Economical answered 10/6, 2011 at 6:27 Comment(1)
Thanks for your answer, do you have any suggested alternative solutions?Plasm
M
4

Seems like you got it all backwards. First of all, let's talk about the Liskov Substitution Principle. It says that any type should be replaceable by the base type. This also apply to the visitor pattern.

If you have a method called void Accept(IVisitor visitor), it should not matter if a FancyVisitor or a SipleVisitor that is visiting.

The whole idea with the visitor pattern is that the subject (i.e. the class that is being visited) should not know anything about the visitor more than the contract (base class or interface) that it implements. And each Visitor class should be specific for a certain class being visited.

And that's the problem with your code. You are trying to make a general Visitor class that can visit all your system components. That's plain wrong.

As I see it, you have two options:

You want to collect the same kind of information from all system components.

Easy. Create a new interface which all system components implement. Then change the visitor to Visit(ISystemCompoent subject).

You want to collect different kinds of information from each system component

Then you need to create different visitor base classes (or interfaces).

Mope answered 10/6, 2011 at 6:25 Comment(1)
I believe the point of the visitor pattern was that the visitor would have specific methods for each class in the hierarchy. The correct method is called via double dispatch through the visited class's Accept method. I'm not sure but your suggestions sound more like the strategy pattern where no double dispatch is used. Because traditionally the visitor needs to know about each class in the hierarchy, it makes it difficult for an external library user to add new classes to the hierarchy and also modify the visitor interface.Plasm
E
3

No, it is not possible to mix the visitor pattern with visions of an extensible class hierarchy. They are mutually exclusive.

Economical answered 10/6, 2011 at 6:27 Comment(1)
Thanks for your answer, do you have any suggested alternative solutions?Plasm
P
1

One solution from this series of blog posts could involve using "interfaces and dynamic type casts to overcome the Visitor pattern’s problems with extensible class hierarchies"

eg:

   class UserThingC : IThing
   {
      public void Accept(SystemVisitor visitor)
      {
         var userVisitor = visitor as UserVisitor;
         if (userVisitor == null) throw new ArgumentException("visitor");
         userVisitor.Visit(this);
      }
   }

(Not saying this is the best, just an alternative)

Plasm answered 10/6, 2011 at 7:4 Comment(0)
M
1

Yes, you can do this using reflection. The main idea of using a visitor pattern is for double dispatch. Using reflection you can get all the Visit(...) methods from any visitor and invoke the right method based on the parameter type of the Visit method.

If you go this route, you won't necessarily need an inheritance hierarchy for the visitor or the element you are visiting. In fact, the element classes won't even need to know about the visitor interface (or base class).

To make it clear, below is a code sample that implements a generic visitor that uses reflection to do double dispatch. Using the GenericVisitor<T>::AcceptVisitor(...), you can get any element (derived or not) to call the right method in the any visitor (derived or not) as long as the visitor T defines a method Visit(...) for that particular element class.

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

namespace VisitorPattern
{
    class GenericVisitor<T>
    {
        // Dictionary whose key is the parameter type and value is the MethodInfo for method "Visit(ParameterType)"
        static Dictionary<Type, MethodInfo> s_visitorMethodDict;
        static GenericVisitor()
        {
            s_visitorMethodDict = new Dictionary<Type, MethodInfo>();

            Type visitorType = typeof(T);
            MethodInfo[] visitorMethods = visitorType.GetMethods();

            // Loop through all the methods in class T with the name "Visit".
            foreach (MethodInfo mi in visitorMethods)
            {
                if (mi.Name != "Visit")
                    continue;

                // Ignore methods with parameters > 1.
                ParameterInfo[] parameters = mi.GetParameters();
                if (parameters.Length != 1)
                    continue;

                // Store the method in the dictionary with the parameter type as the key.
                ParameterInfo pi = parameters[0];
                if (!s_visitorMethodDict.ContainsKey(pi.ParameterType))
                    s_visitorMethodDict.Add(pi.ParameterType, mi);
            }
        }

        public static bool AcceptVisitor(object element, T visitor)
        {
            if (element == null || visitor == null)
                return false;

            Type elementType = element.GetType();

            if (!s_visitorMethodDict.ContainsKey(elementType))
                return false;

            // Get the "Visit" method on the visitor that takes parameter of the elementType
            MethodInfo mi = s_visitorMethodDict[elementType];

            // Dispatch!
            mi.Invoke(visitor, new object[] { element });

            return true;
        }
    }

    // Element classes (note: they don't necessarily have to be derived from a base class.)
    class A { }
    class B { }

    class Visitor
    {
        public void Visit(A a) { System.Console.WriteLine("Visitor: Visited A"); }
        public void Visit(B b) { System.Console.WriteLine("Visitor: Visited B"); }
    }

    interface IVisitor
    {
        void Visit(A a);
        void Visit(B b);
    }

    class DerivedVisitor : IVisitor
    {
        public void Visit(A a) { System.Console.WriteLine("DerivedVisitor: Visited A"); }
        public void Visit(B b) { System.Console.WriteLine("DerivedVisitor: Visited B"); }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Object a = new A();
            Object b = new B();

            // Example of Visitor that doesn't use inheritance.
            Visitor v1 = new Visitor();
            GenericVisitor<Visitor>.AcceptVisitor(a, v1);
            GenericVisitor<Visitor>.AcceptVisitor(b, v1);

            // Example of Visitor that uses inheritance.
            IVisitor v2 = new DerivedVisitor();
            GenericVisitor<IVisitor>.AcceptVisitor(a, v2);
            GenericVisitor<IVisitor>.AcceptVisitor(b, v2);
        }
    }
}
Meuser answered 22/7, 2011 at 23:21 Comment(2)
Your example makes no sense to me. A derived visitor should be an interface that extends IVisitor and adds new methods for new class types added to the object graph. All of your myriad visitors expose the same set of methods which doesn't address the OP's question at all. Furthermore, you've offloaded the visitor implementation away from the class hierarchy -- the syntax should be a.Accept(v1) not GenericVisitor<Visitor>.AcceptVisitor(a, v1) -- which defeats one of the main advantages of the pattern.Economical
To specifically answer how Nick could use this in his example, he would have to change his for loop in the main function from thing.Accept(visitor); to GenericVisitor<UserVisitor>.AcceptVisitor(thing, visitor);. I agree it's not the traditional Visitor pattern but it still gives you the ability of double dispatch which is the essence of the Visitor pattern. }Meuser
L
0

You could use the new dynamic keyword like this:

public class Visitable1
{
    public void Accept(dynamic visitor)
    {
        visitor.Visit(this);
    }
}

public class DynamicVisitor
{
    public void Visit(Visitable1 token)
    {
        // Call token methods/properties
    }
}

However, you expose your code to MissingMethodException

Literature answered 23/12, 2015 at 18:55 Comment(0)
S
0

Yes, you can do this.

  1. Change all your UserThing's Accept(SystemVisitor visitor)methods to accept a UserVisitor instead.

  2. Add an abstract base class for all your UserThings

  3. In the abstract base class add an Accept method that attempts to cast the visitor from a SystemVisitor to a UserVisitor. if it succeeds it calls the Accept method on the UserThing.

    public override void Accept(SystemVisitor visitor)
    {
        var visitorAsUser = visitor as UserVisitor;
        if (visitorAsUser != null)
            return this.Accept(UserVisitor);
    }
    

The SystemVisitor still knows nothing about your UserThings and an existing SystemVisitor cannot visit them, but your UserVisitor can.

Squirt answered 4/4, 2016 at 17:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.