Thoroughly understand the static, dynamic, and pseudo dynamic dispatch of visitor patterns

Keywords: Java Design Pattern architecture

This article is excerpted from "design patterns should be learned this way"

1. Scenario of KPI assessment using visitor mode

At the end of each year, the management will begin to evaluate the work performance of employees for one year. Employees are divided into engineers and managers; The management includes CEO and CTO. CTO pays attention to the code quantity of engineers and the quantity of new products of managers; The CEO pays attention to the KPI of the engineer, the KPI of the manager and the number of new products.
Because CEO s and CTO s have different concerns about different employees, they need to deal with different types of employees. At this point, the visitor pattern can come in handy. Look at the code.

//Employee base class
public abstract class Employee {

    public String name;
    public int kpi;//Employee KPI

    public Employee(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    //The core method is to accept visitors' visits
    public abstract void accept(IVisitor visitor);
}

The Employee class defines the basic employee information and an accept() method. The accept() method means to accept visitors' access, which is implemented by specific subclasses. Visitor is an interface that can access different data by passing in different implementation classes. Let's look at the code of the Engineer class.

//engineer
public class Engineer extends Employee {

    public Engineer(String name) {
        super(name);
    }

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    //Code quantity of engineers in a year
    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}

The code of Manager class is as follows.

//manager
public class Manager extends Employee {

    public Manager(String name) {
        super(name);
    }

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    //Number of new products made in one year
    public int getProducts() {
        return new Random().nextInt(10);
    }
}

Engineers are assessed for code quantity and managers are assessed for new product quantity. Their responsibilities are different. It is precisely because of such differences that the access mode can play a role in this scenario. Employee, Engineer and manager are equivalent to data structures. These types are relatively stable and will not change.
Add these employees to a business report class. Company executives can view the performance of all employees through the showReport() method of the report class. The code is as follows.

//Employee Business Report
public class BusinessReport {

    private List<Employee> employees = new LinkedList<Employee>();

    public BusinessReport() {
        employees.add(new Manager("manager-A"));
        employees.add(new Engineer("engineer-A"));
        employees.add(new Engineer("engineer-B"));
        employees.add(new Engineer("engineer-C"));
        employees.add(new Manager("manager-B"));
        employees.add(new Engineer("engineer-D"));
    }

    /**
     * Show reports to visitors
     * @param visitor Senior management of the company, such as CEO and CTO
     */
    public void showReport(IVisitor visitor) {
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
    }
}

Let's look at the definition of visitor type. The visitor declares two visit() methods to access engineers and managers respectively. The code is as follows.

public interface IVisitor {

    //Access engineer type
    void visit(Engineer engineer);

    //Access manager type
    void visit(Manager manager);
}

The above code defines an ivistor interface, which has two visit() methods with parameters of Engineer and Manager respectively, that is, two different methods will be called for access to Engineer and Manager, so as to achieve the purpose of differentiation processing. The specific implementation classes of these two visitors are CEOVisitor class and ctoovisitor class. First, let's look at the code of the ceovistor class.

//CEO visitor
public class CEOVisitor implements IVisitor {

    public void visit(Engineer engineer) {
        System.out.println("engineer: " + engineer.name + ", KPI: " + engineer.kpi);
    }

    public void visit(Manager manager) {
        System.out.println("manager: " + manager.name + ", KPI: " + manager.kpi +
                ", Number of new products: " + manager.getProducts());
    }
}

Among the CEO's visitors, the CEO pays attention to the KPI of the engineer, the KPI of the manager and the number of new products, and processes them respectively through two visit() methods. If you do not use the visitor mode and only use a visit() method for processing, you need to judge in this visit() method and then process them separately. The code is as follows.

public class ReportUtil {
    public void visit(Employee employee) {
        if (employee instanceof Manager) {
            Manager manager = (Manager) employee;
            System.out.println("manager: " + manager.name + ", KPI: " + manager.kpi +
                    ", Number of new products: " + manager.getProducts());
        } else if (employee instanceof Engineer) {
            Engineer engineer = (Engineer) employee;
            System.out.println("engineer: " + engineer.name + ", KPI: " + engineer.kpi);
        }
    }
}

This leads to the nesting of if...else logic and type coercion, which is difficult to expand and maintain. When there are many types, the ReportUtil will be very complex. The visitor mode is used to process different element types through the same function, which makes the structure clearer and more flexible. Then add a CTO visitor class ctoviewer.

public class CTOVisitor implements IVisitor {

    public void visit(Engineer engineer) {
        System.out.println("engineer: " + engineer.name + ", Number of lines of code: " + engineer.getCodeLines());
    }

