Architect's Inner Work, the most complex of the 23 design patterns for visitors

Keywords: Programming Java Spring JDK

Visitor Pattern is a design pattern that separates data structure from data operation.It refers to the encapsulation of operations that act on various elements in a data structure. It can define new operations that act on these elements without changing the data structure.

Visitor mode is called the most complex design mode and is not used frequently. The author of the design mode also commented that: Most of the time, you do not need to use the Visitor mode, but once you need it, you do.The basic idea of visitor mode is to provide an accept() method within a fixed number of object structures (elements) in the system to receive access to visitor objects.Different visitors access the same element differently, allowing the same set of elements to produce different data results.The accept () method can receive different visitor objects and internally forward itself (elements) to the visit () method of the received visitor object.The visit() method of the corresponding type within the visitor is called back to execute the operation on the element.That is, a specific element is finally passed to a specific visitor through two dynamic distributions (the first is to distribute the accept () method to the visitor and the second is to distribute the visit () method to the element.This decouples the data structure and operation without changing the state of the element.

1. Scenarios for the Visitor Mode

There are also many visitor modes in the life scene, such as KPI appraisal at the end of each year, KPI appraisal standards are relatively stable, but employees participating in KPI appraisal may change every year, so employees are visitors.We usually eat in the dining room or restaurant. The menu and the way of eating in the restaurant are relatively stable, but the people who eat in the restaurant are changing every day, so the people who eat in the restaurant are visitors.

The core of the visitor mode is to decouple the data structure from the data operations so that the operations on the elements have excellent extensibility.Different operations on the same element can be implemented by extending different types of data operations (visitors).In short, if you have multiple operations on different types of data (of a stable number of types) in your collection, use the visitor mode.

Visitor mode scenarios apply to the following scenarios:

  • The data structure is stable and the operations on the data structure are changing frequently.
  • Scenarios that require data structures separate from data operations;
  • Operations need to be performed on different data types (elements) instead of using branching to determine specific types of scenarios.

There are five main roles in the visitor pattern:

  • Visitor: An interface or abstract class crowned with the visit() method of access behavior to each specific Element whose parameters are concrete Element objects.Theoretically, the number of methods in Visitor is equal to the number of elements.If the number of elements changes frequently, the method of the Visitor will also change, which does not apply to the visitor mode.
  • ConcreteVisitor: Implement operations on specific elements;
  • Element: An interface or abstract class that defines a method that accepts visitor access, accept(), to indicate that all element types support visitor access;
  • ConcreteElement: A specific element type that provides a concrete implementation of the recipient.The usual implementation is visitor.visit(this);
  • ObjectStructure: Internally maintains a collection of elements and provides a method for accepting visitors to manipulate all elements of the collection.

1.1 Use Visitor Mode to Achieve Company KPI Assessment

At the end of each year, the management of the company begins to evaluate the performance of employees for a year. The management has CEO and CTO. The CEO is concerned with the KPI s of engineers and managers and the number of new products. The CTO is concerned with the amount of code of engineers and the number of new products of managers.

Because CEO s and CTO s have different concerns for different employees, different types of employees need to be treated differently.The visitor mode comes in handy.Let's look at the specific code implementation, starting with creating the Employee class for the employee:

public abstract class Employee {

    private String name;

    private int kpi;

    public Employee(String name) {
        this.name = name;
        this.kpi = new Random().nextInt(10);
    }

    /**
     * Receive access from visitors
     * @param visitor
     */
    public abstract void accept(IVisitor visitor);
}

The accept() method of the Employee class represents the acceptance of access by the visitor and is implemented by a specific subclass.A visitor is an interface that passes in different implementation classes and can access different data.Create Engineer Engineer and Manager classes respectively:

public class Engineer extends Employee {
    public Engineer(String name) {
        super(name);
    }

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

    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}
public class Manager extends Employee {
    public Manager(String name) {
        super(name);
    }

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

    public int getPrducts() {
        return new Random().nextInt(10);
    }
}

The engineer examines the number of codes and the manager examines the number of products. They have different responsibilities.It is this difference that enables access modes to work in this scenario.Add these employees to a business report class where top management can view the performance of all employees and create a BusinessReport class by using the showReport() method:

public class BusinessReport {

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

    public BusinessReport() {
        employeeList.add(new Engineer("Engineer 1"));
        employeeList.add(new Engineer("Engineer 2"));
        employeeList.add(new Engineer("Engineer 3"));
        employeeList.add(new Engineer("Engineer 4"));

        employeeList.add(new Manager("Product Manager 1"));
        employeeList.add(new Manager("Product Manager 2"));
    }

    /**
     *
     * @param visitor Top management, such as CEO, CTO
     */
    public void showReport(IVisitor visitor) {
        for(Employee employee : employeeList) {
            employee.accept(visitor);
        }
    }
}

Define the visitor type, create the interface IVisitor, and the visitor declares two visit() methods for engineers and managers, respectively, with the following code:

public interface IVisitor {

    void visit(Engineer engineer);

    void visit(Manager manager);

}

Specific visitor CEO Visitor and CTO Visitor classes:

public class CEOVisitor implements IVisitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println("Engineer:" + engineer.name + ", KPI: " + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("Manager:" + manager.name + ", KPI: " + manager.kpi +
                            ", New Product Quantity" + manager.getPrducts() );
    }
}
public class CTOVisitor implements IVisitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println("Engineer:" + engineer.name + ", Number of codes:" + engineer.getCodeLines());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("Manager:" + manager.name +
                            ", New Product Quantity" + manager.getPrducts() );
    }
}

