Talk about Visitor mode in C #

Talk about Visitor mode in C #

Write in front

Visitor mode is rarely used in daily work. If we count unfamiliar modes, it is very likely to be on the list. In addition, many articles mentioned that the visitor mode focuses on overcoming the characteristics of language single assignment, and there is little mention about when to use this mode and how this mode is spoken, resulting in many people's feeling of seeing flowers in the fog. Today, with Lao Hu, let's unveil it a little bit.
 

Mode evolution

for instance

Now suppose we have a simple requirement to count the number of words, words and pictures in a document. The number of words and words exist in the paragraph, and the number of pictures is counted separately. So we can write the first version of the code quickly

A basic Abstract version is used

    abstract class DocumentElement
    {
        public abstract void UpdateStatus(DocumentStatus status);
    }

    public class DocumentStatus
    {
        public int CharNum { get; set; }
        public int WordNum { get; set; }
        public int ImageNum { get; set; }
        public void ShowStatus()
        {
            Console.WriteLine("I have {0} char, {1} word and {2} image", CharNum, WordNum, ImageNum);
        }
    }

    class ImageElement : DocumentElement
    {
        public override void UpdateStatus(DocumentStatus status)
        {
            status.ImageNum++;
        }
    }

    class ParagraphElement : DocumentElement
    {
        public int CharNum { get; set; }
        public int WordNum { get; set; }

        public ParagraphElement(int charNum, int wordNum)
        {
            CharNum = charNum;
            WordNum = wordNum;
        }

        public override void UpdateStatus(DocumentStatus status)
        {
            status.CharNum += CharNum;
            status.WordNum += WordNum;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            DocumentStatus docStatus = new DocumentStatus();
            List<DocumentElement> list = new List<DocumentElement>();
            DocumentElement e1 = new ImageElement();
            DocumentElement e2 = new ParagraphElement(10, 20);
            list.Add(e1);
            list.Add(e2);
            list.ForEach(e => e.UpdateStatus(docStatus));
            docStatus.ShowStatus();
        }
    }

The operation results are as follows, which is very simple

However, if you look closely at this version of the code, you will find the following problems:

  • All DocumentElement derived classes must access DocumentStatus. According to the Demeter rule, this is not a good phenomenon. If DocumentStatus is modified in the future, these derived classes are likely to be affected
  • Statistics codes are scattered in different derived classes, which is inconvenient to maintain

In view of this, we have launched the second version of the code
 

The version of tpye switch is used

In this version of the code, we have abandoned the previous practice of statistics in the specific DocumentElement derived class, and directly handle it uniformly in the statistics class

    public abstract class DocumentElement
    {
        //nothing to do now
    }

    public class DocumentStatus
    {
        public int CharNum { get; set; }
        public int WordNum { get; set; }
        public int ImageNum { get; set; }
        public void ShowStatus()
        {
            Console.WriteLine("I have {0} char, {1} word and {2} image", CharNum, WordNum, ImageNum);
        }

        public void Update(DocumentElement documentElement)
        {
            switch(documentElement)
            {
                case ImageElement imageElement:
                    ImageNum++;
                    break;

                case ParagraphElement paragraphElement:
                    WordNum += paragraphElement.WordNum;
                    CharNum += paragraphElement.CharNum;
                    break;
            }
        }
    }

    public class ImageElement : DocumentElement
    {

    }

    public class ParagraphElement : DocumentElement
    {
        public int CharNum { get; set; }
        public int WordNum { get; set; }

        public ParagraphElement(int charNum, int wordNum)
        {
            CharNum = charNum;
            WordNum = wordNum;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            DocumentStatus docStatus = new DocumentStatus();
            List<DocumentElement> list = new List<DocumentElement>();
            DocumentElement e1 = new ImageElement();
            DocumentElement e2 = new ParagraphElement(10, 20);
            list.Add(e1);
            list.Add(e2);
            docStatus.ShowStatus();
        }
    }

The test results are the same as the first version of the code. This version of the code overcomes the problem that the statistical code is scattered and the specific class depends on the statistical class in the first version. Instead, we focus on the statistical tasks in the statistical class. But at the same time, it introduces type switch, which is also a bad signal, which is specifically reflected in:

  • The code is lengthy and difficult to maintain
  • If there are more derivation levels, you need to carefully select the case order to prevent the class with lower inheritance level from appearing in front of the class with further inheritance level, resulting in the situation that the later cases can never be accessed, which leads to additional energy cost
     

Try to use an overloaded version

In view of the problems of the above type switch version, as a keen programmer, someone may immediately put forward an overload scheme: "wouldn't it be ok if we wrote the corresponding Update method for each specific DocumentElement?" Like this

    public class DocumentStatus
    {
        //Omit the same code
        public void Update(ImageElement imageElement)
        {
           ImageNum++;
        }

        public void Update(ParagraphElement paragraphElement)
        {
           WordNum += paragraphElement.WordNum;
           CharNum += paragraphElement.CharNum;
        }
    }

    //Omit the same code
    class Program
    {
        static void Main(string[] args)
        {
            DocumentStatus docStatus = new DocumentStatus();
            List<DocumentElement> list = new List<DocumentElement>();
            list.Add(new ImageElement());
            list.Add(new ParagraphElement(10, 20));
            list.ForEach(e => docStatus.Update(e));
            docStatus.ShowStatus();
        }
    }

