The author | hai-ning yu (JingFan)

preface

In software development, we may encounter different problems each time, some of which are related to e-commerce business, some of which are related to underlying data structures, and some of which may focus on performance optimization. However, there are some similarities in how we solve problems at the code level. Has anyone summed up these commonalities?

B: of course. In 1994, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides published a seminal book: Design Patterns: In this book on Reusable Object-oriented Software, we abstract away some of the common issues that people encounter in the development world, resulting in 23 classic design patterns. Many problems can be abstracted into one or more of these 23 design patterns. Because design patterns are so versatile, they have become a common language for developers, and code abstracted into design patterns is easier to understand and maintain.

Overall, design patterns fall into three categories:

  1. Creational Patterns: Create and reuse design Patterns related to objects
  2. Structural Patterns: Design patterns associated with composing and constructing objects
  3. Behavioral Patterns: Design patterns that correlate behavior between objects

The design Pattern described in this paper is Visitor Pattern, which is a kind of Behavior Pattern and is used to solve the problem of how to combine and expand objects with similar Behavior. More specifically, this paper introduces the usage scenarios, advantages and disadvantages of Visitor Pattern, as well as the Double Dispatch technology related to Visitor Pattern. At the end of the article, it explains how to solve the problem of Visitor Pattern with Pattern Matching just released by Java 14.

The problem

Suppose we have a map program with many nodes on the map, such as Building, Factory, and School, as shown below:

interface Node { String getName(); String getDescription(); // Ignore...... for the rest of the methods } class Building implements Node { ... } class Factory implements Node { ... } class School implements Node { ... }Copy the code

We need to add draw Node. It’s easy, we’ll add draw() to Node, and the rest of the implementation classes will implement this method separately. Draw () = draw(); draw() = export (); Add or modify the interface again. Interfaces, as Bridges between components, should be as stable as possible and should not change frequently. So, you want to be able to make your interface as extensible as possible, to maximize the range of functionality of your interface without constantly changing it. After some deliberation, you come up with the following solution.

Preliminary solution

Draw service (DrawService, DrawService, DrawService, DrawService, DrawService, DrawService)

public class DrawService { public void draw(Building building) { System.out.println("draw building"); } public void draw(Factory factory) { System.out.println("draw factory"); } public void draw(School school) { System.out.println("draw school"); } public void draw(Node node) { System.out.println("draw node"); }}Copy the code

Here is the class diagram:

You think you’ve solved the problem, so you’re going home from work with a little more testing:

public class App { private void draw(Node node) { DrawService drawService = new DrawService(); drawService.draw(node); } public static void main(String[] args) { App app = new App(); app.draw(new Factory()); }}Copy the code

Click Run, output:

draw node
Copy the code

What’s going on here? You take a closer look at your code: “I did pass a Factory object, should output Draw Factory”. Seriously you checked a few data again, this just discovered the reason.

Explain why

To understand why, let’s first look at the editor’s two variable type binding modes.

Taken the Dynamic/newest Binding

Let’s take a look at this code

class NodeService { public String getName(Node node) { return node.getName(); }}Copy the code

When the program runs NodeService::getName, it must determine the type of the Node parameter, whether it is Factory, School, or Building, so that it can call the getName method of the corresponding implementation class. Can the program get this information at compile time? Obviously not, because Node types can change depending on the environment in which they are running, or even from another system, and we can’t get this information at compile time. All the program can do is start, see what type Node is when it runs to getName, and call the corresponding getName() implementation to get the result. Deciding which method to call at run time (not compile time) is called Dynamic/Late Binding.

Taken the Static/Early Binding

Let’s look at another piece of code

public void drawNode(Node node) {
    DrawService drawService = new DrawService();
    drawService.draw(node);
}
Copy the code

Does the compiler know the type of node when we run drawservice.draw (node)? The runtime is sure to know, so why do we pass in a Factory and output draw Node instead of Draw Factory? We can think about it from a procedural point of view. DrawService contains only four draw methods, with arguments of Factory, Building, School, and Node. What if the caller passes in a City? After all, the caller can implement a City class itself. What method should the program call in this case? We don’t have the draw(City) method. To prevent this from happening, the program selects DrawService:: Draw (Node) directly at compile time. No matter what implementation the caller passes in, we always use the DrawService:: Draw (Node) method to ensure that the program runs safely. Deciding which method to call at compile time (not run time) is called Static/Early Binding. This explains why we exported draw Node.

Final solution

This is because the compiler does not know the type of the variable. In this case, let’s just tell the compiler what type it is. Can it be done? Of course we can do that, we check the variable types ahead of time.

if (node instanceof Building) {
    Building building = (Building) node;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) node;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) node;
    drawService.draw(school);
} else {
    drawService.draw(node);
}
Copy the code