Test main method:

public static void main(String[] args) {
    BusinessReport businessReport = new BusinessReport();
    businessReport.showReport(new CEOVisitor());
    businessReport.showReport(new CTOVisitor());
}

The results are as follows:

In the above case, Employee plays the Element role, while Engineer and Manger are both ConcreteElement s; CEO Visitor and CTO Visitor are specific Visitor objects; and BusinessReport is ObjectStructure.

The most important advantage of the Visitor mode is that it increases the number of visitors. As you can see from the code, if you want to add a visitor, you only need to implement a new class for the Visitor Interface to separate the data object from the data operation.If visitor mode is not practical and you don't want to operate differently on different elements, you must use if-else and type conversion, which make code upgrade-only maintenance.

We need to evaluate the appropriateness of using the visitor pattern, for example, whether our object structure is stable enough to often define new operations, and whether using the visitor pattern can optimize our code rather than complicate it.

1.2 From Static to Dynamic

When a variable is declared, the type is called the Static Type of the variable. Some people call the static type Apparent Type, while the object it refers to is really the type, which is also called the Actual Type of the variable.For example:

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

The code above declares a list whose static type (also known as the obvious type) is List, while its actual type is ArrayList.The choice of method based on the type of object is Dispatch.There are two kinds of assignment, dynamic and static.

1.2.1 Static Assignment

Static Dispatch is the assignment of a variable by its static type to determine the execution version of the method, which can be determined at compile time.The classic method of static allocation is method overload. See this code below:

public class StaticDispatch {

    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;
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.test(string);
        staticDispatch.test(integer);
    }

}

When static assignment judgment is made, we have determined the version of the method based on multiple criteria (i.e. parameter type and number), which is the concept of multiple assignment because there is more than one criterion for consideration.So the Java language is a static, multi-dispatch language.

1.2.2 Dynamic Assignment

Dynamic assignment, as opposed to static, is determined at run time, not at the version of the method during compilation.Java is a dynamic single-assignment language.

Pseudo Dynamic Double Assignment in 1.2.3 Visitor Mode

From the previous analysis, we know that Java is a static, multi-dispatched, dynamic, single-dispatched language.Dynamic double assignment is not supported at the Java bottom level.However, by using design patterns, pseudo-dynamic double-assignment can also be implemented in the Java language.What is used in visitor mode is pseudo-dynamic double-assignment.The so-called dynamic double assignment is to judge the behavior of a method at runtime based on two actual types, while the visitor mode is implemented by two dynamic single assignments to achieve this effect. Or go back to the showReport() method in the BusinessReport class in the previous corporate KPI review business scenario:

 /**
 *
 * @param visitor Top management, such as CEO, CTO
 */
public void showReport(IVisitor visitor) {
    for(Employee employee : employeeList) {
        employee.accept(visitor);
    }
}

This determines the result of the showReport() method execution and therefore the action of the accept() method based on the two actual types Employee and IVisitor.

Parsing the calling process of the accept() method 1. When the accept() method is called, it depends on the actual type of Employee to decide whether to call Engineer or Manager's accept() method.

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

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

this is an Engineer type, so the visit(Engineer enginner) method of the corresponding IVisitor interface needs to be determined based on the actual type of the visitor, so the dynamic assignment process is completed.

The above process is to dynamically assign the accept() method for the first time and the visit() method for the second visitor through two dynamic double assignments, thus reaching the result of determining the behavior of a method based on two actual types.

2. Visitor Mode in Source Code

2.1 FileVisitor interface in NIO