    public void visit(Manager manager) {
        System.out.println("manager: " + manager.name + ", Product quantity: " + manager.getProducts());
    }
}

The overloaded visit() method will perform different operations on elements, and the specific implementation of visitors can be replaced by injecting different visitors, which makes the operation on elements more flexible and extensible. At the same time, it eliminates the "ugly" codes such as type conversion and if...else.
The client test code is as follows.

public static void main(String[] args) {
        //Build report
        BusinessReport report = new BusinessReport();
        System.out.println("=========== CEO Look at the report ===========");
        report.showReport(new CEOVisitor());
        System.out.println("=========== CTO Look at the report ===========");
        report.showReport(new CTOVisitor());
}

The operation results are shown in the figure below.

In the above case, Employee plays the role of Element, Engineer and Manager are ConcreteElement, ceovistor and ctoovistor are concrete Visitor objects, and BusinessReport is ObjectStructure.
The biggest advantage of visitor mode is that it is very easy to add visitors. You can see from the code that if you want to add a visitor, you only need to implement a new class of visitor interface, so as to achieve the effect of separating data objects from data operations. If you do not use the visitor pattern and do not want to perform different operations on different elements, you must use if...else and type conversion, which makes the code difficult to upgrade and maintain.
We need to evaluate the suitability of using visitor mode according to the specific situation. For example, whether the object structure is stable enough, whether new operations need to be defined frequently, and whether using the visitor pattern can optimize the code without making the code more complex.

2 from static dispatch to dynamic dispatch

The type of a variable when it is declared is called the Static Type of the variable, and some people call the Static Type obvious type; The real type of the object referenced by the variable is also called the Actual Type of the variable. For example:

List list = null;
list = new ArrayList();

The above code declares a variable list. Its static type (also known as obvious type) is list, and its actual type is ArrayList. The selection of methods according to the type of object is Dispatch. There are two kinds of Dispatch, static Dispatch and dynamic Dispatch.

2.1 static dispatch

Static Dispatch is to dispatch according to the static type of the variable to determine the execution version of the method. Static Dispatch can determine the version of the method at compile time. The most typical application of Static Dispatch is method overloading. Look at the following code.

public class Main {
    public void test(String string){
        System.out.println("string");
    }

    public void test(Integer integer){
        System.out.println("integer");
    }

    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);
        main.test(string);
    }
}

When judging static dispatch, judge the method version according to multiple judgment criteria (i.e. parameter type and number). This is the concept of multi dispatch. Because we have more than one consideration standard, Java is a static multi dispatch language.

2.2 dynamic dispatch

For dynamic dispatch, in contrast to static dispatch, it is not the method version determined at compile time, but can be determined at run time. The most typical application of dynamic dispatch is polymorphism. For example, look at the following code.

interface Person{
    void test();
}
class Man implements Person{
    public void test(){
        System.out.println("man");
    }
}
class Woman implements Person{
    public void test(){
        System.out.println("woman");
    }
}
public class Main {
    public static void main(String[] args) {
        Person man = new Man();
        Person woman = new Woman();
        man.test();
        woman.test();
    }
}

The output result of this code is to print men and women in turn. However, the test() method version here cannot be judged according to the static types of Man and Woman. Their static types are both Person interfaces, so there is no way to judge at all.
Obviously, such output results are generated because the version of the test() method is determined at run time, which is dynamic dispatch.
The method of dynamic dispatch judgment is to obtain the actual reference types of Man and Woman at runtime, and then determine the version of the method. At this time, the judgment basis is only the actual reference type, and there is only one judgment basis, so this is the concept of single dispatch. At this time, there is only one consideration standard, that is, the actual reference type of variable. Accordingly, this shows that Java is a dynamic single dispatch language.

3 pseudo dynamic dispatch in visitor mode

Through the previous analysis, we know that Java is a static multi dispatch and dynamic single dispatch language. The Java underlying does not support dynamic double dispatch. However, by using design patterns, pseudo dynamic double dispatch can also be implemented in Java. Pseudo dynamic double dispatch is used in visitor mode. The so-called dynamic double dispatch is to judge the operation behavior of a method according to two actual types at run time, and the implementation means of visitor mode is to carry out two dynamic single dispatch to achieve this effect.
Returning to the previous KPI assessment business scenario, the code of the showReport() method in the BusinessReport class is as follows.

public void showReport(IVisitor visitor) {
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
}

Here, the execution result of the showReport() method is determined according to the actual types of Employee and ivistor, which determines the action of the accept() method.
The call process of the accept() method is analyzed as follows.

(1) When the accept() method is called, it is determined whether to call the accept() method of Engineer or Manager according to the actual type of Employee.

(2) At this time, the version of the accept() method has been determined. If it is an Engineer, its accept() method calls the following line of code.

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
		

