Dr. Yan Hong's book "JAVA and Patterns" begins with a description of the Visitor pattern:
Visitor pattern is the behavior pattern of objects. The purpose of the visitor pattern is to encapsulate some operations that are applied to certain data structure elements. Once these operations need to be modified, the data structure accepting this operation can remain unchanged.
The concept of dispatch
The type when a variable is declared is called the static type of the variable (Static Type), and some people call the static type the apparent type (Apparent Type); and the real type of the object referenced by the variable is also called the actual type of the variable (Actual Type). for example:
Copy the code code as follows:
List list = null;
list = new ArrayList();
A variable list is declared, its static type (also called obvious type) is List, and its actual type is ArrayList.
The selection of methods based on the type of object is dispatch. Dispatch is divided into two types, namely static dispatch and dynamic dispatch.
Static Dispatch occurs at compile time, and dispatch occurs based on static type information. Static dispatch is no stranger to us. Method overloading is static dispatch.
Dynamic Dispatch occurs during runtime, and dynamic dispatch dynamically replaces a method.
static dispatch
Java supports static dispatch through method overloading. Using the story of Mozi riding a horse as an example, Mozi could ride a white horse or a black horse. The class diagram of Mozi and white horse, black horse and horse is as follows:
In this system, Mozi is represented by the Mozi class. The code is as follows:
public class Mozi {
public void ride(Horse h){
System.out.println("horse riding");
}
public void ride(WhiteHorse wh){
System.out.println("riding a white horse");
}
public void ride(BlackHorse bh){
System.out.println("Ride the dark horse");
}
public static void main(String[] args) {
Horse wh = new WhiteHorse();
Horse bh = new BlackHorse();
Mozi mozi = new Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
Obviously, the ride() method of the Mozi class is overloaded from three methods. These three methods accept parameters of Horse, WhiteHorse, BlackHorse and other types respectively.
So what results will the program print when running? The result is that the program prints the same two lines of "horseback". In other words, Mozi discovered that all he was riding were horses.
Why? The two calls to the ride() method pass in different parameters, namely wh and bh. Although they have different real types, their static types are all the same, which are Horse types.
The dispatch of overloaded methods is based on static types, and this dispatch process is completed at compile time.
dynamic dispatch
Java supports dynamic dispatch through method overriding. Using the story of a horse eating grass as an example, the code is as follows:
Copy the code code as follows:
public class Horse {
public void eat(){
System.out.println("Horse eating grass");
}
}
Copy the code code as follows:
public class BlackHorse extends Horse {
@Override
public void eat() {
System.out.println("Dark horse eating grass");
}
}
Copy the code code as follows:
public class Client {
public static void main(String[] args) {
Horse h = new BlackHorse();
h.eat();
}
}
The static type of variable h is Horse, and the real type is BlackHorse. If the eat() method in the last line above calls the eat() method of the BlackHorse class, then what is printed above is "Black Horse Eating Grass"; on the contrary, if the eat() method above calls the eat() method of the Horse class , then what is printed is "horse eats grass".
Therefore, the core of the problem is that the Java compiler does not always know which code will be executed during compilation, because the compiler only knows the static type of the object, but does not know the real type of the object; and the method call is based on the object's Real types, not static types. In this way, the eat() method in the last line above calls the eat() method of the BlackHorse class, and prints "black horse eating grass".
type of dispatch
The object to which a method belongs is called the receiver of the method. The receiver of the method and the parameters of the method are collectively called the volume of the method. For example, the copy code of the Test class in the example below is as follows:
public class Test {
public void print(String str){
System.out.println(str);
}
}
In the above class, the print() method belongs to the Test object, so its receiver is also the Test object. The print() method has a parameter called str, and its type is String.
Depending on how many types of quantities dispatch can be based on, object-oriented languages can be divided into single-dispatch languages (Uni-Dispatch) and multi-dispatch languages (Multi-Dispatch). Single-dispatch languages select methods based on the type of one instance, while multi-dispatch languages select methods based on the type of more than one instance.
Both C++ and Java are single-dispatch languages, and examples of multi-dispatch languages include CLOS and Cecil. According to this distinction, Java is a dynamic single-dispatch language, because the dynamic dispatch of this language only takes into account the type of the method receiver, and it is a static multi-dispatch language, because this language dispatches overloaded methods. The type of the method's receiver and the types of all the method's parameters are taken into account.
In a language that supports dynamic single dispatch, there are two conditions that determine which operation a request will call: one is the name of the request, and the real type of the receiver. Single dispatch limits the method selection process so that only one instance can be considered, which is usually the receiver of the method. In the Java language, if an operation is performed on an object of unknown type, then the real type test of the object will only occur once. This is the characteristic of dynamic single dispatch.
double dispatch
A method decides to execute different code based on the types of two variables. This is "double dispatch". The Java language does not support dynamic multiple dispatch, which means that Java does not support dynamic double dispatch. But by using design patterns, dynamic double dispatch can also be implemented in the Java language.
In Java, two dispatches can be achieved through two method calls. The class diagram is as follows:
There are two objects in the picture, the one on the left is called West and the one on the right is called East. Now the West object first calls the East object's goEast() method, passing itself in. When the East object is called, it immediately knows who the caller is based on the parameters passed in, so the goWest() method of the "caller" object is called in turn. Through two calls, program control is handed over to two objects in turn. The sequence diagram is as follows:
In this way, there are two method calls. The program control is passed between the two objects. First, it is passed from the West object to the East object, and then it is passed back to the West object.
But just returning the ball does not solve the problem of double distribution. The key is how to use these two calls and the dynamic single dispatch function of the Java language to trigger two single dispatches during this passing process.
Dynamic single dispatch in the Java language occurs when a subclass overrides a method of a parent class. In other words, both West and East must be placed in their own type hierarchy, as shown below:
source code
The West class copy code is as follows:
public abstract class West {
public abstract void goWest1(SubEast1 east);
public abstract void goWest2(SubEast2 east);
}
SubWest1 class copy code code is as follows:
public class SubWest1 extends West{
@Override
public void goWest1(SubEast1 east) {
System.out.println("SubWest1 + " + east.myName1());
}
@Override
public void goWest2(SubEast2 east) {
System.out.println("SubWest1 + " + east.myName2());
}
}
SubWest Class 2
Copy the code code as follows:
public class SubWest2 extends West{
@Override
public void goWest1(SubEast1 east) {
System.out.println("SubWest2 + " + east.myName1());
}
@Override
public void goWest2(SubEast2 east) {
System.out.println("SubWest2 + " + east.myName2());
}
}
The East class copy code is as follows:
public abstract class East {
public abstract void goEast(West west);
}
SubEast1 class copy code code is as follows:
public class SubEast1 extends East{
@Override
public void goEast(West west) {
west.goWest1(this);
}
public String myName1(){
return "SubEast1";
}
}
SubEast2 class copy code code is as follows:
public class SubEast2 extends East{
@Override
public void goEast(West west) {
west.goWest2(this);
}
public String myName2(){
return "SubEast2";
}
}
The client class copy code is as follows:
public class Client {
public static void main(String[] args) {
//combination 1
East east = new SubEast1();
West west = new SubWest1();
east.goEast(west);
//combination 2
east = new SubEast1();
west = new SubWest2();
east.goEast(west);
}
}
The running results are as follows. Copy the code. The code is as follows:
SubWest1 + SubEast1
SubWest2 + SubEast1
When the system is running, SubWest1 and SubEast1 objects are first created, and then the client calls the goEast() method of SubEast1 and passes in the SubWest1 object. Since the SubEast1 object overrides the goEast() method of its superclass East, a dynamic single dispatch occurs at this time. When the SubEast1 object receives the call, it will get the SubWest1 object from the parameter, so it immediately calls the goWest1() method of this object and passes itself in. Since the SubEast1 object has the right to choose which object to call, another dynamic method dispatch is performed at this time.
At this time, the SubWest1 object has obtained the SubEast1 object. By calling the myName1() method of this object, you can print out your own name and the name of the SubEast object. The sequence diagram is as follows:
Since one of these two names comes from the East hierarchy and the other comes from the West hierarchy, their combination is determined dynamically. This is the implementation mechanism of dynamic double dispatch.
The structure of the visitor pattern
The visitor pattern is suitable for systems with relatively undetermined data structures. It decouples the coupling between the data structure and the operations that act on the structure, allowing the set of operations to evolve relatively freely. A simplified diagram of the visitor pattern is shown below:
Each node of the data structure can accept a call from a visitor. This node passes the node object to the visitor object, and the visitor object in turn performs the operations of the node object. This process is called "double dispatch". The node calls the visitor, passing itself in, and the visitor executes an algorithm against this node. A schematic class diagram for the Visitor pattern is shown below:
The roles involved in the visitor mode are as follows:
● Abstract visitor (Visitor) role : declares one or more method operations to form the interface that all specific visitor roles must implement.
● Concrete Visitor (ConcreteVisitor) role : implements the interface declared by the abstract visitor, that is, each access operation declared by the abstract visitor.
● Abstract node (Node) role : declares an acceptance operation and accepts a visitor object as a parameter.
● ConcreteNode role : implements the acceptance operation specified by the abstract node.
● Structure object (ObjectStructure) role : has the following responsibilities, can traverse all elements in the structure; if necessary, provide a high-level interface so that visitor objects can access each element; if necessary, can be designed as a composite object or A collection, such as List or Set.
source code
As you can see, the abstract visitor role prepares an access operation for each specific node. Since there are two nodes, there are two corresponding access operations.
Copy the code code as follows:
public interface Visitor {
/**
* Corresponds to the access operation of NodeA
*/
public void visit(NodeA node);
/**
* Corresponds to the access operation of NodeB
*/
public void visit(NodeB node);
}
The specific visitorA class copy code is as follows:
public class VisitorA implements Visitor {
/**
* Corresponds to the access operation of NodeA
*/
@Override
public void visit(NodeA node) {
System.out.println(node.operationA());
}
/**
* Corresponds to the access operation of NodeB
*/
@Override
public void visit(NodeB node) {
System.out.println(node.operationB());
}
}
The specific visitor VisitorB class copy code is as follows:
public class VisitorB implements Visitor {
/**
* Corresponds to the access operation of NodeA
*/
@Override
public void visit(NodeA node) {
System.out.println(node.operationA());
}
/**
* Corresponds to the access operation of NodeB
*/
@Override
public void visit(NodeB node) {
System.out.println(node.operationB());
}
}
The abstract node class copy code code is as follows:
public abstract class Node {
/**
* Accept operation
*/
public abstract void accept(Visitor visitor);
}
Specific node class NodeA
Copy the code code as follows:
public class NodeA extends Node{
/**
* Accept operation
*/
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
/**
*NodeA-specific method
*/
public String operationA(){
return "NodeA";
}
}
Specific node class NodeB
Copy the code code as follows:
public class NodeB extends Node{
/**
*Accept method
*/
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
/**
*NodeB-specific methods
*/
public String operationB(){
return "NodeB";
}
}
Structural object role class. This structural object role holds a collection and provides the add() method to the outside world as a management operation for the collection. By calling this method, a new node can be added dynamically.
Copy the code code as follows:
public class ObjectStructure {
private List<Node> nodes = new ArrayList<Node>();
/**
* Execute method operation
*/
public void action(Visitor visitor){
for(Node node : nodes)
{
node.accept(visitor);
}
}
/**
* Add a new element
*/
public void add(Node node){
nodes.add(node);
}
}
The client class copy code is as follows:
public class Client {
public static void main(String[] args) {
//Create a structure object
ObjectStructure os = new ObjectStructure();
//Add a node to the structure
os.add(new NodeA());
//Add a node to the structure
os.add(new NodeB());
//Create a visitor
Visitor visitor = new VisitorA();
os.action(visitor);
}
}
Although a complex object tree structure with multiple branch nodes does not appear in this schematic implementation, in actual systems the visitor pattern is usually used to handle complex object tree structures, and the visitor pattern can Used to deal with tree structure problems that span multiple hierarchies. This is where the visitor pattern is so powerful.
Preparation process sequence diagram
First, this illustrative client creates a structure object and then passes in a new NodeA object and a new NodeB object.
Secondly, the client creates a VisitorA object and passes this object to the structure object.
Then, the client calls the structure object aggregation management method to add the NodeA and NodeB nodes to the structure object.
Finally, the client calls the action method action() of the structure object to start the access process.
Access process sequence diagram
The structure object will traverse all the nodes in the collection it saves, which in this system are nodes NodeA and NodeB. First, NodeA will be accessed. This access consists of the following operations:
(1) The accept() method of the NodeA object is called and the VisitorA object itself is passed in;
(2) The NodeA object in turn calls the access method of the VisitorA object and passes in the NodeA object itself;
(3) The VisitorA object calls the unique method operationA() of the NodeA object.
Thus, the double dispatch process is completed. Then, NodeB will be accessed. The access process is the same as the access process of NodeA, which will not be described here.
Advantages of Visitor Pattern
● Good extensibility can add new functions to the elements in the object structure without modifying the elements in the object structure.
● Good reusability allows visitors to define functions common to the entire object structure, thereby improving the degree of reusability.
● Separating irrelevant behaviors You can use visitors to separate irrelevant behaviors, and encapsulate related behaviors together to form a visitor, so that the function of each visitor is relatively single.
Disadvantages of Visitor Pattern
● It is difficult to change the object structure. It is not suitable for situations where the classes in the object structure change frequently. Because the object structure changes, the visitor's interface and the visitor's implementation must change accordingly, which is too costly.
● Breaking the Encapsulation Visitor pattern usually requires the object structure to open internal data to visitors and ObjectStructrue, which breaks the encapsulation of the object.