FileVisitor interface under NIO module in JDK, which provides support for recursive traversal of file trees.Take a look at the source code:

public interface FileVisitor<T> {

    /**
     * Invoked for a directory before entries in the directory are visited.
     *
     * <p> If this method returns {@link FileVisitResult#CONTINUE CONTINUE},
     * then entries in the directory are visited. If this method returns {@link
     * FileVisitResult#SKIP_SUBTREE SKIP_SUBTREE} or {@link
     * FileVisitResult#SKIP_SIBLINGS SKIP_SIBLINGS} then entries in the
     * directory (and any descendants) will not be visited.
     *
     * @param   dir
     *          a reference to the directory
     * @param   attrs
     *          the directory's basic attributes
     *
     * @return  the visit result
     *
     * @throws  IOException
     *          if an I/O error occurs
     */
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException;

    /**
     * Invoked for a file in a directory.
     *
     * @param   file
     *          a reference to the file
     * @param   attrs
     *          the file's basic attributes
     *
     * @return  the visit result
     *
     * @throws  IOException
     *          if an I/O error occurs
     */
    FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException;

    /**
     * Invoked for a file that could not be visited. This method is invoked
     * if the file's attributes could not be read, the file is a directory
     * that could not be opened, and other reasons.
     *
     * @param   file
     *          a reference to the file
     * @param   exc
     *          the I/O exception that prevented the file from being visited
     *
     * @return  the visit result
     *
     * @throws  IOException
     *          if an I/O error occurs
     */
    FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException;

    /**
     * Invoked for a directory after entries in the directory, and all of their
     * descendants, have been visited. This method is also invoked when iteration
     * of the directory completes prematurely (by a {@link #visitFile visitFile}
     * method returning {@link FileVisitResult#SKIP_SIBLINGS SKIP_SIBLINGS},
     * or an I/O error when iterating over the directory).
     *
     * @param   dir
     *          a reference to the directory
     * @param   exc
     *          {@code null} if the iteration of the directory completes without
     *          an error; otherwise the I/O exception that caused the iteration
     *          of the directory to complete prematurely
     *
     * @return  the visit result
     *
     * @throws  IOException
     *          if an I/O error occurs
     */
    FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException;
}

The method defined above this interface represents the critical process of traversing a file, allowing control over the entire process when the file is accessed, the directory is accessed, the directory is accessed, and errors are released.Calling a method in the interface returns the access result FileVisitResult object value, which determines what to do next when the current operation is complete.The standard return value of FileVisitResult is stored in the enumeration type:

public enum FileVisitResult {
    /**
     * Continue. When returned from a {@link FileVisitor#preVisitDirectory
     * preVisitDirectory} method then the entries in the directory should also
     * be visited.
     */
     //The current traversal will continue
    CONTINUE,
    /**
     * Terminate.
     */
     //Indicates that the current traversal process will stop
    TERMINATE,
    /**
     * Continue without visiting the entries in this directory. This result
     * is only meaningful when returned from the {@link
     * FileVisitor#preVisitDirectory preVisitDirectory} method; otherwise
     * this result type is the same as returning {@link #CONTINUE}.
     */
     //The current traversal will continue, but ignore all nodes in the current directory
    SKIP_SUBTREE,
    /**
     * Continue without visiting the <em>siblings</em> of this file or directory.
     * If returned from the {@link FileVisitor#preVisitDirectory
     * preVisitDirectory} method then the entries in the directory are also
     * skipped and the {@link FileVisitor#postVisitDirectory postVisitDirectory}
     * method is not invoked.
     */
     //The current traversal will continue, but ignore the sibling nodes of the current file/directory
    SKIP_SIBLINGS;
}

2.2 BeanDefinitionVisitor class in Spring

In Spring's Ioc there is a BeanDefinitionVisitor class, which has a visitBeanDefinition() method, look at the source code:

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

Other data is accessed in the method, such as the name of the parent class, the name of your own class, the name in the Ioc container, and so on.

3. Advantages and disadvantages of visitor mode

Advantage

  • The data structure and data operations are decoupled so that the set of operations can change independently.
  • Good scalability: different operations on datasets can be achieved by expanding the role of the visitor;
  • The specific types of elements are not single, and visitors are operational.
  • The separation of roles and responsibilities conforms to the principle of single responsibility.

shortcoming

  • Cannot add element type: If the system data structure object changes and new data objects are added frequently, the visitor class must add operations corresponding to the element type, which violates the open-close principle.

Posted by A JM on Sun, 29 Mar 2020 21:03:38 -0700