At this time, this is the engineer type, so it corresponds to the visit(Engineer engineer) method of the ivistor interface. At this time, the version of the visit() method needs to be determined according to the actual type of the visitor. In this way, the dynamic double dispatch process is completed.
In the above process, the accept() method is dynamically dispatched for the first time and the visitor's visit() method is dynamically dispatched for the second time, so as to determine the behavior of a method according to two actual types.
The original method is usually to pass in an interface and directly use the method of the interface. This is dynamic single dispatch, just like the policy mode. Here, the visitor interface passed in by the showReport() method does not directly call its own visit() method, but dynamically dispatch once through the actual type of Employee, and then conduct its own dynamic dispatch in the method version determined after dispatch.

Note: it is determined here that the accept (ivistor visitor) method is determined by static dispatch, so this is not within the scope of this dynamic double dispatch, and the static dispatch is completed during compilation. Therefore, the static dispatch of the accept (ivistor visitor) method has nothing to do with the dynamic double dispatch of the visitor model. In the final analysis, dynamic double dispatch is still dynamic dispatch, which occurs at runtime. It is essentially different from static dispatch. It can not be said that one dynamic dispatch plus one static dispatch is dynamic double dispatch, and the double dispatch of visitor mode itself also refers to something else.

The type of this is not determined by dynamic dispatch. In which class it is written, its static type is which class. This is determined at compile time. What is uncertain is its actual type. Please distinguish it.

4 Application of visitor pattern in JDK source code

First, let's look at the FileVisitor interface under the NIO module of JDK, which provides the support of recursively traversing the file tree. The method on this interface represents the key process in the traversal process, allowing control in the process of file access, directory access, directory access, error, etc. In other words, this interface has a corresponding hook program to handle before, during and after the file is accessed, and when an error occurs.
Calling the method in FileVisitor will return the FileVisitResult object value of the access result, which is used to determine what to do after the current operation is completed. The standard return value of FileVisitResult is stored in the FileVisitResult enumeration type. The code is as follows.

public interface FileVisitor<T> {

    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException;

    FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException;

    FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException;

    FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException;
}

(1) FileVisitResult.CONTINUE: this access result indicates that the current traversal process will continue.

(2)FileVisitResult.SKIP_SIBLINGS: this access result indicates that the current traversal process will continue, but the sibling nodes of the current file / directory should be ignored.

(3)FileVisitResult.SKIP_SUBTREE: this access result indicates that the current traversal process will continue, but all nodes in the current directory should be ignored.

(4) FileVisitResult.TERMINATE: this access result indicates that the current traversal process will stop.

It is convenient for visitors to traverse the file tree, such as finding files that meet certain conditions in a folder or files created in a certain day. This class provides corresponding methods. Its implementation is also very simple. The code is as follows.

public class SimpleFileVisitor<T> implements FileVisitor<T> {
    protected SimpleFileVisitor() {
    }

    @Override
    public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(file);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(file);
        throw exc;
    }

    @Override
    public FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(dir);
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }
}

5 Application of visitor pattern in Spring source code

Let's look at the application of visitor pattern in Spring. There is a BeanDefinitionVisitor class in Spring IoC, including a visitBeanDefinition() method. The source code is as follows.


public class BeanDefinitionVisitor {

	@Nullable
	private StringValueResolver valueResolver;


	public BeanDefinitionVisitor(StringValueResolver valueResolver) {
		Assert.notNull(valueResolver, "StringValueResolver must not be null");
		this.valueResolver = valueResolver;
	}

	protected BeanDefinitionVisitor() {
	}

	public void visitBeanDefinition(BeanDefinition beanDefinition) {
		visitParentName(beanDefinition);
		visitBeanClassName(beanDefinition);
		visitFactoryBeanName(beanDefinition);
		visitFactoryMethodName(beanDefinition);
		visitScope(beanDefinition);
		if (beanDefinition.hasPropertyValues()) {
			visitPropertyValues(beanDefinition.getPropertyValues());
		}
		if (beanDefinition.hasConstructorArgumentValues()) {
			ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
			visitIndexedArgumentValues(cas.getIndexedArgumentValues());
			visitGenericArgumentValues(cas.getGenericArgumentValues());
		}
	}
	...
}

We can see that in the visitBeanDefinition() method, other data are accessed, such as the name of the parent class, its own class name, the name in the IoC container and other information.
Focus on WeChat's official account of Tom architecture and reply to the "design pattern" to get the complete source code.

[recommendation] Tom bomb architecture: 30 real cases of design patterns (with source code attached). Challenging the annual salary of 60W is not a dream

This article is the original of "Tom bomb architecture". Please indicate the source for reprint. Technology lies in sharing, I share my happiness!
If this article is helpful to you, you are welcome to pay attention and praise; If you have any suggestions, you can also leave comments or private letters. Your support is the driving force for me to adhere to my creation. Focus on WeChat official account Tom structure, get more dry cargo!

Posted by Invincible on Wed, 24 Nov 2021 00:50:18 -0800