vendredi 30 juillet 2021

Advantages of Acyclic Visitor over Command with Switch On Type

The visitor pattern is useful in situations where the element hierarchy is stable and the desired functionality for operating on those elements changes often.

In cases where the element hierarchy changes, the visitor pattern suffers from coupling that forces rebuilding all classes in both the element and functionality hierarchies.

To improve on upon this, the Acyclic Visitor uses an extra level of abstraction, with an empty Visitor interface at the top, and a specific interface for each class in the element hierarchy.

Assuming two concrete element types IntMessage and StringMessage, an acyclic visitor would look like this:

abstract class Message // parent for the model/element/data classes
{
    public abstract void Accept(Visitor visitor);
}
class IntMessage : Message  // concrete element type 1
{
    internal int data;

    public override void Accept(Visitor visitor)
    {
        // check if the concrete visitor knows how to work on IntMessage
        if (visitor is IntMessageVisitor)
            (visitor as IntMessageVisitor).Visit(this);   
    }
}
class StringMessage : Message  // concrete element type 2
{
    internal String msg;
    public override void Accept(Visitor visitor)
    {
        // check if the concrete visitor knows how to work on StringMessage
        if (visitor is StringMessageVisitor)
            (visitor as StringMessageVisitor).Visit(this);
    }
}


interface Visitor  // empty parent interface for acyclic visitor
{
}

interface IntMessageVisitor : Visitor
{
    void Visit(IntMessage message);
}
interface StringMessageVisitor : Visitor
{
    void Visit(StringMessage message);
}

A concrete visitor would inherit from all the specific visitor interfaces for the element types that it knows how to visit. The advantage of this is that in cases where new classes are added to the element hierarchy, only concrete visitors who need to visit the new element are forced to change.

class PrintVisitor : StringMessageVisitor, IntMessageVisitor
{
        public void Visit(IntMessage message)
        {
            Console.WriteLine("Int message with data = " + message.data);
        }
        public void Visit(StringMessage message)
        {
            Console.WriteLine("String message with data = " + message.msg);
        }
}

Enough setup, let's move on to the question.

The question is, given the complexity of the acyclic visitor pattern, does it have any real benefit over using a simple command with a switch-on-type?

For example, we could rewrite the PrintVisitor as the following print command:

class PrintCommand : Command
{
    public void Execute(Message message)
    {
        // switch on type
        if (message.GetType() == typeof(IntMessage))
        {
            Console.WriteLine("Int message with data = " + ((IntMessage)message).data);
        }
        else
        if (message.GetType() == typeof(StringMessage))
        {
            Console.WriteLine("Int message with data = " + ((StringMessage)message).msg);
        }
    }
}

If new classes are added to the element hierarchy in the future ( for example DateMessage ), then still only the commands that want to work on the new element type will need to change. The resulting design would be much simpler, without multiple interface inheritance, and double dispatch, at the "cost" of using runtime type information instead of virtual functions.

It seems that as far as OCP and future maintenance there is no extra cost for the switch on type over the acyclic visitor.

Is there ever any reason to prefer the ACyclic Visitor over command with switch-on-type ?

Aucun commentaire:

Enregistrer un commentaire