This code works, but it’s tedious to write. We need to let the caller determine the node type and choose the method to call. Is there a better way? Visitor Pattern, which uses a method called Double Dispatch to transfer routing work from the caller to the respective implementation class so that the client doesn’t have to write the tedious judgment logic. Let’s start by looking at what the code looks like when implemented.

interface Visitor { void visit(Node node); void visit(Factory factory); void visit(Building building); void visit(School school); } class DrawVisitor implements Visitor { @Override public void visit(Node node) { System.out.println("draw node"); } @Override public void visit(Factory factory) { System.out.println("draw factory"); } @Override public void visit(Building building) { System.out.println("draw building"); } @Override public void visit(School school) { System.out.println("draw school"); } } interface Node { ... void accpet(Visitor v); } class Factory implements Node { ... @override public void accept(Visitor v) {/** * The caller knows that Visitor::visit(Factory) does exist. * So Visitor::visit(Factory) method is called */ v.visit(this); } } class Building implements Node { ... @override public void accept(Visitor v) {/** * The caller knows that Visitor::visit(Building) does exist. * So Visitor::visit(Building) method is called */ v.visit(this); } } class School implements Node { ... @override public void accept(Visitor v) {Visitor::visit(School) {Visitor::visit(School); * So Visitor::visit(School) method is called */ v.visit(this); }}Copy the code

This is how the caller uses it

Visitor drawVisitor = new DrawVisitor();
Factory factory = new Factory();
factory.accept(drawVisitor);
Copy the code

As can be seen, the Visitor Pattern actually gracefully implements the if instanceof above, so that the caller’s code is much cleaner. The overall class diagram is shown below

Why is it called Double Dispatch?

After understanding how Visitor Pattern solves this problem, some students may wonder why the technology used by Visitor Pattern is called Double Dispatch. What exactly is Double Dispatch? Before we look at Double Dispatch, what is a Single Dispatch

U Single Dispatch

Choosing different invocation methods depending on the implementation of the runtime class is called Single Dispatch, for example

String name = node.getName();
Copy the code

Are we calling Factory::getName, School::getName or Building::getName? This depends on the node implementation class, which is called Single Dispatch: Tier 1 routing

U Double Dispatch

Review our Visitor Pattern code

node.accept(drawVisitor);
Copy the code

There are two layers of routing:

  • Select the specific implementation of accept (Factory:: Accept, School:: Accept or Building:: Accept)

  • The specific method for selecting visit (in this example, there is only one DrawVisit::visit)

It takes two routes to execute the corresponding logic, which is called Double Dispatch

Advantages of Visitor Pattern

1. Visitor Pattern can increase the extensibility of the interface as much as possible without changing the interface frequently (only need to change once: add an Accept method)

As in the draw example above, suppose we now have a new requirement that requires the ability to display node information. Of course, the traditional approach would have been to add a new method showDetails() to Node, but now we don’t need to change the interface, we just need to add a new Visitor.

class ShowDetailsVisitor implements Visitor { @Override public void visit(Node node) { System.out.println("node details"); } @Override public void visit(Factory factory) { System.out.println("factory details"); } @Override public void visit(Building building) { System.out.println("building details"); } @Override public void visit(School school) { System.out.println("school details"); ShowDetailsVisitor = new showDetailsVisitor (); Factory factory = new Factory(); factory.accept(showDetailsVisitor); // factory detailsCopy the code

From this example, we can see a typical usage scenario of the Visitor Pattern: it is ideal for scenarios where interface methods are frequently added. For example, we now have 4 classes A, B, C, D, 3 methods x, y, z, horizontal drawing methods, vertical drawing classes, we can get the following figure:

               x      y      z
    A       A::x   A::y   A::z
    B       B::x   B::y   B::z
    C       C::x   C::y   C::z
Copy the code

In general our table is vertically extended, that is, we tend to add implementation classes rather than implementation methods. The Visitor Pattern is exactly suitable for another scenario: horizontal extension. Instead of adding implementation classes, we need to add interface methods frequently. The Visitor Pattern allows us to do this without frequently modifying the interface.

2. Visitor Pattern makes it easy for multiple implementation classes to share a logic

Because all implementation methods are written in one class (such as DrawVisitor), it is very convenient to have the same logic used for each type (such as Factory/Building/School) rather than repeating the logic in each interface implementation class.

Disadvantages of Visitor Pattern

  • The Visitor Pattern breaks the encapsulation of the domain model

Normally, the Factory logic is written in the Factory class, but the Visitor Pattern requires us to move part of the Factory logic (such as draw) to another class (DrawVisitor). The logic of a domain model is divided in two places. This makes the domain model difficult to understand and maintain.

  • The Visitor Pattern is partly responsible for implementing the logical coupling of classes

All the methods (draws) of the implementation classes (Factory/School/Building) are written in a single class (DrawVisitor), which is logically coupled in a way that makes code maintenance difficult.

  • Visitor Pattern makes the relationship between classes complicated and difficult to understand

As the Double Dispatch name suggests, it takes two dispatches to successfully invoke the logic: the first is to call the ACCPET method, and the second is to call the VISIT method, which becomes complicated and can be easily messed up by code maintainers.

Pattern Matching

Here’s another little interlude. Java 14 introduced the Pattern Matching feature, which has been around for years in Scala/Haskel, but many students didn’t know what it was because Java was just introduced. Therefore, before explaining the relationship between Pattern Matching and Visitor Pattern, let’s briefly introduce what Pattern Matching is. Remember we wrote this code?

if (node instanceof Building) {
    Building building = (Building) building;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) factory;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) school;
    drawService.draw(school);
} else {
    drawService.draw(node);
}
Copy the code

With Pattern Matching, we can simplify this code:

if (node instanceof Building building) {
    drawService.draw(building);
} else if (node instanceof Factory factory) {
    drawService.draw(factory);
} else if (node instanceof School school) {
    drawService.draw(school);
} else {
    drawService.draw(node);
}
Copy the code

However, Java Pattern Matching is a bit cumbersome, while Scala is better:

node match {
  case node: Factory => drawService.draw(node)
  case node: Building => drawService.draw(node)
  case node: School => drawService.draw(node)
  case _ => drawService.draw(node)
}
Copy the code

As it is relatively simple to write, many people advocate Pattern Matching as a substitute for Visitor Pattern. Personally, I think Pattern Matching looks a lot simpler. Many people think Pattern Matching is an advanced version OF the Switch Case, but it’s not. Check out TOUR OF Scala-Pattern Matching. As for the relationship between Visitor Pattern and Pattern Matching, look at Scala’s Pattern Matching = Visitor Pattern on Steroids, which I won’t repeat in this article.

References:

Scala’s Pattern Matching = Visitor Pattern on Steroids

When should I use the Visitor Design Pattern?

Design Pattern – Behavioral Patterns – Visitor

Pattern Matching for instanceof in Java 14