It looks good, but unfortunately, this code fails to compile. The compiler will complain that it can't convert DocumentElement to its subclass. Why? At this point, we have to mention single dispatch and double dispatch in programming language
 

Single dispatch and double dispatch

As we all know, polymorphism is one of the three basic features of OOP, that is, the following code

    public class Father
    {
	public virtual void DoSomething(string str){}
    }

    public class Son : Father
    {
	public override void DoSomething(string str){}
    }

    Father son = new Son();
    son.DoSomething();

Although Son is declared as the Father type, it will be dynamically bound to its actual type Son at runtime and call the correct rewritten function. This is polymorphic. Dynamic binding is performed through the object calling the function. In mainstream languages, such as C#, C + + and JAVA, the compiler will expand when compiling class functions, implicitly pass the this pointer to the method, and the above method will be expanded to

    void DoSomething(this, string);
    void DoSomething(this, string);

In fact, the first parameter of this function is dynamically bound at runtime, which is also the first parameter of this function.
As for double dispatch, as the name suggests, it is a dispatch method that can bind two parameters at run time. Unfortunately, C# and others do not support it, so you should be able to understand why the above code can not be compiled. The above code has become through the expansion of the compiler

    public void Update(DocumentStatus status, ImageElement imageElement)
    public void Update(DocumentStatus status, ParagraphElement imageElement)

Because C # does not support double dispatch and the second parameter cannot be dynamically resolved, even if the actual type is ImageElement, but the declared type is its base class DocumentElement, it will be rejected by the compiler.
Therefore, in order to realize double dispatch in C # which does not support double dispatch, we need to add a springboard function. Through this function, we make the second parameter act as the called object to realize dynamic binding, so as to find the correct overloaded function. We need to lead to today's protagonist, Visitor mode.

 

Visitor mode

Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate.

To be more straightforward, the Visitor pattern allows different access methods to be customized for different specific types, and the Visitor itself can also be different types. Take a look at UML

In the Visitor mode, we need to abstract visitors to facilitate customization of more different types of visitors

  • Abstract DocumentElementVisitor, which contains two versions of Visit methods, and customize access methods for different types in its subclasses
    public abstract class DocumentElementVisitor
    {
        public abstract void Visit(ImageElement imageElement);
        public abstract void Visit(ParagraphElement imageElement);
    }

    public class DocumentStatus : DocumentElementVisitor
    {
        public int CharNum { get; set; }
        public int WordNum { get; set; }
        public int ImageNum { get; set; }
        public void ShowStatus()
        {
            Console.WriteLine("I have {0} char, {1} word and {2} image", CharNum, WordNum, ImageNum);
        }

        public void Update(DocumentElement documentElement)
        {
            documentElement.Accept(this);
        }

        public override void Visit(ImageElement imageElement)
        {
            ImageNum++;
        }

        public override void Visit(ParagraphElement paragraphElement)
        {
            WordNum += paragraphElement.WordNum;
            CharNum += paragraphElement.CharNum;
        }
    }
  • Add an Accept method to the base class of the visited class. This method is used to realize double dispatch. This method is the springboard function mentioned above. Its function is to make the second parameter act as the called object and use polymorphism for the second time (the first polymorphism occurs when the Accept method is called)
    public abstract class DocumentElement
    {
        public abstract void Accept(DocumentElementVisitor visitor);
    }
    
    public class ImageElement : DocumentElement
    {
        public override void Accept(DocumentElementVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

    public class ParagraphElement : DocumentElement
    {
        public int CharNum { get; set; }
        public int WordNum { get; set; }

        public ParagraphElement(int charNum, int wordNum)
        {
            CharNum = charNum;
            WordNum = wordNum;
        }

        public override void Accept(DocumentElementVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

Here, the Accept method is the essence of the Visitor mode. By calling the Accept method of the visited base class, the visited base class dynamically binds the correct visited subclass through the single dispatch of the language. Then in the subclass method, the first parameter is called again as the execution object. According to the single dispatch mechanism of the language, the first parameter can also be dynamically bound to the correct type, so as to realize double dispatch

This is a brief introduction to the Visitor mode. The advantages of this mode are:

  • When you want to assign a subclass of different language types, you can overcome the defect that there is no right subclass of parameters, especially when you want to assign a subclass of different language types
  • It's very convenient to add visitors. Imagine that if we want to add a DocumentPriceCount in the future and need to charge for paragraphs and pictures, we just need to create a new class, inherit from DocumentVisitor and implement the corresponding Visit method at the same time

I hope that through this article, you can have a certain understanding of Visitor mode and can use it appropriately in practice.
If you have any views and opinions on this article, please leave a message in the comment area and make progress together!

Posted by nareshrevoori on Mon, 23 May 2022 11:47:23 +0300